feat: migrate voice app, add otel/rum, dockerfile, k8s deployment with ingress
Build and Publish / Metadata (push) Successful in 1s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 18s

This commit is contained in:
mats committed 2026-09-22 09:43:47 +02:00
1 parent a0d5a9a2c7
commit 5757156843
8170 files changed
+3141 -2086520

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
node_modules
.git
.codesearch
test.js
k8s/
.gitea/
+53
View File
@@ -0,0 +1,53 @@
name: Build and Publish
run-name: ${{ gitea.actor }} build 🚀
on: [push]
jobs:
Metadata:
runs-on: amd64
outputs:
version: ${{ steps.meta.outputs.version }}
git_commit: ${{ steps.meta.outputs.git_commit }}
build_date: ${{ steps.meta.outputs.build_date }}
steps:
- name: Checkout
uses: actions/checkout@v4
- id: meta
name: Derive build metadata
run: |
GIT_COMMIT=$(git rev-parse HEAD)
if git describe --tags --exact-match >/dev/null 2>&1; then
VERSION=$(git describe --tags --exact-match)
else
VERSION=$(git rev-parse --short=12 HEAD)
fi
BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "git_commit=$GIT_COMMIT" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "build_date=$BUILD_DATE" >> $GITHUB_OUTPUT
BuildAndDeployAmd64:
needs: Metadata
runs-on: amd64
steps:
- uses: actions/checkout@v4
- name: Build amd64 image
run: |
docker build \
--build-arg APP_RELEASE=${{ needs.Metadata.outputs.version }} \
--build-arg VERSION=${{ needs.Metadata.outputs.version }} \
--build-arg GIT_COMMIT=${{ needs.Metadata.outputs.git_commit }} \
--build-arg BUILD_DATE=${{ needs.Metadata.outputs.build_date }} \
--progress=plain \
-t matst80/gibbz:latest \
.
- name: Push amd64 images
run: |
docker push matst80/gibbz:latest
- name: Deploy Kubernetes manifests
run: |
kubectl apply -f k8s/deployment.yaml
- name: Rollout amd64 gibbz deployment
run: |
kubectl rollout restart deployment/gibbz -n home
kubectl rollout status deployment/gibbz -n home
+1
View File
@@ -1 +1,2 @@
node_modules/
.codesearch/
+28
View File
@@ -0,0 +1,28 @@
ARG APP_RELEASE=dev
FROM node:22-alpine AS app
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY server.js tracing.js ./
COPY public/ ./public/
# Inject build release into index.html if APP_RELEASE is specified
ARG APP_RELEASE
ENV APP_RELEASE=${APP_RELEASE}
RUN if [ -n "${APP_RELEASE}" ]; then \
sed -i "s/window.__APP_RELEASE__ || \"dev\"/\"${APP_RELEASE}\"/g" ./public/index.html; \
fi
ENV PORT=8080 \
HOST=0.0.0.0
EXPOSE 8080
USER node
CMD ["node", "server.js"]
+45
View File
@@ -0,0 +1,45 @@
IMAGE ?= matst80/gibbz:latest
NAMESPACE ?= home
VERSION ?= $(shell git describe --tags --exact-match 2>/dev/null || git rev-parse --short=12 HEAD)
GIT_COMMIT ?= $(shell git rev-parse HEAD)
BUILD_DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
.PHONY: all help build test check docker-build docker-push deploy rollout status logs
all: check
help: ## Show target help
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}'
test: ## Run local tests
npm test
check: test ## Quality gate: run all tests
build: check ## Build local dependencies and run tests
npm ci
docker-build: ## Build docker image
docker build \
--build-arg APP_RELEASE=$(VERSION) \
--build-arg VERSION=$(VERSION) \
--build-arg GIT_COMMIT=$(GIT_COMMIT) \
--build-arg BUILD_DATE=$(BUILD_DATE) \
-t $(IMAGE) \
.
docker-push: ## Push docker image
docker push $(IMAGE)
deploy: ## Apply Kubernetes manifests to the cluster
kubectl apply -f k8s/deployment.yaml
rollout: ## Trigger rolling restart of the deployment
kubectl rollout restart deployment/gibbz -n $(NAMESPACE)
kubectl rollout status deployment/gibbz -n $(NAMESPACE)
status: ## Check deployment and pod status
kubectl get pods,svc,ingress -l app=gibbz -n $(NAMESPACE)
logs: ## Tail pod logs
kubectl logs -l app=gibbz -n $(NAMESPACE) -f
View File
File renamed without changes.
-43
View File
@@ -1,43 +0,0 @@
# Copy to .env (cmd/api autoloads it via godotenv when run from backend/).
# Every value below is the default from internal/config — uncomment to override.
# HTTP listen address.
#API_ADDR=:8080
# Database: a local SQLite file by default (pure-Go driver; WAL +
# busy_timeout are auto-applied unless the DSN has its own _pragma params).
# For Postgres instead: `make db-up`, then use the DSN below.
#DATABASE_URL=sqlite:dev.db
#DATABASE_URL=postgres://fullstack:fullstack@localhost:5432/fullstack?sslmode=disable
# Comma-separated CORS origins allowed to call the API from a browser.
# "*" allows every origin.
#CORS_ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
# debug | info | warn | error
#LOG_LEVEL=info
# json | text
#LOG_FORMAT=json
# Print every SQL query bun runs (bundebug).
#DB_DEBUG=false
# --- OIDC auth (issuer: https://users.k6n.net) ---
# Auth is ENABLED when OIDC_CLIENT_ID is set: /api/auth/login starts the
# authorization-code + PKCE flow, /api/v1/* then requires the session cookie.
# Leave both empty to run standalone (no /api/auth routes, /api/v1 open).
# The registered redirect URIs for this client cover
# https://*.preview.k6n.net/api/auth/callback and localhost on any port
# (loopback exception), so dev and preview hosts both work.
#OIDC_ISSUER=https://users.k6n.net
#OIDC_CLIENT_ID=fullstack-template
# Secret from the issuer's client registry — NOT safe to commit (keep it in .env).
#OIDC_CLIENT_SECRET=
# Secret used to sign session cookies. Unset = random per boot (all sessions
# are dropped on restart). Set a stable value in production.
#AUTH_SESSION_SECRET=
# Session lifetime as a Go duration.
#AUTH_SESSION_TTL=168h
-6
View File
@@ -1,6 +0,0 @@
bin/
.env
*.out
*.db
*.db-wal
*.db-shm
-154
View File
@@ -1,154 +0,0 @@
# Backend — agent handbook
Go 1.27 API server: stdlib `net/http` (Go 1.22+ method patterns +
`r.PathValue`), Bun ORM (SQLite file by default, Postgres via DSN), layered
so each change touches a predictable set of files. Follow this handbook
unless the user says otherwise.
## Commands
| Task | Command |
|---|---|
| Full gate: fmt-check + vet + tests | `make check` — run before declaring any change done |
| Dev server | `make run` from `backend/` (SQLite: no setup, `make migrate-up` once) |
| Tests | `make test` (`go test ./...`; no database server required) |
| Optional Postgres up / down | `make db-up` / `make db-down` (set `DATABASE_URL=postgres://…`) |
| Migrations | `make migrate-up` / `make migrate-down` / `make migrate-status` |
| New migration | `make migrate-new name=create_projects` |
| Binaries | `make build` (writes `bin/`) |
No linters beyond `go vet` and `gofmt` are configured — don't add config
files for other tools unless asked.
## Architecture — dependencies flow one way
```
cmd/api (wiring only)
│
▼
internal/api ──HTTP──▶ internal/items.Service ──▶ items.Repository (interface)
(transport) (business rules) ▲
BunRepository (SQL)
```
- `internal/api` — transport only: route patterns, parse input, call the
service, map errors to status codes. No SQL, no business rules.
- `internal/items/service.go` — validation and business rules. Depends on
the `Repository` interface, never on `*bun.DB`; that is what makes the
tests fake-backed.
- `internal/items/repository.go` — the only file speaking Bun/SQL.
- `internal/config` — the only file reading `os.Getenv`.
- `cmd/*` — wiring and process concerns (signals, exit codes). No logic.
Imports may only point rightward in the diagram. `internal/api` must never
import `internal/database`, and nothing imports `internal/api` except `cmd`.
## Adding a resource (the recipe)
`items` is the reference. For a new resource `projects`:
1. `internal/projects/model.go` — struct with `bun:"table:…,alias:…"` tags,
json tags, `Repository` interface, `ErrNotFound`, `ValidationError`.
2. `internal/projects/repository.go` — `BunRepository` implementing it.
3. `internal/projects/service.go` — `Service`, `CreateInput`, pointer-field
`UpdateInput`, validation.
4. `make migrate-new name=create_projects`, then fill in the generated Go
migration in `internal/database/migrations/` (model-driven DDL, plus a
down migration).
5. `internal/api/projects.go` — handler struct + `fail` error mapping.
6. `internal/api/server.go` — register routes on the mux.
7. Tests: `projects/service_test.go` (fake repo) + extend
`api/server_test.go`.
8. `make check`.
## HTTP conventions
- Routes are method-prefixed ServeMux patterns (`"PATCH /api/v1/projects/{id}"`);
read segments with `r.PathValue("id")`, parse via the `pathID` helper.
Wrong methods get 405 + `Allow` — the `jsonNotFounds` wrapper in
server.go keeps that mux behavior, so don't register catch-all routes.
- Success bodies are the resource JSON directly; lists are bare arrays.
Create answers 201 + `Location`; delete answers 204.
- Errors are always `{"error":{"code","message"}}`. Codes live in
`internal/api/respond.go`: `invalid_json`, `validation_failed`,
`not_found`, `invalid_id`, `method_not_allowed`, `internal_error`,
`timeout`, `unavailable`.
Map domain errors only in the handler's `fail` — sentinel `ErrNotFound`
→ 404, `*ValidationError` → 400, everything else → 500 (logged, never
leaked to the client).
- Decode request bodies with `decodeJSON` (strict: unknown fields and
>1 MiB bodies are 400s). List endpoints take `?limit=&offset=` and
resource-specific filters; defaults and caps live in the service.
- Middleware chain (outermost first): logging → recover → cors → timeout
→ requireAuth (only when OIDC is configured). Add transport-level concerns
there, not in handlers.
## Auth
OIDC authorization-code + PKCE against the issuer in `OIDC_ISSUER`
(default `https://users.k6n.net`). Enabled iff `OIDC_CLIENT_ID` is set;
otherwise the server runs standalone (no `/api/auth` routes, `/api/v1` open)
and logs that loudly. Pieces:
- `internal/auth` — the OIDC client (discovery-driven endpoints, never
hardcoded paths) and the cookie layer: session + login-transaction values
are `<base64url(json)>.<base64url(hmac-sha256)>` sealed by `Signer`.
Tokens stay server-side; the browser only ever holds the session cookie.
- `internal/api/auth.go` — the four routes: `GET /api/auth/session`
(always 200), `GET /api/auth/login?returnTo=`, `GET /api/auth/callback`,
`POST /api/auth/logout`. The callback path `/api/auth/callback` is fixed;
the redirect URI is derived per request from `X-Forwarded-Host`/`Proto`
(set by the Vite proxy) so localhost, `*.preview.k6n.net`, and the API
origin all work with one client registration.
- `requireAuth` guards `/api/v1/*` with the session cookie (unknown
sub-routes answer 401, not 404). It is skipped in standalone mode and in
`api.New` tests that omit `Deps.Auth`.
- Login failures redirect to `/?auth_error=<code>` — the SPA reads that;
details go to server logs only. `returnTo` is sanitized to a relative
path (no `//`, no control chars) to stay an internal redirect.
## Migrations
- One Go file per migration in `internal/database/migrations/`, named
`<YYYYMMDDHHMMSS>_<snake_name>.go`, registering itself via
`Migrations.MustRegister(up, down)` in `init()`. `make migrate-new
name=<snake_name>` scaffolds it with a fresh timestamp — never invent
timestamps by hand. Apply with `go run ./cmd/migrate up` (also `down`,
`status`).
- Prefer model-driven DDL (`db.NewCreateTable().Model((*pkg.Thing)(nil))`):
bun emits dialect-correct SQL for SQLite and Postgres, so migrations stay
portable across the default file database and a real Postgres. Use
`db.Exec` with raw SQL only when a model can't express it.
- Never edit or reorder an applied migration — add a new one. Every
migration gets a down migration.
- `internal/database/database_test.go` runs all migrations plus the items
repository against in-memory SQLite — extend it when adding resources so
dialect drift fails `make check`, not production.
## Databases
- Default: `DATABASE_URL=sqlite:dev.db` — a local file (modernc pure-Go
driver, no CGO, WAL + busy_timeout applied automatically unless the DSN
carries its own `_pragma` params). Zero setup for iteration.
- Postgres: `make db-up` then `DATABASE_URL=postgres://fullstack:fullstack@localhost:5432/fullstack?sslmode=disable`.
The repository layer uses portable queries (`?` placeholders, no
dialect-specific SQL) — if you must, branch on `db.Dialect().Name()`.
## Testing
- Three layers: `internal/<pkg>/service_test.go` drives business rules
through an in-memory fake of the Repository interface;
`internal/api/server_test.go` drives the real mux + middleware with the
same fake behind the real service; `internal/database/database_test.go`
runs migrations + the real BunRepository against in-memory SQLite.
- Fakes are package-local and duplicated on purpose — don't build a shared
testutil package until there are enough resources to justify it.
- Table-driven tests for error cases; one flow test per handler group.
## Config
- Env-only; `.env` in backend/ is autoloaded by `cmd/api`. Add a field in
`internal/config/config.go`, a line in `.env.example`, and a row in the
README table.
- CORS allows the Vite dev server (`http://localhost:5173`) by default so
the frontend can call this API during development.
-52
View File
@@ -1,52 +0,0 @@
# Backend tasks — run from backend/. `make check` is the gate to run
# before declaring any change done.
.PHONY: help run build test vet fmt fmt-check check tidy \
db-up db-down migrate-up migrate-down migrate-status migrate-new
help: ## List targets
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}'
run: ## Start the API server (SQLite default — run `make migrate-up` once)
go run ./cmd/api
build: ## Compile the binaries into bin/
go build -o bin/api ./cmd/api
go build -o bin/migrate ./cmd/migrate
test: ## Run all tests
go test ./...
vet: ## Run go vet
go vet ./...
fmt: ## Format all source files
gofmt -w .
fmt-check: ## Fail if any file needs gofmt
@files=$$(gofmt -l .); if [ -n "$$files" ]; then \
echo "gofmt needed on:"; echo "$$files"; exit 1; fi
check: fmt-check vet test ## Full gate: fmt-check + vet + test
tidy: ## Tidy go.mod / go.sum
go mod tidy
db-up: ## Optional: start dev Postgres (docker compose) — set DATABASE_URL to use it
docker compose up -d --wait db
db-down: ## Stop the optional dev Postgres and delete its volume
docker compose down -v
migrate-up: ## Apply pending migrations
go run ./cmd/migrate up
migrate-down: ## Roll back the most recent migration group
go run ./cmd/migrate down
migrate-status: ## List migrations and their state
go run ./cmd/migrate status
migrate-new: ## Create a Go migration scaffold: make migrate-new name=create_projects
@test -n "$(name)" || (echo "usage: make migrate-new name=create_projects"; exit 2)
go run ./cmd/migrate new $(name)
-114
View File
@@ -1,114 +0,0 @@
# fullstack-template backend
Go 1.27 API server template: stdlib `net/http` routing (Go 1.22+ method
patterns with `r.PathValue`), [Bun](https://bun.uptrace.dev) ORM (SQLite
file by default, Postgres via `DATABASE_URL`), and a layered layout
(handler → service → repository behind interfaces) designed to be copied
per resource and iterated on with an LLM.
## Quickstart
No containers, no database server — the default database is a local SQLite
file (`dev.db`):
```sh
cd backend
make migrate-up # apply migrations (creates dev.db)
make run # API on :8080
```
Try it:
```sh
curl -s localhost:8080/api/v1/items
curl -s -X POST localhost:8080/api/v1/items \
-H 'Content-Type: application/json' \
-d '{"title":"First item","description":"Hello template"}'
curl -s localhost:8080/api/v1/items/1
curl -s -X PATCH localhost:8080/api/v1/items/1 \
-H 'Content-Type: application/json' -d '{"status":"archived"}'
curl -s -i -X DELETE localhost:8080/api/v1/items/1
```
## Endpoints
| Method | Path | Notes |
|---|---|---|
| GET | `/healthz` | liveness |
| GET | `/readyz` | checks the database |
| GET | `/api/auth/session` | who is signed in; always 200, `{"authenticated":bool,"user":…\|null}` |
| GET | `/api/auth/login` | starts the OIDC redirect; optional `?returnTo=/path` |
| GET | `/api/auth/callback` | issuer lands here; do not call directly |
| POST | `/api/auth/logout` | clears the session cookie; 204 |
| GET | `/api/v1/items` | `?status=active|archived&limit=&offset=` — requires a session when auth is on |
| POST | `/api/v1/items` | 201 + `Location`, body `{"title","description","status"}` |
| GET | `/api/v1/items/{id}` | |
| PATCH | `/api/v1/items/{id}` | partial update; `null`-absent fields untouched |
| DELETE | `/api/v1/items/{id}` | 204 |
Errors are always `{"error":{"code":"...","message":"..."}}`.
## OIDC authentication
Set `OIDC_CLIENT_ID` / `OIDC_CLIENT_SECRET` (see [.env.example](.env.example))
and the API authenticates against the issuer at `OIDC_ISSUER`
(default `https://users.k6n.net`) using the authorization-code + PKCE flow:
- The browser is sent to the issuer from `GET /api/auth/login`; the callback
path is fixed at `/api/auth/callback`. The redirect URI is derived from the
incoming request (`X-Forwarded-Host`/`X-Forwarded-Proto` when behind the
Vite proxy, `Host`/TLS otherwise), so the same server works on
`localhost:<any port>` (loopback exception), `*.preview.k6n.net`
(registered wildcard), and the API origin itself — as long as those URIs
are registered for the client with the issuer.
- The signed-in identity (sub, name, email, roles) is kept in an
HMAC-SHA256-signed session cookie (`HttpOnly`, `SameSite=Lax`). ID and
access tokens never reach the browser and are not persisted server-side;
- When auth is enabled, everything under `/api/v1/` answers
`401 unauthorized` without a valid session. `/healthz`, `/readyz`, and
`/api/auth/*` stay public.
- Without `OIDC_CLIENT_ID` the server runs standalone: no `/api/auth`
routes and an open `/api/v1` (logged loudly at boot).
`AUTH_SESSION_SECRET` signs the cookies — unset it only in development (a
random one is generated per boot, invalidating all sessions on restart).
`AUTH_SESSION_TTL` (default `168h`) bounds the session.
## Layout
```
cmd/
api/ server entrypoint (config, wiring, graceful shutdown)
migrate/ migration CLI: up | down | status | new
internal/
api/ routing, middleware, JSON conventions, item + auth handlers
auth/ OIDC client (code + PKCE flow) and signed session cookies
config/ env parsing — the only place reading os.Getenv
database/ bun connection (sqlite or postgres) + migrator
migrations/ one Go file per migration: <timestamp>_<name>.go
items/ reference resource: model, Repository interface,
Bun implementation, service (business rules)
compose.yaml optional dev Postgres
Makefile `make check` = fmt-check + vet + test
```
`internal/items` is the pattern to copy when adding a resource — see
AGENTS.md for the step-by-step recipe.
## Configuration
All settings come from the environment and a `.env` file in backend/ is
autoloaded. The database defaults to the SQLite file `dev.db`; for
Postgres run `make db-up` and set
`DATABASE_URL=postgres://fullstack:fullstack@localhost:5432/fullstack?sslmode=disable`.
See [.env.example](.env.example) for the full list.
## Tests
```sh
make test # or: go test ./...
```
Service and handler tests run against in-memory fakes; the database test
runs the real migrations and repository against in-memory SQLite — no
database server needed for any of them.
-141
View File
@@ -1,141 +0,0 @@
// Command api runs the HTTP server. Run it from backend/ so the godotenv
// autoload below finds backend/.env.
package main
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
// Loads a .env file next to the working directory, if present.
_ "github.com/joho/godotenv/autoload"
"git.k6n.net/mats/fullstack-template/internal/api"
"git.k6n.net/mats/fullstack-template/internal/auth"
"git.k6n.net/mats/fullstack-template/internal/config"
"git.k6n.net/mats/fullstack-template/internal/database"
"git.k6n.net/mats/fullstack-template/internal/items"
)
func main() {
if err := run(); err != nil {
slog.Error("server exited with error", "err", err)
os.Exit(1)
}
slog.Info("server stopped")
}
func run() error {
cfg, err := config.Load()
if err != nil {
return err
}
logger := newLogger(cfg)
slog.SetDefault(logger)
db, err := database.Open(cfg)
if err != nil {
return err
}
defer db.Close()
pingCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.PingContext(pingCtx); err != nil {
return fmt.Errorf("connect to database: %w (check DATABASE_URL; for postgres, is `make db-up` running?)", err)
}
svc := items.NewService(items.NewBunRepository(db))
// OIDC is on when a client id is configured; without one the API runs
// standalone (no /api/auth routes, /api/v1 unprotected) — see README.
authDeps, err := configureAuth(logger, cfg)
if err != nil {
return err
}
handler := api.New(api.Deps{
Logger: logger,
Items: svc,
DB: db,
AllowedOrigins: cfg.AllowedOrigins,
Auth: authDeps,
})
srv := &http.Server{
Addr: cfg.Addr,
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
errCh := make(chan error, 1)
go func() {
logger.Info("api listening", "addr", cfg.Addr)
errCh <- srv.ListenAndServe()
}()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
select {
case err := <-errCh:
if errors.Is(err, http.ErrServerClosed) {
return nil
}
return err
case <-ctx.Done():
logger.Info("shutting down")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
return fmt.Errorf("graceful shutdown: %w", err)
}
return nil
}
}
// configureAuth builds the OIDC collaborators, or nil in standalone mode.
func configureAuth(logger *slog.Logger, cfg config.Config) (*api.AuthDeps, error) {
if cfg.OIDCClientID == "" {
logger.Warn("OIDC_CLIENT_ID not set — running standalone: /api/auth is disabled and /api/v1 is unprotected")
return nil, nil
}
if cfg.AuthSessionSecretEphemeral {
logger.Warn("AUTH_SESSION_SECRET not set — generated an ephemeral one; every restart signs everyone out")
}
// Discovery failure at boot is fatal: a server that cannot verify
// identities must not come up half-working.
client, err := auth.New(context.Background(), auth.Config{
Issuer: cfg.OIDCIssuer,
ClientID: cfg.OIDCClientID,
ClientSecret: cfg.OIDCClientSecret,
Scopes: []string{"openid", "profile", "email"},
})
if err != nil {
return nil, fmt.Errorf("oidc discovery against %s: %w", cfg.OIDCIssuer, err)
}
logger.Info("oidc auth enabled", "issuer", cfg.OIDCIssuer, "client_id", cfg.OIDCClientID)
return &api.AuthDeps{
Client: client,
Signer: auth.NewSigner(cfg.AuthSessionSecret),
SessionTTL: cfg.AuthSessionTTL,
}, nil
}
func newLogger(cfg config.Config) *slog.Logger {
opts := &slog.HandlerOptions{Level: cfg.LogLevel}
if cfg.LogFormat == "text" {
return slog.New(slog.NewTextHandler(os.Stdout, opts))
}
return slog.New(slog.NewJSONHandler(os.Stdout, opts))
}
-105
View File
@@ -1,105 +0,0 @@
// Command migrate manages database migrations.
//
// Usage:
//
// go run ./cmd/migrate up apply all pending migrations
// go run ./cmd/migrate down roll back the most recent group
// go run ./cmd/migrate status list migrations and whether they ran
// go run ./cmd/migrate new NAME create a Go migration scaffold named
// <timestamp>_NAME.go in
// internal/database/migrations/
package main
import (
"context"
"fmt"
"os"
_ "github.com/joho/godotenv/autoload"
"git.k6n.net/mats/fullstack-template/internal/config"
"git.k6n.net/mats/fullstack-template/internal/database"
)
func main() {
cmd := "up"
if len(os.Args) > 1 {
cmd = os.Args[1]
}
if cmd != "up" && cmd != "down" && cmd != "status" && cmd != "new" {
fmt.Fprintln(os.Stderr, "usage: migrate [up|down|status|new NAME]")
os.Exit(2)
}
cfg, err := config.Load()
if err != nil {
fatal(err)
}
db, err := database.Open(cfg)
if err != nil {
fatal(err)
}
defer db.Close()
ctx := context.Background()
m := database.NewMigrator(db)
switch cmd {
case "up":
if err := m.Init(ctx); err != nil {
fatal(err)
}
group, err := m.Migrate(ctx)
if err != nil {
fatal(err)
}
if group.IsZero() {
fmt.Println("no pending migrations")
} else {
fmt.Printf("applied %d migration(s) (group %d)\n", len(group.Migrations), group.ID)
}
case "down":
if err := m.Init(ctx); err != nil {
fatal(err)
}
group, err := m.Rollback(ctx)
if err != nil {
fatal(err)
}
if group.IsZero() {
fmt.Println("nothing to roll back")
} else {
fmt.Printf("rolled back %d migration(s) (group %d)\n", len(group.Migrations), group.ID)
}
case "status":
if err := m.Init(ctx); err != nil {
fatal(err)
}
migrations, err := m.MigrationsWithStatus(ctx)
if err != nil {
fatal(err)
}
for _, mig := range migrations {
state := "pending"
if mig.IsApplied() {
state = "applied"
}
fmt.Printf("%-8s %s %s\n", state, mig.Name, mig.Comment)
}
case "new":
if len(os.Args) < 3 {
fmt.Fprintln(os.Stderr, "usage: migrate new NAME (snake_case)")
os.Exit(2)
}
file, err := m.CreateGoMigration(ctx, os.Args[2])
if err != nil {
fatal(err)
}
fmt.Println("created", file.Path)
}
}
func fatal(err error) {
fmt.Fprintln(os.Stderr, "migrate:", err)
os.Exit(1)
}
-24
View File
@@ -1,24 +0,0 @@
# Dev-only Postgres matching the default DATABASE_URL in internal/config.
name: fullstack-backend
services:
db:
image: postgres:18-alpine
environment:
POSTGRES_USER: fullstack
POSTGRES_PASSWORD: fullstack
POSTGRES_DB: fullstack
ports:
- "5432:5432"
volumes:
# postgres:18+ wants the mount at /var/lib/postgresql (not .../data)
# so in-place major upgrades via pg_upgrade remain possible.
- pgdata:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U fullstack -d fullstack"]
interval: 2s
timeout: 2s
retries: 15
volumes:
pgdata:
-43
View File
@@ -1,43 +0,0 @@
module git.k6n.net/mats/fullstack-template
go 1.27.1
require (
github.com/coreos/go-oidc/v3 v3.21.0
github.com/joho/godotenv v1.5.1
github.com/uptrace/bun v1.2.18
github.com/uptrace/bun/dialect/pgdialect v1.2.18
github.com/uptrace/bun/dialect/sqlitedialect v1.2.18
github.com/uptrace/bun/driver/pgdriver v1.2.18
github.com/uptrace/bun/driver/sqliteshim v1.2.18
github.com/uptrace/bun/extra/bundebug v1.2.18
golang.org/x/oauth2 v0.37.0
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.34 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
golang.org/x/sys v0.41.0 // indirect
mellium.im/sasl v0.3.2 // indirect
modernc.org/libc v1.68.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.46.1 // indirect
)
-109
View File
@@ -1,109 +0,0 @@
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-oidc/v3 v3.21.0 h1:wZo4Q9Pum8dYEj0eMUPrqR+kvuGkeUplbLpNCkBqoWM=
github.com/coreos/go-oidc/v3 v3.21.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
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/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/uptrace/bun v1.2.18 h1:3HnRcMfS6OBPMG1eSOzlbFJ/X/AyMEJb7rMxE6VQvDU=
github.com/uptrace/bun v1.2.18/go.mod h1:wNltaKJk4JtOt4SG5I5zmA7v0/Mzjh1+/S906Rayd3Y=
github.com/uptrace/bun/dialect/pgdialect v1.2.18 h1:IZ6nM2+OYrL8lkEAy7UkSEZvoa3vluTAUlZfPtlRB2k=
github.com/uptrace/bun/dialect/pgdialect v1.2.18/go.mod h1:Tqdf4QP1okrGYpXfodXvCOK6Ob1OOTwSaoAzCgBB3IU=
github.com/uptrace/bun/dialect/sqlitedialect v1.2.18 h1:Z33SY/U++XK9uGWqS4h8OZVxfCXguIG+sU9cYq2PGFQ=
github.com/uptrace/bun/dialect/sqlitedialect v1.2.18/go.mod h1:1MVOS/Ncy4FZbkJcgUFH6OqYoQinYNjkEwsmNQEXz2A=
github.com/uptrace/bun/driver/pgdriver v1.2.18 h1:Zojuc83ulApocXomBLEcx1DqCZweREafHCjPfyXo88I=
github.com/uptrace/bun/driver/pgdriver v1.2.18/go.mod h1:ZRJcARw93nxbQ5WawTrc5EO+F+GygkcYgDLEnT17CcE=
github.com/uptrace/bun/driver/sqliteshim v1.2.18 h1:fDCXp4L46A23OuUikDbL14SRmm3y+7XO4fkFe1bs2A4=
github.com/uptrace/bun/driver/sqliteshim v1.2.18/go.mod h1:MqvqMCAAKNn6M0HF9YK/Z6xrnCP6sih5OZ37AxdAlHw=
github.com/uptrace/bun/extra/bundebug v1.2.18 h1:5cgkqdvhpSHIEONazSytm4RWYFneNtcznaWLt6r8m4M=
github.com/uptrace/bun/extra/bundebug v1.2.18/go.mod h1:M+U9YJVJcmk0RrszCb2Q1oskJiJ0LuC44FxDhZLP1ws=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0=
mellium.im/sasl v0.3.2/go.mod h1:NKXDi1zkr+BlMHLQjY3ofYuU4KSPFxknb8mfEu6SveY=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.2 h1:4yPaaq9dXYXZ2V8s1UgrC3KIj580l2N4ClrLwnbv2so=
modernc.org/ccgo/v4 v4.30.2/go.mod h1:yZMnhWEdW0qw3EtCndG1+ldRrVGS+bIwyWmAWzS0XEw=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
-221
View File
@@ -1,221 +0,0 @@
package api
import (
"log/slog"
"net/http"
"net/url"
"strings"
"time"
"git.k6n.net/mats/fullstack-template/internal/auth"
)
// AuthDeps bundles the OIDC collaborators; nil Auth in Deps disables the
// /api/auth routes and the /api/v1 guard (standalone mode).
type AuthDeps struct {
Client *auth.Client
Signer *auth.Signer
SessionTTL time.Duration // zero means the 24h default
}
const defaultSessionTTL = 24 * time.Hour
type authHandler struct {
client *auth.Client
signer *auth.Signer
sessionTTL time.Duration
logger *slog.Logger
}
// sessionUser is the public shape of the signed-in identity.
type sessionUser struct {
Sub string `json:"sub"`
Name string `json:"name"`
Email string `json:"email"`
Roles []string `json:"roles"`
}
// login starts the browser flow: it parks a login transaction (state,
// nonce, PKCE verifier, return path) in a short-lived cookie and redirects
// to the issuer's authorization endpoint.
func (h *authHandler) login(w http.ResponseWriter, r *http.Request) {
tx, err := auth.NewLoginTx(sanitizeReturnTo(r.URL.Query().Get("returnTo")))
if err != nil {
h.fail(w, err)
return
}
cookie, err := h.signer.NewLoginTxCookie(tx, requestSecure(r))
if err != nil {
h.fail(w, err)
return
}
http.SetCookie(w, cookie)
http.Redirect(w, r, h.client.AuthCodeURL(tx, redirectURI(r)), http.StatusFound)
}
// callback completes the flow: it checks state, exchanges the code with the
// PKCE verifier, verifies the ID token (signature, iss/aud/exp, nonce), and
// installs the session cookie. Any failure redirects back to the app with
// an auth_error code — the SPA surfaces it; details stay in server logs.
func (h *authHandler) callback(w http.ResponseWriter, r *http.Request) {
secure := requestSecure(r)
http.SetCookie(w, auth.Expired(auth.CookieLoginTx, secure))
fail := func(code string) {
http.Redirect(w, r, "/?auth_error="+url.QueryEscape(code), http.StatusFound)
}
// An issuer-reported error (user denied, interaction required, …) is
// more useful than our own missing-transaction complaint.
if e := r.URL.Query().Get("error"); e != "" {
fail(e)
return
}
tx, ok := h.readLoginTx(r)
if !ok {
fail("expired_login")
return
}
q := r.URL.Query()
if q.Get("state") != tx.State {
fail("state_mismatch")
return
}
user, err := h.client.CompleteCode(r.Context(), q.Get("code"), redirectURI(r), tx)
if err != nil {
h.logger.Error("oidc code exchange failed", "err", err)
fail("code_exchange")
return
}
cookie, err := h.signer.NewSessionCookie(
auth.Session{Sub: user.Sub, Name: user.Name, Email: user.Email, Roles: user.Roles},
h.sessionTTL, secure)
if err != nil {
h.fail(w, err)
return
}
http.SetCookie(w, cookie)
http.Redirect(w, r, tx.ReturnTo, http.StatusFound)
}
// session reports who is signed in; it answers 200 either way so the SPA
// can poll it without treating "anonymous" as an error.
func (h *authHandler) session(w http.ResponseWriter, r *http.Request) {
type sessionResponse struct {
Authenticated bool `json:"authenticated"`
User *sessionUser `json:"user"`
}
res := sessionResponse{}
if sess, ok := CurrentSession(h.signer, r); ok {
res.Authenticated = true
res.User = &sessionUser{Sub: sess.Sub, Name: sess.Name, Email: sess.Email, Roles: sess.Roles}
}
writeJSON(w, http.StatusOK, res)
}
// logout clears the session cookie.
func (h *authHandler) logout(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, auth.Expired(auth.CookieSession, requestSecure(r)))
w.WriteHeader(http.StatusNoContent)
}
// readLoginTx loads and validates the login-transaction cookie.
func (h *authHandler) readLoginTx(r *http.Request) (auth.LoginTx, bool) {
cookie, err := r.Cookie(auth.CookieLoginTx)
if err != nil {
return auth.LoginTx{}, false
}
var tx auth.LoginTx
if h.signer.Open(cookie.Value, &tx) != nil || !tx.Valid() {
return auth.LoginTx{}, false
}
return tx, true
}
// CurrentSession returns the valid session behind the request, if any. It
// is the single read path shared by the session endpoint and requireAuth.
func CurrentSession(signer *auth.Signer, r *http.Request) (auth.Session, bool) {
cookie, err := r.Cookie(auth.CookieSession)
if err != nil {
return auth.Session{}, false
}
var sess auth.Session
if signer.Open(cookie.Value, &sess) != nil || !sess.Valid() {
return auth.Session{}, false
}
return sess, true
}
// requireAuth guards the business API behind a valid session cookie. It is
// installed inside the chain, so unknown /api/v1/* routes answer 401, not
// 404 — route existence is not public information.
func requireAuth(signer *auth.Signer) middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/api/v1/") {
if _, ok := CurrentSession(signer, r); !ok {
writeError(w, http.StatusUnauthorized, "unauthorized",
"sign in via /api/auth/login to call the API")
return
}
}
next.ServeHTTP(w, r)
})
}
}
// redirectURI rebuilds the public origin the browser used, so the issuer
// sends the user back through the same host that started the flow — the
// vite proxy (localhost or *.preview.k6n.net) or the API itself. The proxy
// sets X-Forwarded-Host/Proto; direct requests fall back to r.Host/TLS.
func redirectURI(r *http.Request) string {
return requestScheme(r) + "://" + requestHost(r) + "/api/auth/callback"
}
// requestSecure reports whether the browser reached us over https.
func requestSecure(r *http.Request) bool { return requestScheme(r) == "https" }
// requestScheme resolves the scheme from X-Forwarded-Proto (first hop) or
// the connection itself.
func requestScheme(r *http.Request) string {
if proto := forwardedValue(r.Header.Get("X-Forwarded-Proto")); proto != "" {
return proto
}
if r.TLS != nil {
return "https"
}
return "http"
}
// requestHost resolves the host from X-Forwarded-Host (first hop) or Host.
func requestHost(r *http.Request) string {
if host := forwardedValue(r.Header.Get("X-Forwarded-Host")); host != "" {
return host
}
return r.Host
}
// forwardedValue returns the first value of a forwarded header, which is
// what our own proxy appended.
func forwardedValue(v string) string {
first, _, _ := strings.Cut(v, ",")
return strings.TrimSpace(first)
}
// sanitizeReturnTo keeps post-login redirects inside the app: relative
// paths only, no protocol-relative //host or scheme tricks.
func sanitizeReturnTo(v string) string {
if v == "" || !strings.HasPrefix(v, "/") || strings.HasPrefix(v, "//") ||
strings.ContainsAny(v, "\r\n\\") {
return "/"
}
return v
}
// fail maps unexpected internal errors to the standard envelope.
func (h *authHandler) fail(w http.ResponseWriter, err error) {
h.logger.Error("auth handler failed", "err", err)
writeError(w, http.StatusInternalServerError, "internal_error", "an internal error occurred")
}
-400
View File
@@ -1,400 +0,0 @@
package api_test
import (
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"git.k6n.net/mats/fullstack-template/internal/api"
"git.k6n.net/mats/fullstack-template/internal/auth"
"git.k6n.net/mats/fullstack-template/internal/items"
)
// fakeIDP is a minimal OpenID Connect issuer: discovery, a token endpoint
// that answers any authorization code with a freshly signed RS256 ID token,
// and the matching JWKS. Enough to drive the real client end to end.
type fakeIDP struct {
server *httptest.Server
key *rsa.PrivateKey
clientID string
// nonceForCode records the nonce embedded in the token issued per code.
nonceForCode map[string]string
}
func newFakeIDP(t *testing.T) *fakeIDP {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("generate rsa key: %v", err)
}
idp := &fakeIDP{key: key, clientID: "test-client", nonceForCode: map[string]string{}}
mux := http.NewServeMux()
mux.HandleFunc("GET /.well-known/openid-configuration", func(w http.ResponseWriter, _ *http.Request) {
writeJSONString(w, map[string]any{
"issuer": idp.server.URL,
"authorization_endpoint": idp.server.URL + "/authorize",
"token_endpoint": idp.server.URL + "/token",
"jwks_uri": idp.server.URL + "/keys",
"id_token_signing_alg_values_supported": []string{"RS256"},
})
})
mux.HandleFunc("GET /keys", func(w http.ResponseWriter, _ *http.Request) {
pub := idp.key.PublicKey
writeJSONString(w, map[string]any{
"keys": []map[string]any{{
"kty": "RSA", "kid": "test-key", "alg": "RS256",
"n": base64.RawURLEncoding.EncodeToString(pub.N.Bytes()),
"e": base64.RawURLEncoding.EncodeToString([]byte{1, 0, 1}),
}},
})
})
mux.HandleFunc("POST /token", func(w http.ResponseWriter, r *http.Request) {
code := r.FormValue("code")
claims := map[string]any{
"iss": idp.server.URL,
"sub": "user-1",
"aud": idp.clientID,
"exp": time.Now().Add(time.Hour).Unix(),
"iat": time.Now().Unix(),
"name": "Test User",
"email": "test@example.com",
"roles": []string{"admin"},
}
// The authorize step would have bound the nonce to this code; the
// test injects it via nonceForCode before calling the callback.
if nonce := idp.nonceForCode[code]; nonce != "" {
claims["nonce"] = nonce
}
writeJSONString(w, map[string]any{
"access_token": "at-" + code,
"token_type": "Bearer",
"id_token": idp.sign(t, claims),
})
})
idp.server = httptest.NewServer(mux)
t.Cleanup(idp.server.Close)
return idp
}
// sign produces a compact RS256 JWS for the claims.
func (f *fakeIDP) sign(t *testing.T, claims map[string]any) string {
t.Helper()
header, err := json.Marshal(map[string]string{"alg": "RS256", "typ": "JWT", "kid": "test-key"})
if err != nil {
t.Fatalf("marshal header: %v", err)
}
payload, err := json.Marshal(claims)
if err != nil {
t.Fatalf("marshal claims: %v", err)
}
signingInput := base64.RawURLEncoding.EncodeToString(header) + "." +
base64.RawURLEncoding.EncodeToString(payload)
digest := sha256.Sum256([]byte(signingInput))
sig, err := rsa.SignPKCS1v15(rand.Reader, f.key, crypto.SHA256, digest[:])
if err != nil {
t.Fatalf("sign: %v", err)
}
return signingInput + "." + base64.RawURLEncoding.EncodeToString(sig)
}
func writeJSONString(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(v)
}
// newAuthTestServer builds the full api stack with OIDC pointed at idp.
func newAuthTestServer(t *testing.T, idp *fakeIDP) http.Handler {
t.Helper()
client, err := auth.New(context.Background(), auth.Config{
Issuer: idp.server.URL,
ClientID: idp.clientID,
ClientSecret: "test-secret",
})
if err != nil {
t.Fatalf("auth.New: %v", err)
}
return api.New(api.Deps{
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
Items: items.NewService(newFakeRepo()),
DB: stubPinger{},
AllowedOrigins: []string{"http://localhost:5173"},
Auth: &api.AuthDeps{
Client: client,
Signer: auth.NewSigner("test-signing-secret"),
SessionTTL: time.Hour,
},
})
}
// startLogin drives GET /api/auth/login and returns (tx cookie, parsed
// authorize URL).
func startLogin(t *testing.T, h http.Handler, target string) (*http.Cookie, *url.URL) {
t.Helper()
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, target, nil)
req.Host = "localhost:5173"
h.ServeHTTP(rec, req)
if rec.Code != http.StatusFound {
t.Fatalf("login = %d: %s", rec.Code, rec.Body.String())
}
var txCookie *http.Cookie
for _, c := range rec.Result().Cookies() {
if c.Name == auth.CookieLoginTx {
txCookie = c
}
}
if txCookie == nil {
t.Fatalf("login set no %s cookie", auth.CookieLoginTx)
}
authURL, err := url.Parse(rec.Header().Get("Location"))
if err != nil {
t.Fatalf("parse authorize url: %v", err)
}
return txCookie, authURL
}
func TestOIDCLoginFlow(t *testing.T) {
idp := newFakeIDP(t)
h := newAuthTestServer(t, idp)
// Anonymous: session reports unauthenticated, /api/v1 is closed.
rec := do(t, h, http.MethodGet, "/api/auth/session", "")
var sess struct {
Authenticated bool `json:"authenticated"`
User any `json:"user"`
}
decodeBody(t, rec, &sess)
if sess.Authenticated || sess.User != nil {
t.Errorf("anonymous session = %+v", sess)
}
if rec = do(t, h, http.MethodGet, "/api/v1/items", ""); rec.Code != http.StatusUnauthorized {
t.Fatalf("items without session = %d, want 401", rec.Code)
}
// Login redirect: PKCE + state + nonce present, redirect URI rebuilt
// from the request host.
txCookie, authURL := startLogin(t, h, "/api/auth/login?returnTo=/overview")
q := authURL.Query()
if q.Get("response_type") != "code" || q.Get("client_id") != idp.clientID {
t.Errorf("authorize query = %v", q)
}
if q.Get("code_challenge") == "" || q.Get("code_challenge_method") != "S256" {
t.Errorf("PKCE missing: %v", q)
}
if q.Get("state") == "" || q.Get("nonce") == "" {
t.Errorf("state/nonce missing: %v", q)
}
if got := q.Get("redirect_uri"); got != "http://localhost:5173/api/auth/callback" {
t.Errorf("redirect_uri = %q", got)
}
// The IdP remembers the nonce for the code it would mint here.
idp.nonceForCode["stub-code"] = q.Get("nonce")
// Callback: state + tx cookie exchange for a session cookie.
req := httptest.NewRequest(http.MethodGet,
"/api/auth/callback?code=stub-code&state="+url.QueryEscape(q.Get("state")), nil)
req.Host = "localhost:5173"
req.AddCookie(txCookie)
rec = httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusFound {
t.Fatalf("callback = %d: %s", rec.Code, rec.Body.String())
}
if loc := rec.Header().Get("Location"); loc != "/overview" {
t.Errorf("callback Location = %q, want /overview", loc)
}
var sessionCookie *http.Cookie
for _, c := range rec.Result().Cookies() {
if c.Name == auth.CookieSession {
sessionCookie = c
}
}
if sessionCookie == nil {
t.Fatal("callback set no session cookie")
}
// Signed in: session knows the user, /api/v1 opens up.
req = httptest.NewRequest(http.MethodGet, "/api/auth/session", nil)
req.AddCookie(sessionCookie)
rec = httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("session = %d", rec.Code)
}
var after struct {
Authenticated bool `json:"authenticated"`
User struct {
Sub string `json:"sub"`
Name string `json:"name"`
Email string `json:"email"`
Roles []string `json:"roles"`
} `json:"user"`
}
decodeBody(t, rec, &after)
if !after.Authenticated || after.User.Sub != "user-1" ||
after.User.Name != "Test User" || after.User.Email != "test@example.com" {
t.Errorf("session after login = %+v", after)
}
if len(after.User.Roles) != 1 || after.User.Roles[0] != "admin" {
t.Errorf("roles = %v", after.User.Roles)
}
req = httptest.NewRequest(http.MethodGet, "/api/v1/items", nil)
req.AddCookie(sessionCookie)
rec = httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("items with session = %d: %s", rec.Code, rec.Body.String())
}
// Logout clears the session; /api/v1 closes again.
req = httptest.NewRequest(http.MethodPost, "/api/auth/logout", nil)
req.AddCookie(sessionCookie)
rec = httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusNoContent {
t.Fatalf("logout = %d", rec.Code)
}
var cleared *http.Cookie
for _, c := range rec.Result().Cookies() {
if c.Name == auth.CookieSession && c.MaxAge < 0 {
cleared = c
}
}
if cleared == nil {
t.Fatal("logout did not expire the session cookie")
}
}
func TestOIDCCallbackRejectsBadState(t *testing.T) {
idp := newFakeIDP(t)
h := newAuthTestServer(t, idp)
txCookie, _ := startLogin(t, h, "/api/auth/login")
req := httptest.NewRequest(http.MethodGet, "/api/auth/callback?code=c&state=wrong", nil)
req.AddCookie(txCookie)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusFound || rec.Header().Get("Location") != "/?auth_error=state_mismatch" {
t.Fatalf("callback with wrong state = %d %q", rec.Code, rec.Header().Get("Location"))
}
// And without any tx cookie at all.
rec = do(t, h, http.MethodGet, "/api/auth/callback?code=c&state=x", "")
if rec.Code != http.StatusFound || rec.Header().Get("Location") != "/?auth_error=expired_login" {
t.Fatalf("callback without tx cookie = %d %q", rec.Code, rec.Header().Get("Location"))
}
}
func TestOIDCCallbackPassesIssuerErrorThrough(t *testing.T) {
idp := newFakeIDP(t)
h := newAuthTestServer(t, idp)
rec := do(t, h, http.MethodGet, "/api/auth/callback?error=access_denied", "")
if rec.Code != http.StatusFound ||
rec.Header().Get("Location") != "/?auth_error=access_denied" {
t.Fatalf("callback issuer error = %d %q", rec.Code, rec.Header().Get("Location"))
}
}
func TestAuthRoutesAbsentInStandaloneMode(t *testing.T) {
h := api.New(api.Deps{
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
Items: items.NewService(newFakeRepo()),
DB: stubPinger{},
AllowedOrigins: []string{"http://localhost:5173"},
})
if rec := do(t, h, http.MethodGet, "/api/auth/session", ""); rec.Code != http.StatusNotFound {
t.Errorf("session without OIDC = %d, want 404", rec.Code)
}
if rec := do(t, h, http.MethodGet, "/api/v1/items", ""); rec.Code != http.StatusOK {
t.Errorf("items without OIDC = %d, want 200 (standalone stays open)", rec.Code)
}
}
func TestRequireAuthCoversUnknownV1Routes(t *testing.T) {
idp := newFakeIDP(t)
h := newAuthTestServer(t, idp)
if rec := do(t, h, http.MethodGet, "/api/v1/nope", ""); rec.Code != http.StatusUnauthorized {
t.Errorf("unknown /api/v1 route = %d, want 401 (not 404)", rec.Code)
}
if !strings.Contains(recBodyFor(t, h, "/api/v1/nope"), "unauthorized") {
t.Error("401 body should carry the unauthorized code")
}
}
func TestForwardedHeadersBuildPreviewRedirectURI(t *testing.T) {
idp := newFakeIDP(t)
h := newAuthTestServer(t, idp)
req := httptest.NewRequest(http.MethodGet, "/api/auth/login", nil)
req.Host = "backend.internal:8080"
req.Header.Set("X-Forwarded-Host", "worker-7.preview.k6n.net")
req.Header.Set("X-Forwarded-Proto", "https")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
authURL, err := url.Parse(rec.Header().Get("Location"))
if err != nil {
t.Fatalf("parse authorize url: %v", err)
}
if got := authURL.Query().Get("redirect_uri"); got != "https://worker-7.preview.k6n.net/api/auth/callback" {
t.Errorf("redirect_uri = %q", got)
}
// Session cookies set behind https must carry Secure.
var txCookie *http.Cookie
for _, c := range rec.Result().Cookies() {
if c.Name == auth.CookieLoginTx {
txCookie = c
}
}
if txCookie == nil || !txCookie.Secure {
t.Fatal("tx cookie behind https must be Secure")
}
}
func TestReturnToSanitization(t *testing.T) {
idp := newFakeIDP(t)
h := newAuthTestServer(t, idp)
// Open-redirect attempts fall back to "/" inside the tx cookie.
for _, raw := range []string{"https://evil.example", "//evil.example", "\r\n/x", "\\evil"} {
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/auth/login?returnTo="+url.QueryEscape(raw), nil)
h.ServeHTTP(rec, req)
var txCookie *http.Cookie
for _, c := range rec.Result().Cookies() {
if c.Name == auth.CookieLoginTx {
txCookie = c
}
}
if txCookie == nil {
t.Fatalf("returnTo=%q: no tx cookie", raw)
}
var tx auth.LoginTx
if err := auth.NewSigner("test-signing-secret").Open(txCookie.Value, &tx); err != nil {
t.Fatalf("open tx cookie: %v", err)
}
if tx.ReturnTo != "/" {
t.Errorf("returnTo=%q sanitized to %q, want /", raw, tx.ReturnTo)
}
}
}
func recBodyFor(t *testing.T, h http.Handler, target string) string {
t.Helper()
return do(t, h, http.MethodGet, target, "").Body.String()
}
-126
View File
@@ -1,126 +0,0 @@
package api
import (
"errors"
"log/slog"
"net/http"
"strconv"
"git.k6n.net/mats/fullstack-template/internal/items"
)
// itemHandler adapts HTTP requests to the items service: parse input, call
// the service, map errors to status codes. No business logic lives here.
type itemHandler struct {
svc *items.Service
logger *slog.Logger
}
func (h *itemHandler) list(w http.ResponseWriter, r *http.Request) {
f := items.Filter{
Status: items.Status(r.URL.Query().Get("status")),
Limit: queryInt(r, "limit", items.DefaultLimit),
Offset: queryInt(r, "offset", 0),
}
result, err := h.svc.List(r.Context(), f)
if err != nil {
h.fail(w, err)
return
}
writeJSON(w, http.StatusOK, result)
}
func (h *itemHandler) create(w http.ResponseWriter, r *http.Request) {
var in items.CreateInput
if !decodeJSON(w, r, &in) {
return
}
item, err := h.svc.Create(r.Context(), in)
if err != nil {
h.fail(w, err)
return
}
w.Header().Set("Location", "/api/v1/items/"+strconv.FormatInt(item.ID, 10))
writeJSON(w, http.StatusCreated, item)
}
func (h *itemHandler) get(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
item, err := h.svc.Get(r.Context(), id)
if err != nil {
h.fail(w, err)
return
}
writeJSON(w, http.StatusOK, item)
}
func (h *itemHandler) update(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
var in items.UpdateInput
if !decodeJSON(w, r, &in) {
return
}
item, err := h.svc.Update(r.Context(), id, in)
if err != nil {
h.fail(w, err)
return
}
writeJSON(w, http.StatusOK, item)
}
func (h *itemHandler) delete(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
if err := h.svc.Delete(r.Context(), id); err != nil {
h.fail(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
// pathID parses the {id} path segment; writes a 400 and returns false on
// input that can't be an id.
func pathID(w http.ResponseWriter, r *http.Request) (int64, bool) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil || id <= 0 {
writeError(w, http.StatusBadRequest, "invalid_id", "id must be a positive integer")
return 0, false
}
return id, true
}
// queryInt reads an int query parameter, falling back when absent or junk.
func queryInt(r *http.Request, key string, fallback int) int {
v := r.URL.Query().Get(key)
if v == "" {
return fallback
}
n, err := strconv.Atoi(v)
if err != nil || n < 0 {
return fallback
}
return n
}
// fail is the single translation point from items errors to the wire:
// ErrNotFound → 404, *ValidationError → 400, anything else → 500 (logged).
func (h *itemHandler) fail(w http.ResponseWriter, err error) {
var vErr *items.ValidationError
switch {
case errors.Is(err, items.ErrNotFound):
writeError(w, http.StatusNotFound, "not_found", "item not found")
case errors.As(err, &vErr):
writeError(w, http.StatusBadRequest, "validation_failed", vErr.Error())
default:
h.logger.Error("request failed", "err", err)
writeError(w, http.StatusInternalServerError, "internal_error", "an internal error occurred")
}
}
-107
View File
@@ -1,107 +0,0 @@
package api
import (
"log/slog"
"net/http"
"runtime/debug"
"time"
)
// middleware wraps an http.Handler. Keep middleware transport-level;
// business concerns belong in services.
type middleware func(http.Handler) http.Handler
// chain composes middlewares so chain(h, a, b) == a(b(h)): the first
// argument runs outermost.
func chain(h http.Handler, mws ...middleware) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
// logging records one line per request after it completes.
func logging(logger *slog.Logger) middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(rec, r)
logger.Info("request",
"method", r.Method,
"path", r.URL.Path,
"status", rec.status,
"duration_ms", time.Since(start).Milliseconds(),
)
})
}
}
// recoverPanics turns panics into 500s instead of dropped connections.
func recoverPanics(logger *slog.Logger) middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if v := recover(); v != nil {
logger.Error("panic recovered", "panic", v, "stack", string(debug.Stack()))
writeError(w, http.StatusInternalServerError, "internal_error", "an internal error occurred")
}
}()
next.ServeHTTP(w, r)
})
}
}
// cors lets the configured origins call the API from a browser (e.g. the
// Vite dev server on :5173). A "*" entry allows every origin.
func cors(allowed []string) middleware {
set := make(map[string]struct{}, len(allowed))
allowAll := false
for _, o := range allowed {
if o == "*" {
allowAll = true
}
set[o] = struct{}{}
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if _, ok := set[origin]; origin != "" && (allowAll || ok) {
h := w.Header()
h.Set("Access-Control-Allow-Origin", origin)
h.Add("Vary", "Origin")
h.Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS")
h.Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
h.Set("Access-Control-Max-Age", "600")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
}
next.ServeHTTP(w, r)
})
}
}
// timeout bounds each request's handler time. The body is the raw timeout
// response — http.TimeoutHandler does not set a content type for it.
func timeout(d time.Duration) middleware {
return func(next http.Handler) http.Handler {
return http.TimeoutHandler(next, d,
`{"error":{"code":"timeout","message":"request timed out"}}`)
}
}
// statusRecorder captures the response status for logging.
type statusRecorder struct {
http.ResponseWriter
status int
}
func (r *statusRecorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}
// Unwrap lets http.ResponseController reach the underlying writer.
func (r *statusRecorder) Unwrap() http.ResponseWriter { return r.ResponseWriter }
-53
View File
@@ -1,53 +0,0 @@
package api
import (
"encoding/json"
"net/http"
)
// Error responses always use this envelope:
//
// {"error": {"code": "...", "message": "..."}}
//
// Known codes: invalid_json, validation_failed, not_found, invalid_id,
// method_not_allowed, internal_error, timeout, unavailable. Add new codes
// here when needed.
type errorResponse struct {
Error errorDetail `json:"error"`
}
type errorDetail struct {
Code string `json:"code"`
Message string `json:"message"`
}
// maxBodySize caps JSON request bodies at 1 MiB.
const maxBodySize = 1 << 20
// writeJSON sets the content type and encodes v.
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
// The status line is already sent, so an encode failure can only be
// logged — and only by wrapping the ResponseWriter. Not worth it here.
_ = json.NewEncoder(w).Encode(v)
}
// writeError emits the error envelope with the given status and code.
func writeError(w http.ResponseWriter, status int, code, message string) {
writeJSON(w, status, errorResponse{Error: errorDetail{Code: code, Message: message}})
}
// decodeJSON strictly decodes the request body into dst: unknown fields and
// bodies over maxBodySize are rejected. It writes the 400 response itself
// and returns false, telling the handler to stop.
func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) bool {
r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err := dec.Decode(dst); err != nil {
writeError(w, http.StatusBadRequest, "invalid_json", "request body is not valid JSON: "+err.Error())
return false
}
return true
}
-151
View File
@@ -1,151 +0,0 @@
// Package api wires HTTP routing, middleware, and JSON conventions on top
// of the stdlib ServeMux (Go 1.22+ method patterns and r.PathValue).
package api
import (
"context"
"log/slog"
"net/http"
"strings"
"time"
"git.k6n.net/mats/fullstack-template/internal/items"
)
// Pinger abstracts the database health check so tests can stub it.
type Pinger interface {
PingContext(ctx context.Context) error
}
// Deps are the server's runtime collaborators, wired in cmd/api.
type Deps struct {
Logger *slog.Logger
Items *items.Service
DB Pinger
AllowedOrigins []string
RequestTimeout time.Duration // zero means the 30s default
Auth *AuthDeps // nil disables OIDC: /api/auth answers 404, /api/v1 stays open
}
const defaultRequestTimeout = 30 * time.Second
// New builds the full handler: middleware chain around the route table.
// Middleware order (outermost first): logging → recover → cors → timeout
// → (requireAuth, only when OIDC is configured).
func New(deps Deps) http.Handler {
if deps.RequestTimeout <= 0 {
deps.RequestTimeout = defaultRequestTimeout
}
var authH *authHandler
if deps.Auth != nil {
if deps.Auth.SessionTTL <= 0 {
deps.Auth.SessionTTL = defaultSessionTTL
}
authH = &authHandler{
client: deps.Auth.Client,
signer: deps.Auth.Signer,
sessionTTL: deps.Auth.SessionTTL,
logger: deps.Logger,
}
}
s := &server{
logger: deps.Logger,
db: deps.DB,
items: &itemHandler{svc: deps.Items, logger: deps.Logger},
auth: authH,
}
mux := http.NewServeMux()
// Liveness is "the process is up"; readiness is "it can serve real
// traffic", here meaning the database answers.
mux.HandleFunc("GET /healthz", s.health)
mux.HandleFunc("GET /readyz", s.readiness)
if s.auth != nil {
mux.HandleFunc("GET /api/auth/session", s.auth.session)
mux.HandleFunc("GET /api/auth/login", s.auth.login)
mux.HandleFunc("GET /api/auth/callback", s.auth.callback)
mux.HandleFunc("POST /api/auth/logout", s.auth.logout)
}
// Reference CRUD resource. Method-specific patterns make the mux
// answer 405 with an Allow header for free; read segments with
// r.PathValue("id").
mux.HandleFunc("GET /api/v1/items", s.items.list)
mux.HandleFunc("POST /api/v1/items", s.items.create)
mux.HandleFunc("GET /api/v1/items/{id}", s.items.get)
mux.HandleFunc("PATCH /api/v1/items/{id}", s.items.update)
mux.HandleFunc("DELETE /api/v1/items/{id}", s.items.delete)
// The wrapper keeps the mux's automatic 405 + Allow responses for
// wrong-method requests and renders all unmatched routes as JSON 404s
// (the mux default is plain text).
mws := []middleware{
logging(deps.Logger),
recoverPanics(deps.Logger),
cors(deps.AllowedOrigins),
timeout(deps.RequestTimeout),
}
if s.auth != nil {
mws = append(mws, requireAuth(deps.Auth.Signer))
}
return chain(jsonNotFounds(mux), mws...)
}
type server struct {
logger *slog.Logger
db Pinger
items *itemHandler
auth *authHandler
}
// jsonNotFounds delegates to mux and turns its default plain-text responses
// for unmatched requests into the JSON error envelope. A registered "/" or
// catch-all route would swallow the mux's automatic 405s, so this checks
// mux.Handler instead: an empty pattern means nothing matched, and probing
// the registered methods distinguishes "wrong method" (405 + Allow) from
// "unknown route" (404). The probe runs only on the miss path.
func jsonNotFounds(mux *http.ServeMux) http.Handler {
methods := []string{
http.MethodGet, http.MethodPost, http.MethodPut,
http.MethodPatch, http.MethodDelete,
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, pattern := mux.Handler(r); pattern == "" {
var allowed []string
for _, m := range methods {
probe := r.Clone(r.Context())
probe.Method = m
if _, pattern := mux.Handler(probe); pattern != "" {
allowed = append(allowed, m)
}
}
if len(allowed) > 0 {
w.Header().Set("Allow", strings.Join(allowed, ", "))
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed",
r.Method+" is not allowed for this route")
return
}
writeError(w, http.StatusNotFound, "not_found", "route not found")
return
}
mux.ServeHTTP(w, r)
})
}
func (s *server) health(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (s *server) readiness(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
if err := s.db.PingContext(ctx); err != nil {
writeError(w, http.StatusServiceUnavailable, "unavailable", "database is not reachable")
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "ready"})
}
-295
View File
@@ -1,295 +0,0 @@
package api_test
import (
"context"
"encoding/json"
"errors"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"sort"
"strings"
"sync"
"testing"
"time"
"git.k6n.net/mats/fullstack-template/internal/api"
"git.k6n.net/mats/fullstack-template/internal/items"
)
// These tests run the full HTTP stack (mux patterns, middleware, JSON
// conventions) against the real items service over an in-memory repository.
// fakeRepo is the same in-memory fake as internal/items/service_test.go,
// duplicated on purpose: test fakes stay package-local.
type fakeRepo struct {
mu sync.Mutex
nextID int64
byID map[int64]items.Item
}
func newFakeRepo() *fakeRepo {
return &fakeRepo{nextID: 1, byID: map[int64]items.Item{}}
}
func (f *fakeRepo) List(_ context.Context, fl items.Filter) ([]items.Item, error) {
f.mu.Lock()
defer f.mu.Unlock()
ids := make([]int64, 0, len(f.byID))
for id := range f.byID {
ids = append(ids, id)
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
out := []items.Item{}
for _, id := range ids {
item := f.byID[id]
if fl.Status != "" && item.Status != fl.Status {
continue
}
out = append(out, item)
}
if fl.Offset < len(out) {
out = out[fl.Offset:]
} else {
out = []items.Item{}
}
if fl.Limit < len(out) {
out = out[:fl.Limit]
}
return out, nil
}
func (f *fakeRepo) Get(_ context.Context, id int64) (items.Item, error) {
f.mu.Lock()
defer f.mu.Unlock()
item, ok := f.byID[id]
if !ok {
return items.Item{}, items.ErrNotFound
}
return item, nil
}
func (f *fakeRepo) Create(_ context.Context, item *items.Item) error {
f.mu.Lock()
defer f.mu.Unlock()
item.ID = f.nextID
f.nextID++
now := time.Now()
item.CreatedAt, item.UpdatedAt = now, now
f.byID[item.ID] = *item
return nil
}
func (f *fakeRepo) Update(_ context.Context, item *items.Item) error {
f.mu.Lock()
defer f.mu.Unlock()
if _, ok := f.byID[item.ID]; !ok {
return items.ErrNotFound
}
item.UpdatedAt = time.Now()
f.byID[item.ID] = *item
return nil
}
func (f *fakeRepo) Delete(_ context.Context, id int64) error {
f.mu.Lock()
defer f.mu.Unlock()
if _, ok := f.byID[id]; !ok {
return items.ErrNotFound
}
delete(f.byID, id)
return nil
}
// stubPinger fakes the database for /readyz.
type stubPinger struct{ err error }
func (p stubPinger) PingContext(context.Context) error { return p.err }
func newTestServer(t *testing.T) http.Handler {
t.Helper()
return api.New(api.Deps{
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
Items: items.NewService(newFakeRepo()),
DB: stubPinger{},
AllowedOrigins: []string{"http://localhost:5173"},
})
}
func do(t *testing.T, h http.Handler, method, target, body string) *httptest.ResponseRecorder {
t.Helper()
var rdr io.Reader
if body != "" {
rdr = strings.NewReader(body)
}
req := httptest.NewRequest(method, target, rdr)
if body != "" {
req.Header.Set("Content-Type", "application/json")
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec
}
func decodeBody(t *testing.T, rec *httptest.ResponseRecorder, dst any) {
t.Helper()
if err := json.NewDecoder(rec.Body).Decode(dst); err != nil {
t.Fatalf("decode response body %q: %v", rec.Body.String(), err)
}
}
func TestHealthAndReadiness(t *testing.T) {
h := newTestServer(t)
if rec := do(t, h, http.MethodGet, "/healthz", ""); rec.Code != http.StatusOK {
t.Errorf("healthz = %d, want 200", rec.Code)
}
if rec := do(t, h, http.MethodGet, "/readyz", ""); rec.Code != http.StatusOK {
t.Errorf("readyz = %d, want 200", rec.Code)
}
sick := api.New(api.Deps{
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
Items: items.NewService(newFakeRepo()),
DB: stubPinger{err: errors.New("down")},
})
if rec := do(t, sick, http.MethodGet, "/readyz", ""); rec.Code != http.StatusServiceUnavailable {
t.Errorf("readyz with dead db = %d, want 503", rec.Code)
}
}
func TestItemCRUDFlow(t *testing.T) {
h := newTestServer(t)
// Create.
rec := do(t, h, http.MethodPost, "/api/v1/items", `{"title":"First","description":"hello"}`)
if rec.Code != http.StatusCreated {
t.Fatalf("create = %d: %s", rec.Code, rec.Body.String())
}
var created items.Item
decodeBody(t, rec, &created)
if created.ID != 1 || created.Title != "First" || created.Status != items.StatusActive {
t.Errorf("created = %+v", created)
}
if loc := rec.Header().Get("Location"); loc != "/api/v1/items/1" {
t.Errorf("Location = %q, want /api/v1/items/1", loc)
}
// Get by id (PathValue routing).
rec = do(t, h, http.MethodGet, "/api/v1/items/1", "")
if rec.Code != http.StatusOK {
t.Fatalf("get = %d: %s", rec.Code, rec.Body.String())
}
var got items.Item
decodeBody(t, rec, &got)
if got.Title != "First" {
t.Errorf("get title = %q", got.Title)
}
// Patch one field.
rec = do(t, h, http.MethodPatch, "/api/v1/items/1", `{"status":"archived"}`)
if rec.Code != http.StatusOK {
t.Fatalf("patch = %d: %s", rec.Code, rec.Body.String())
}
var patched items.Item
decodeBody(t, rec, &patched)
if patched.Status != items.StatusArchived || patched.Title != "First" {
t.Errorf("patched = %+v", patched)
}
// List.
rec = do(t, h, http.MethodGet, "/api/v1/items", "")
if rec.Code != http.StatusOK {
t.Fatalf("list = %d: %s", rec.Code, rec.Body.String())
}
var listed []items.Item
decodeBody(t, rec, &listed)
if len(listed) != 1 {
t.Fatalf("list len = %d, want 1", len(listed))
}
// Delete, then confirm it's gone.
if rec := do(t, h, http.MethodDelete, "/api/v1/items/1", ""); rec.Code != http.StatusNoContent {
t.Fatalf("delete = %d, want 204", rec.Code)
}
if rec := do(t, h, http.MethodGet, "/api/v1/items/1", ""); rec.Code != http.StatusNotFound {
t.Errorf("get after delete = %d, want 404", rec.Code)
}
}
func TestErrorResponses(t *testing.T) {
h := newTestServer(t)
do(t, h, http.MethodPost, "/api/v1/items", `{"title":"seed"}`)
tests := []struct {
name string
method string
target string
body string
wantStatus int
wantCode string
}{
{"invalid path id", "GET", "/api/v1/items/abc", "", 400, "invalid_id"},
{"unknown item", "GET", "/api/v1/items/999", "", 404, "not_found"},
{"broken json", "POST", "/api/v1/items", `{"title":`, 400, "invalid_json"},
{"unknown field", "POST", "/api/v1/items", `{"title":"x","wat":1}`, 400, "invalid_json"},
{"validation", "POST", "/api/v1/items", `{"title":""}`, 400, "validation_failed"},
{"unknown route", "GET", "/api/v1/nope", "", 404, "not_found"},
{"empty patch", "PATCH", "/api/v1/items/1", `{}`, 400, "validation_failed"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rec := do(t, h, tt.method, tt.target, tt.body)
if rec.Code != tt.wantStatus {
t.Fatalf("status = %d, want %d (body %s)", rec.Code, tt.wantStatus, rec.Body.String())
}
var res struct {
Error struct {
Code string `json:"code"`
} `json:"error"`
}
decodeBody(t, rec, &res)
if res.Error.Code != tt.wantCode {
t.Errorf("code = %q, want %q", res.Error.Code, tt.wantCode)
}
})
}
}
func TestMethodNotAllowed(t *testing.T) {
h := newTestServer(t)
rec := do(t, h, http.MethodPut, "/api/v1/items/1", `{}`)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("put = %d, want 405 (ServeMux method patterns)", rec.Code)
}
if allow := rec.Header().Get("Allow"); !strings.Contains(allow, "PATCH") {
t.Errorf("Allow = %q, want it to mention PATCH", allow)
}
}
func TestCORSPreflight(t *testing.T) {
h := newTestServer(t)
req := httptest.NewRequest(http.MethodOptions, "/api/v1/items", nil)
req.Header.Set("Origin", "http://localhost:5173")
req.Header.Set("Access-Control-Request-Method", http.MethodPost)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusNoContent {
t.Fatalf("preflight = %d, want 204", rec.Code)
}
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "http://localhost:5173" {
t.Errorf("Allow-Origin = %q", got)
}
// Unknown origins get no CORS headers and fall through to the mux.
req = httptest.NewRequest(http.MethodOptions, "/api/v1/items", nil)
req.Header.Set("Origin", "http://evil.example")
req.Header.Set("Access-Control-Request-Method", http.MethodPost)
rec = httptest.NewRecorder()
h.ServeHTTP(rec, req)
if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "" {
t.Errorf("Allow-Origin for unknown origin = %q, want none", got)
}
}
-137
View File
@@ -1,137 +0,0 @@
package auth
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
)
// Cookie names. The login-tx cookie is scoped to /api/auth so it is only
// sent to the endpoints that need it.
const (
CookieSession = "fs_session"
CookieLoginTx = "fs_auth_tx"
)
// Session is the signed-in identity kept in the session cookie. It is a
// self-contained, HMAC-protected claim set — not a bearer token: it carries
// no issuer credentials and only opens this API.
type Session struct {
Sub string
Name string
Email string
Roles []string
ExpiresAt time.Time
}
// Valid reports whether the session is still within its TTL.
func (s Session) Valid() bool { return time.Now().Before(s.ExpiresAt) }
// ErrTampered is returned when a cookie value fails its HMAC check or is
// malformed — treat it exactly like a missing cookie.
var ErrTampered = errors.New("auth cookie failed integrity check")
// Signer seals values into "<base64url(json)>.<base64url(hmac-sha256)>"
// cookie payloads and opens them back. Construction is cheap; keep one
// per server and share it.
type Signer struct{ key []byte }
// NewSigner returns a Signer keyed by secret.
func NewSigner(secret string) *Signer { return &Signer{key: []byte(secret)} }
// Seal encodes v into a signed cookie value.
func (s *Signer) Seal(v any) (string, error) {
payload, err := json.Marshal(v)
if err != nil {
return "", fmt.Errorf("marshal cookie payload: %w", err)
}
return base64.RawURLEncoding.EncodeToString(payload) + "." + s.mac(payload), nil
}
// Open verifies a sealed value and decodes it into v.
func (s *Signer) Open(sealed string, v any) error {
value, mac, ok := strings.Cut(sealed, ".")
if !ok {
return ErrTampered
}
payload, err := base64.RawURLEncoding.DecodeString(value)
if err != nil {
return ErrTampered
}
sig, err := base64.RawURLEncoding.DecodeString(mac)
if err != nil {
return ErrTampered
}
if !hmac.Equal(sig, s.macBytes(payload)) {
return ErrTampered
}
if err := json.Unmarshal(payload, v); err != nil {
return ErrTampered
}
return nil
}
func (s *Signer) mac(payload []byte) string {
return base64.RawURLEncoding.EncodeToString(s.macBytes(payload))
}
func (s *Signer) macBytes(payload []byte) []byte {
h := hmac.New(sha256.New, s.key)
h.Write(payload)
return h.Sum(nil)
}
// NewSessionCookie returns the Set-Cookie for a session valid for ttl.
func (s *Signer) NewSessionCookie(sess Session, ttl time.Duration, secure bool) (*http.Cookie, error) {
sess.ExpiresAt = time.Now().Add(ttl)
value, err := s.Seal(sess)
if err != nil {
return nil, err
}
return &http.Cookie{
Name: CookieSession,
Value: value,
Path: "/",
MaxAge: int(ttl.Seconds()),
HttpOnly: true,
Secure: secure,
SameSite: http.SameSiteLaxMode,
}, nil
}
// NewLoginTxCookie returns the short-lived Set-Cookie carrying tx between
// the login redirect and the callback.
func (s *Signer) NewLoginTxCookie(tx LoginTx, secure bool) (*http.Cookie, error) {
value, err := s.Seal(tx)
if err != nil {
return nil, err
}
return &http.Cookie{
Name: CookieLoginTx,
Value: value,
Path: "/api/auth",
MaxAge: int(txTTL.Seconds()),
HttpOnly: true,
Secure: secure,
SameSite: http.SameSiteLaxMode,
}, nil
}
// Expired returns a Set-Cookie that deletes the named cookie.
func Expired(name string, secure bool) *http.Cookie {
return &http.Cookie{
Name: name,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: secure,
SameSite: http.SameSiteLaxMode,
}
}
-140
View File
@@ -1,140 +0,0 @@
package auth
import (
"net/http"
"strings"
"testing"
"time"
)
func TestSignerSealOpen(t *testing.T) {
s := NewSigner("test-secret")
tx, err := NewLoginTx("/some/path")
if err != nil {
t.Fatalf("NewLoginTx: %v", err)
}
sealed, err := s.Seal(tx)
if err != nil {
t.Fatalf("Seal: %v", err)
}
var opened LoginTx
if err := s.Open(sealed, &opened); err != nil {
t.Fatalf("Open: %v", err)
}
// Compare fields: time.Time equality also compares the monotonic clock,
// which the JSON roundtrip drops.
if opened.State != tx.State || opened.Nonce != tx.Nonce ||
opened.Verifier != tx.Verifier || opened.ReturnTo != tx.ReturnTo ||
!opened.ExpiresAt.Equal(tx.ExpiresAt) {
t.Errorf("opened = %+v, want %+v", opened, tx)
}
}
func TestSignerRejectsTampering(t *testing.T) {
s := NewSigner("test-secret")
sess := Session{Sub: "user-1", ExpiresAt: time.Now().Add(time.Hour)}
sealed, err := s.Seal(sess)
if err != nil {
t.Fatalf("Seal: %v", err)
}
tests := []struct {
name string
value func(string) string
reason string
}{
{"flipped payload", func(v string) string {
return flipLastChar(strings.Split(v, ".")[0]) + "." + strings.Split(v, ".")[1]
}, "forged payload"},
{"flipped mac", func(v string) string {
return strings.Split(v, ".")[0] + "." + flipLastChar(strings.Split(v, ".")[1])
}, "forged mac"},
{"wrong key", func(v string) string {
other, _ := NewSigner("other-secret").Seal(sess)
return other
}, "signed with a different key"},
{"garbage", func(string) string { return "not-a-sealed-value" }, "garbage"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var out Session
if err := s.Open(tt.value(sealed), &out); err == nil {
t.Errorf("Open accepted %s", tt.reason)
}
})
}
}
func TestSessionAndTxCookies(t *testing.T) {
s := NewSigner("test-secret")
sessCookie, err := s.NewSessionCookie(Session{Sub: "u1"}, 2*time.Hour, true)
if err != nil {
t.Fatalf("NewSessionCookie: %v", err)
}
if sessCookie.Name != CookieSession || sessCookie.Path != "/" {
t.Errorf("session cookie = %+v", sessCookie)
}
if !sessCookie.HttpOnly || !sessCookie.Secure || sessCookie.SameSite != http.SameSiteLaxMode {
t.Errorf("session cookie attrs = %+v", sessCookie)
}
var sess Session
if err := s.Open(sessCookie.Value, &sess); err != nil {
t.Fatalf("Open session: %v", err)
}
if !sess.Valid() {
t.Error("session with future ExpiresAt should be valid")
}
sess.ExpiresAt = time.Now().Add(-time.Minute)
if sess.Valid() {
t.Error("expired session should be invalid")
}
tx, err := NewLoginTx("/")
if err != nil {
t.Fatalf("NewLoginTx: %v", err)
}
if !tx.Valid() {
t.Error("fresh login tx should be valid")
}
tx.ExpiresAt = time.Now().Add(-time.Second)
if tx.Valid() {
t.Error("expired login tx should be invalid")
}
tx, _ = NewLoginTx("/")
txCookie, err := s.NewLoginTxCookie(tx, false)
if err != nil {
t.Fatalf("NewLoginTxCookie: %v", err)
}
if txCookie.Name != CookieLoginTx || txCookie.Path != "/api/auth" {
t.Errorf("tx cookie = %+v", txCookie)
}
if !txCookie.HttpOnly || txCookie.Secure {
t.Errorf("tx cookie attrs = %+v", txCookie)
}
expired := Expired(CookieSession, true)
if expired.Name != CookieSession || expired.MaxAge != -1 {
t.Errorf("Expired cookie = %+v", expired)
}
}
func TestNewLoginTxUsesDistinctSecrets(t *testing.T) {
a, _ := NewLoginTx("/")
b, _ := NewLoginTx("/")
if a.State == b.State || a.Nonce == b.Nonce || a.Verifier == b.Verifier {
t.Error("state/nonce/verifier must be fresh per login")
}
}
// flipLastChar swaps the last character of a base64url value so any
// signature or payload check must fail.
func flipLastChar(v string) string {
last := "A"
if strings.HasSuffix(v, last) {
last = "B"
}
return v[:len(v)-1] + last
}
-157
View File
@@ -1,157 +0,0 @@
// Package auth implements the browser login flow against an OpenID Connect
// issuer: authorization code + PKCE (S256), discovery-driven endpoints, and
// RS256 ID token verification via JWKS. The signed-in identity is kept in an
// HMAC-protected session cookie (cookie.go) — tokens never reach the browser.
package auth
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
// txTTL bounds the login transaction: the gap between redirecting to the
// issuer and the callback that completes it.
const txTTL = 10 * time.Minute
// Config identifies this app to the issuer. Scopes defaults to
// ["openid","profile","email"] when empty.
type Config struct {
Issuer string
ClientID string
ClientSecret string
Scopes []string
}
// Client is the configured OIDC relying party. Discovery happens once in
// New; the client is immutable and safe for concurrent use.
type Client struct {
oauth oauth2.Config
verifier *oidc.IDTokenVerifier
}
// New discovers the issuer's endpoints and returns a ready client.
func New(ctx context.Context, cfg Config) (*Client, error) {
provider, err := oidc.NewProvider(ctx, cfg.Issuer)
if err != nil {
return nil, fmt.Errorf("discover issuer %s: %w", cfg.Issuer, err)
}
if len(cfg.Scopes) == 0 {
cfg.Scopes = []string{"openid", "profile", "email"}
}
return &Client{
oauth: oauth2.Config{
ClientID: cfg.ClientID,
ClientSecret: cfg.ClientSecret,
Endpoint: provider.Endpoint(),
Scopes: cfg.Scopes,
},
verifier: provider.Verifier(&oidc.Config{ClientID: cfg.ClientID}),
}, nil
}
// User is the verified identity established by a completed login.
type User struct {
Sub string
Name string
Email string
Roles []string
}
// LoginTx is the per-login state kept in a short-lived cookie between the
// redirect to the issuer and the callback: state and nonce guard against
// CSRF and replay, the verifier carries PKCE, ReturnTo is where the browser
// lands afterwards.
type LoginTx struct {
State string
Nonce string
Verifier string
ReturnTo string
ExpiresAt time.Time
}
// NewLoginTx creates a login transaction with fresh state, nonce, and PKCE
// verifier for the given post-login path.
func NewLoginTx(returnTo string) (LoginTx, error) {
state, err := randomToken(16)
if err != nil {
return LoginTx{}, err
}
nonce, err := randomToken(16)
if err != nil {
return LoginTx{}, err
}
verifier, err := randomToken(48)
if err != nil {
return LoginTx{}, err
}
return LoginTx{
State: state,
Nonce: nonce,
Verifier: verifier,
ReturnTo: returnTo,
ExpiresAt: time.Now().Add(txTTL),
}, nil
}
// Valid reports whether the transaction is still within its TTL.
func (tx LoginTx) Valid() bool { return time.Now().Before(tx.ExpiresAt) }
// AuthCodeURL builds the issuer's authorization URL for tx — PKCE challenge
// and nonce included. redirectURI is the callback URL on the origin the
// browser came from (it must be registered with the issuer).
func (c *Client) AuthCodeURL(tx LoginTx, redirectURI string) string {
return c.oauth.AuthCodeURL(tx.State,
oauth2.SetAuthURLParam("redirect_uri", redirectURI),
oauth2.SetAuthURLParam("nonce", tx.Nonce),
oauth2.S256ChallengeOption(tx.Verifier),
)
}
// CompleteCode exchanges the authorization code (PKCE verifier from tx) and
// verifies the returned ID token — signature via JWKS, plus iss/aud/exp and
// the nonce — returning the signed-in user.
func (c *Client) CompleteCode(ctx context.Context, code, redirectURI string, tx LoginTx) (User, error) {
tok, err := c.oauth.Exchange(ctx, code,
oauth2.SetAuthURLParam("redirect_uri", redirectURI),
oauth2.VerifierOption(tx.Verifier),
)
if err != nil {
return User{}, fmt.Errorf("exchange authorization code: %w", err)
}
rawIDToken, ok := tok.Extra("id_token").(string)
if !ok || rawIDToken == "" {
return User{}, errors.New("token response carries no id_token")
}
idToken, err := c.verifier.Verify(ctx, rawIDToken)
if err != nil {
return User{}, fmt.Errorf("verify id_token: %w", err)
}
if idToken.Nonce != tx.Nonce {
return User{}, errors.New("id_token nonce does not match the login transaction")
}
var claims struct {
Name string `json:"name"`
Email string `json:"email"`
Roles []string `json:"roles"`
}
if err := idToken.Claims(&claims); err != nil {
return User{}, fmt.Errorf("decode id_token claims: %w", err)
}
return User{Sub: idToken.Subject, Name: claims.Name, Email: claims.Email, Roles: claims.Roles}, nil
}
// randomToken returns a URL-safe random string with n bytes of entropy.
func randomToken(n int) (string, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("read randomness: %w", err)
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
-155
View File
@@ -1,155 +0,0 @@
// Package config loads runtime configuration from the environment.
//
// Every setting has a safe local default — the database is a local SQLite
// file, so the server runs with `make migrate-up && make run` and no .env
// file, no containers. Point DATABASE_URL at Postgres for the real thing.
// This is the only package that reads os.Getenv — add new settings here
// and to .env.example, never at the point of use.
package config
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"log/slog"
"net/url"
"os"
"strings"
"time"
)
// Config is the validated runtime configuration.
type Config struct {
// Addr is the HTTP listen address.
Addr string
// DatabaseURL selects the database: "sqlite:FILE" for a local file
// (dev default, pure-Go driver) or a "postgres://…" DSN.
DatabaseURL string
// AllowedOrigins lists CORS origins permitted to call the API from a
// browser (the Vite dev server on :5173 by default). "*" allows all.
AllowedOrigins []string
// LogLevel and LogFormat configure the slog handler.
LogLevel slog.Level
LogFormat string // "json" or "text"
// DebugQueries enables Bun query logging (bundebug).
DebugQueries bool
// OIDCIssuer is the OpenID Connect issuer; endpoints are discovered,
// never hardcoded. Auth is enabled when OIDCClientID is non-empty.
OIDCIssuer string
// OIDCClientID and OIDCClientSecret identify this app as a confidential
// client of the issuer. An empty OIDCClientID disables auth entirely:
// /api/auth/* answers 404 and /api/v1 stays open (standalone mode).
OIDCClientID string
OIDCClientSecret string
// AuthSessionSecret signs session cookies. When unset, a random
// ephemeral secret is generated (AuthSessionSecretEphemeral = true) —
// every restart then invalidates all sessions.
AuthSessionSecret string
AuthSessionSecretEphemeral bool
// AuthSessionTTL is how long a session cookie stays valid.
AuthSessionTTL time.Duration
}
// Load reads the environment and returns a validated Config.
func Load() (Config, error) {
cfg := Config{
Addr: env("API_ADDR", ":8080"),
DatabaseURL: env("DATABASE_URL", "sqlite:dev.db"),
AllowedOrigins: splitCSV(env("CORS_ALLOWED_ORIGINS", "http://localhost:5173,http://127.0.0.1:5173")),
LogFormat: strings.ToLower(env("LOG_FORMAT", "json")),
DebugQueries: env("DB_DEBUG", "false") == "true",
OIDCIssuer: strings.TrimRight(env("OIDC_ISSUER", "https://users.k6n.net"), "/"),
OIDCClientID: env("OIDC_CLIENT_ID", ""),
OIDCClientSecret: env("OIDC_CLIENT_SECRET", ""),
AuthSessionSecret: env("AUTH_SESSION_SECRET", ""),
}
sessionTTL, ttlErr := time.ParseDuration(env("AUTH_SESSION_TTL", "168h"))
if ttlErr == nil {
cfg.AuthSessionTTL = sessionTTL
}
var errs []error
level := strings.ToLower(env("LOG_LEVEL", "info"))
switch level {
case "debug":
cfg.LogLevel = slog.LevelDebug
case "info":
cfg.LogLevel = slog.LevelInfo
case "warn":
cfg.LogLevel = slog.LevelWarn
case "error":
cfg.LogLevel = slog.LevelError
default:
errs = append(errs, fmt.Errorf("invalid LOG_LEVEL %q (want debug, info, warn, or error)", level))
}
if cfg.Addr == "" {
errs = append(errs, errors.New("API_ADDR must not be empty"))
}
if cfg.DatabaseURL == "" {
errs = append(errs, errors.New("DATABASE_URL must not be empty"))
}
if cfg.LogFormat != "json" && cfg.LogFormat != "text" {
errs = append(errs, fmt.Errorf("invalid LOG_FORMAT %q (want json or text)", cfg.LogFormat))
}
if ttlErr != nil {
errs = append(errs, fmt.Errorf("invalid AUTH_SESSION_TTL: %v", ttlErr))
} else if cfg.AuthSessionTTL <= 0 {
errs = append(errs, errors.New("AUTH_SESSION_TTL must be positive"))
}
if _, err := url.Parse(cfg.OIDCIssuer); err != nil || !strings.HasPrefix(cfg.OIDCIssuer, "https://") && !strings.HasPrefix(cfg.OIDCIssuer, "http://") {
errs = append(errs, fmt.Errorf("invalid OIDC_ISSUER %q (want an http(s) URL)", cfg.OIDCIssuer))
}
if cfg.OIDCClientID != "" && cfg.OIDCClientSecret == "" {
errs = append(errs, errors.New("OIDC_CLIENT_SECRET is required when OIDC_CLIENT_ID is set"))
}
if cfg.AuthSessionSecret == "" {
secret, err := randomSecret()
if err != nil {
return Config{}, fmt.Errorf("generate session secret: %w", err)
}
cfg.AuthSessionSecret = secret
cfg.AuthSessionSecretEphemeral = true
}
if len(errs) > 0 {
return Config{}, fmt.Errorf("invalid config: %w", errors.Join(errs...))
}
return cfg, nil
}
// randomSecret returns a URL-safe random string for signing session
// cookies when AUTH_SESSION_SECRET is not provided.
func randomSecret() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
// env returns the environment variable or fallback when unset/blank.
func env(key, fallback string) string {
if v, ok := os.LookupEnv(key); ok && strings.TrimSpace(v) != "" {
return strings.TrimSpace(v)
}
return fallback
}
// splitCSV splits a comma-separated env value, dropping blank entries.
func splitCSV(v string) []string {
parts := strings.Split(v, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
if p = strings.TrimSpace(p); p != "" {
out = append(out, p)
}
}
return out
}
-68
View File
@@ -1,68 +0,0 @@
// Package database owns the Bun connection and the migration registry.
package database
import (
"database/sql"
"fmt"
"strings"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/dialect/sqlitedialect"
"github.com/uptrace/bun/driver/pgdriver"
"github.com/uptrace/bun/driver/sqliteshim"
"github.com/uptrace/bun/extra/bundebug"
"github.com/uptrace/bun/migrate"
"github.com/uptrace/bun/schema"
"git.k6n.net/mats/fullstack-template/internal/config"
migrations "git.k6n.net/mats/fullstack-template/internal/database/migrations"
)
// Open connects to the database named by cfg.DatabaseURL:
//
// sqlite:FILE?params Bun + modernc sqlite (pure Go, no CGO) — dev default
// postgres://… Bun + pgdriver against Postgres
//
// It does not ping — call PingContext during startup to fail fast.
func Open(cfg config.Config) (*bun.DB, error) {
var sqldb *sql.DB
var dialect schema.Dialect
switch {
case strings.HasPrefix(cfg.DatabaseURL, "sqlite:"):
dsn := strings.TrimPrefix(cfg.DatabaseURL, "sqlite:")
// Sensible dev defaults when the DSN brings no pragmas of its own:
// WAL keeps readers unblocked during writes, busy_timeout waits out
// the remaining write locks instead of erroring, foreign_keys is
// what everyone expects to be on.
if !strings.Contains(dsn, "_pragma") {
sep := "?"
if strings.Contains(dsn, "?") {
sep = "&"
}
dsn += sep + "_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)"
}
var err error
sqldb, err = sql.Open(sqliteshim.ShimName, dsn)
if err != nil {
return nil, fmt.Errorf("open sqlite %q: %w", dsn, err)
}
dialect = sqlitedialect.New()
default:
sqldb = sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(cfg.DatabaseURL)))
dialect = pgdialect.New()
}
db := bun.NewDB(sqldb, dialect)
if cfg.DebugQueries {
db.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true)))
}
return db, nil
}
// NewMigrator returns a bun migrator over the migrations registered in
// internal/database/migrations. See cmd/migrate for the CLI around it.
func NewMigrator(db *bun.DB) *migrate.Migrator {
return migrate.NewMigrator(db, migrations.Migrations)
}
@@ -1,84 +0,0 @@
package database_test
import (
"context"
"errors"
"testing"
"git.k6n.net/mats/fullstack-template/internal/config"
"git.k6n.net/mats/fullstack-template/internal/database"
"git.k6n.net/mats/fullstack-template/internal/items"
)
// The whole persistence stack against an in-memory SQLite database:
// migrations produce a working schema and the real BunRepository
// round-trips items. This is also the cheapest way to catch dialect drift
// between the model and the migrations — no Postgres container required.
func TestSQLiteSchemaRoundTrip(t *testing.T) {
ctx := context.Background()
db, err := database.Open(config.Config{DatabaseURL: "sqlite:file:mem?mode=memory&cache=shared"})
if err != nil {
t.Fatalf("open: %v", err)
}
t.Cleanup(func() { _ = db.Close() })
// One connection keeps the shared in-memory database stable across
// the pool (and mirrors how single-writer sqlite behaves anyway).
db.SetMaxOpenConns(1)
migrator := database.NewMigrator(db)
if err := migrator.Init(ctx); err != nil {
t.Fatalf("migrator init: %v", err)
}
if _, err := migrator.Migrate(ctx); err != nil {
t.Fatalf("migrate up: %v", err)
}
svc := items.NewService(items.NewBunRepository(db))
created, err := svc.Create(ctx, items.CreateInput{Title: "first", Description: "from sqlite"})
if err != nil {
t.Fatalf("create: %v", err)
}
if created.ID == 0 || created.Status != items.StatusActive {
t.Fatalf("create returned %+v", created)
}
got, err := svc.Get(ctx, created.ID)
if err != nil {
t.Fatalf("get: %v", err)
}
if got.Title != "first" || got.CreatedAt.IsZero() {
t.Fatalf("get returned %+v", got)
}
archived, err := svc.Update(ctx, created.ID, items.UpdateInput{Status: ptr(items.StatusArchived)})
if err != nil {
t.Fatalf("update: %v", err)
}
if archived.Status != items.StatusArchived {
t.Fatalf("update returned %+v", archived)
}
listed, err := svc.List(ctx, items.Filter{Status: items.StatusArchived})
if err != nil {
t.Fatalf("list: %v", err)
}
if len(listed) != 1 {
t.Fatalf("list archived len = %d, want 1", len(listed))
}
if err := svc.Delete(ctx, created.ID); err != nil {
t.Fatalf("delete: %v", err)
}
if _, err := svc.Get(ctx, created.ID); !errors.Is(err, items.ErrNotFound) {
t.Fatalf("get after delete: got %v, want ErrNotFound", err)
}
// The down migration must undo the up migration cleanly.
if _, err := migrator.Rollback(ctx); err != nil {
t.Fatalf("rollback: %v", err)
}
}
func ptr[T any](v T) *T { return &v }
@@ -1,31 +0,0 @@
package migrations
import (
"context"
"github.com/uptrace/bun"
"git.k6n.net/mats/fullstack-template/internal/items"
)
func init() {
Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
_, err := db.NewCreateTable().
Model((*items.Item)(nil)).
IfNotExists().
Exec(ctx)
if err != nil {
return err
}
_, err = db.NewCreateIndex().
Model((*items.Item)(nil)).
IfNotExists().
Index("items_status_idx").
Column("status").
Exec(ctx)
return err
}, func(ctx context.Context, db *bun.DB) error {
_, err := db.NewDropTable().Model((*items.Item)(nil)).IfExists().Exec(ctx)
return err
})
}
@@ -1,18 +0,0 @@
// Package migrations holds the schema migrations: one Go file per
// migration, named <YYYYMMDDHHMMSS>_<snake_name>.go, each registering
// itself into Migrations via init(). `make migrate-new` scaffolds the next
// one with a fresh timestamp.
//
// Prefer model-driven DDL (db.NewCreateTable().Model((*pkg.Thing)(nil))) —
// bun emits dialect-correct SQL for both sqlite and postgres, so migrations
// stay portable. Reach for db.Exec with dialect-specific SQL only when a
// model can't express it.
package migrations
import "github.com/uptrace/bun/migrate"
// Migrations is the registry consumed by internal/database.NewMigrator.
// The directory option tells `migrate new` where to write (from backend/).
var Migrations = migrate.NewMigrations(
migrate.WithMigrationsDirectory("internal/database/migrations"),
)
-89
View File
@@ -1,89 +0,0 @@
// Package items is the reference vertical slice: model, repository
// interface, Bun implementation, and service. To add a new resource, copy
// this package's shape and register its routes in internal/api.
package items
import (
"context"
"errors"
"time"
"github.com/uptrace/bun"
)
// Status is the lifecycle state of an item, stored as text.
type Status string
const (
StatusActive Status = "active"
StatusArchived Status = "archived"
)
// Valid reports whether s is a known status value.
func (s Status) Valid() bool {
return s == StatusActive || s == StatusArchived
}
// Item is the persisted model. Struct tags are Bun's mapping; json tags
// define the wire format (camelCase, matching the frontend).
type Item struct {
bun.BaseModel `bun:"table:items,alias:i"`
ID int64 `bun:"id,pk,autoincrement" json:"id"`
Title string `bun:"title,notnull" json:"title"`
Description string `bun:"description,nullzero" json:"description"`
Status Status `bun:"status,notnull,default:'active'" json:"status"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"createdAt"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp" json:"updatedAt"`
}
// BeforeAppendModel stamps timestamps on INSERT and UPDATE queries.
// Postgres stores microsecond UTC; truncate + UTC so values set in Go
// (PATCH responses) match what round-trips through the database.
func (i *Item) BeforeAppendModel(_ context.Context, query bun.Query) error {
now := time.Now().UTC().Truncate(time.Microsecond)
switch query.(type) {
case *bun.InsertQuery:
i.CreatedAt, i.UpdatedAt = now, now
case *bun.UpdateQuery:
i.UpdatedAt = now
}
return nil
}
// Filter narrows and pages List queries. Apply normalizeFilter (via the
// service) before handing one to a repository.
type Filter struct {
Status Status
Limit int
Offset int
}
// List result paging defaults, applied by normalizeFilter.
const (
DefaultLimit = 50
MaxLimit = 100
)
// Repository is the persistence contract for items. The service depends on
// this interface — not on Bun — so tests use in-memory fakes and other
// backends remain possible.
type Repository interface {
List(ctx context.Context, f Filter) ([]Item, error)
Get(ctx context.Context, id int64) (Item, error)
Create(ctx context.Context, item *Item) error
Update(ctx context.Context, item *Item) error
Delete(ctx context.Context, id int64) error
}
// ErrNotFound is returned by repositories and services when an item does
// not exist. Handlers map it to 404.
var ErrNotFound = errors.New("item not found")
// ValidationError reports an invalid field value. Handlers map it to 400.
type ValidationError struct {
Field string
Reason string
}
func (e *ValidationError) Error() string { return e.Field + ": " + e.Reason }
-81
View File
@@ -1,81 +0,0 @@
package items
import (
"context"
"database/sql"
"errors"
"github.com/uptrace/bun"
)
// BunRepository is the Postgres implementation of Repository. It is the
// only file in the package that speaks Bun/SQL.
type BunRepository struct {
db *bun.DB
}
// NewBunRepository wraps a *bun.DB opened by internal/database.
func NewBunRepository(db *bun.DB) *BunRepository {
return &BunRepository{db: db}
}
// List returns items newest-first, honoring the (already normalized) filter.
// An empty result set encodes as [] on the wire, not null.
func (r *BunRepository) List(ctx context.Context, f Filter) ([]Item, error) {
var out []Item
q := r.db.NewSelect().Model(&out)
if f.Status != "" {
q = q.Where("i.status = ?", f.Status)
}
if err := q.
Order("i.created_at DESC", "i.id DESC").
Limit(f.Limit).
Offset(f.Offset).
Scan(ctx); err != nil {
return nil, err
}
if out == nil {
out = []Item{}
}
return out, nil
}
// Get fetches a single item by id, mapping missing rows to ErrNotFound.
func (r *BunRepository) Get(ctx context.Context, id int64) (Item, error) {
var item Item
err := r.db.NewSelect().Model(&item).Where("i.id = ?", id).Scan(ctx)
if errors.Is(err, sql.ErrNoRows) {
return Item{}, ErrNotFound
}
return item, err
}
// Create inserts item and reads back the generated columns.
func (r *BunRepository) Create(ctx context.Context, item *Item) error {
return r.db.NewInsert().Model(item).Returning("*").Scan(ctx, item)
}
// Update persists item (matched by primary key); ErrNotFound when the row
// is gone.
func (r *BunRepository) Update(ctx context.Context, item *Item) error {
res, err := r.db.NewUpdate().Model(item).WherePK().Exec(ctx)
if err != nil {
return err
}
if n, _ := res.RowsAffected(); n == 0 {
return ErrNotFound
}
return nil
}
// Delete removes an item by id; ErrNotFound when the row is gone.
func (r *BunRepository) Delete(ctx context.Context, id int64) error {
res, err := r.db.NewDelete().Model((*Item)(nil)).Where("i.id = ?", id).Exec(ctx)
if err != nil {
return err
}
if n, _ := res.RowsAffected(); n == 0 {
return ErrNotFound
}
return nil
}
-147
View File
@@ -1,147 +0,0 @@
package items
import (
"context"
"strings"
)
// Field length limits enforced on input.
const (
maxTitleLen = 200
maxDescriptionLen = 2000
)
// Service holds the business rules for items. It depends only on the
// Repository interface; main wires the Bun implementation in.
type Service struct {
repo Repository
}
// NewService returns a Service over the given repository.
func NewService(repo Repository) *Service {
return &Service{repo: repo}
}
// CreateInput is the request body for creating an item.
type CreateInput struct {
Title string `json:"title"`
Description string `json:"description"`
Status Status `json:"status"`
}
// UpdateInput patches only provided fields (nil = leave untouched), which
// is why every field is a pointer.
type UpdateInput struct {
Title *string `json:"title"`
Description *string `json:"description"`
Status *Status `json:"status"`
}
// List validates the filter and delegates to the repository.
func (s *Service) List(ctx context.Context, f Filter) ([]Item, error) {
f, err := normalizeFilter(f)
if err != nil {
return nil, err
}
return s.repo.List(ctx, f)
}
// Get returns one item.
func (s *Service) Get(ctx context.Context, id int64) (Item, error) {
return s.repo.Get(ctx, id)
}
// Create validates input and stores a new item.
func (s *Service) Create(ctx context.Context, in CreateInput) (Item, error) {
item := Item{
Title: strings.TrimSpace(in.Title),
Description: strings.TrimSpace(in.Description),
}
if item.Title == "" {
return Item{}, &ValidationError{Field: "title", Reason: "required"}
}
if len(item.Title) > maxTitleLen {
return Item{}, &ValidationError{Field: "title", Reason: "must be at most 200 characters"}
}
if len(item.Description) > maxDescriptionLen {
return Item{}, &ValidationError{Field: "description", Reason: "must be at most 2000 characters"}
}
switch in.Status {
case "":
item.Status = StatusActive
case StatusActive, StatusArchived:
item.Status = in.Status
default:
return Item{}, &ValidationError{Field: "status", Reason: "must be active or archived"}
}
if err := s.repo.Create(ctx, &item); err != nil {
return Item{}, err
}
return item, nil
}
// Update applies a partial patch to an existing item.
func (s *Service) Update(ctx context.Context, id int64, in UpdateInput) (Item, error) {
if in.Title == nil && in.Description == nil && in.Status == nil {
return Item{}, &ValidationError{Field: "body", Reason: "at least one field must be set"}
}
item, err := s.repo.Get(ctx, id)
if err != nil {
return Item{}, err
}
if in.Title != nil {
item.Title = strings.TrimSpace(*in.Title)
if item.Title == "" {
return Item{}, &ValidationError{Field: "title", Reason: "required"}
}
if len(item.Title) > maxTitleLen {
return Item{}, &ValidationError{Field: "title", Reason: "must be at most 200 characters"}
}
}
if in.Description != nil {
item.Description = strings.TrimSpace(*in.Description)
if len(item.Description) > maxDescriptionLen {
return Item{}, &ValidationError{Field: "description", Reason: "must be at most 2000 characters"}
}
}
if in.Status != nil {
if !in.Status.Valid() {
return Item{}, &ValidationError{Field: "status", Reason: "must be active or archived"}
}
item.Status = *in.Status
}
if err := s.repo.Update(ctx, &item); err != nil {
return Item{}, err
}
return item, nil
}
// Delete removes an item.
func (s *Service) Delete(ctx context.Context, id int64) error {
return s.repo.Delete(ctx, id)
}
// normalizeFilter applies paging defaults and validates the status filter.
func normalizeFilter(f Filter) (Filter, error) {
switch f.Status {
case "":
case StatusActive, StatusArchived:
default:
return Filter{}, &ValidationError{Field: "status", Reason: "must be active or archived"}
}
if f.Limit <= 0 {
f.Limit = DefaultLimit
}
if f.Limit > MaxLimit {
f.Limit = MaxLimit
}
if f.Offset < 0 {
f.Offset = 0
}
return f, nil
}
-231
View File
@@ -1,231 +0,0 @@
package items_test
import (
"context"
"errors"
"sort"
"strings"
"sync"
"testing"
"time"
"git.k6n.net/mats/fullstack-template/internal/items"
)
// fakeRepo is an in-memory items.Repository — the point of the Repository
// interface is that these tests never touch a database.
type fakeRepo struct {
mu sync.Mutex
nextID int64
byID map[int64]items.Item
}
func newFakeRepo() *fakeRepo {
return &fakeRepo{nextID: 1, byID: map[int64]items.Item{}}
}
func (f *fakeRepo) List(_ context.Context, fl items.Filter) ([]items.Item, error) {
f.mu.Lock()
defer f.mu.Unlock()
ids := make([]int64, 0, len(f.byID))
for id := range f.byID {
ids = append(ids, id)
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
out := []items.Item{}
for _, id := range ids {
item := f.byID[id]
if fl.Status != "" && item.Status != fl.Status {
continue
}
out = append(out, item)
}
if fl.Offset < len(out) {
out = out[fl.Offset:]
} else {
out = []items.Item{}
}
if fl.Limit < len(out) {
out = out[:fl.Limit]
}
return out, nil
}
func (f *fakeRepo) Get(_ context.Context, id int64) (items.Item, error) {
f.mu.Lock()
defer f.mu.Unlock()
item, ok := f.byID[id]
if !ok {
return items.Item{}, items.ErrNotFound
}
return item, nil
}
func (f *fakeRepo) Create(_ context.Context, item *items.Item) error {
f.mu.Lock()
defer f.mu.Unlock()
item.ID = f.nextID
f.nextID++
now := time.Now()
item.CreatedAt, item.UpdatedAt = now, now
f.byID[item.ID] = *item
return nil
}
func (f *fakeRepo) Update(_ context.Context, item *items.Item) error {
f.mu.Lock()
defer f.mu.Unlock()
if _, ok := f.byID[item.ID]; !ok {
return items.ErrNotFound
}
item.UpdatedAt = time.Now()
f.byID[item.ID] = *item
return nil
}
func (f *fakeRepo) Delete(_ context.Context, id int64) error {
f.mu.Lock()
defer f.mu.Unlock()
if _, ok := f.byID[id]; !ok {
return items.ErrNotFound
}
delete(f.byID, id)
return nil
}
func newService() (*items.Service, *fakeRepo) {
repo := newFakeRepo()
return items.NewService(repo), repo
}
func TestCreateValidation(t *testing.T) {
svc, _ := newService()
ctx := context.Background()
tests := []struct {
name string
in items.CreateInput
want string // expected ValidationError.Field
}{
{"empty title", items.CreateInput{Title: " "}, "title"},
{"long title", items.CreateInput{Title: strings.Repeat("x", 201)}, "title"},
{"bad status", items.CreateInput{Title: "ok", Status: "nope"}, "status"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := svc.Create(ctx, tt.in)
var vErr *items.ValidationError
if !errors.As(err, &vErr) {
t.Fatalf("got error %v, want *items.ValidationError", err)
}
if vErr.Field != tt.want {
t.Errorf("field = %q, want %q", vErr.Field, tt.want)
}
})
}
}
func TestCreateDefaults(t *testing.T) {
svc, _ := newService()
item, err := svc.Create(context.Background(), items.CreateInput{Title: " hello "})
if err != nil {
t.Fatalf("Create: %v", err)
}
if item.Title != "hello" {
t.Errorf("title = %q, want trimmed %q", item.Title, "hello")
}
if item.Status != items.StatusActive {
t.Errorf("status = %q, want default %q", item.Status, items.StatusActive)
}
}
func TestUpdatePatchesOnlyProvidedFields(t *testing.T) {
svc, _ := newService()
ctx := context.Background()
created, err := svc.Create(ctx, items.CreateInput{Title: "original", Description: "keep me"})
if err != nil {
t.Fatalf("Create: %v", err)
}
archived := items.StatusArchived
updated, err := svc.Update(ctx, created.ID, items.UpdateInput{Status: &archived})
if err != nil {
t.Fatalf("Update: %v", err)
}
if updated.Status != items.StatusArchived {
t.Errorf("status = %q, want %q", updated.Status, items.StatusArchived)
}
if updated.Title != "original" || updated.Description != "keep me" {
t.Errorf("untouched fields changed: %+v", updated)
}
}
func TestUpdateRequiresField(t *testing.T) {
svc, _ := newService()
ctx := context.Background()
created, _ := svc.Create(ctx, items.CreateInput{Title: "x"})
_, err := svc.Update(ctx, created.ID, items.UpdateInput{})
var vErr *items.ValidationError
if !errors.As(err, &vErr) || vErr.Field != "body" {
t.Fatalf("got %v, want validation error on body", err)
}
}
func TestUpdateNotFound(t *testing.T) {
svc, _ := newService()
title := "x"
_, err := svc.Update(context.Background(), 999, items.UpdateInput{Title: &title})
if !errors.Is(err, items.ErrNotFound) {
t.Fatalf("got %v, want ErrNotFound", err)
}
}
func TestListFilterAndPaging(t *testing.T) {
svc, _ := newService()
ctx := context.Background()
svc.Create(ctx, items.CreateInput{Title: "one"})
svc.Create(ctx, items.CreateInput{Title: "two", Status: items.StatusArchived})
svc.Create(ctx, items.CreateInput{Title: "three", Status: items.StatusArchived})
got, err := svc.List(ctx, items.Filter{Status: items.StatusArchived})
if err != nil {
t.Fatalf("List: %v", err)
}
if len(got) != 2 {
t.Fatalf("len = %d, want 2", len(got))
}
if _, err := svc.List(ctx, items.Filter{Status: "bogus"}); err == nil {
t.Error("List with bogus status: want validation error")
}
got, err = svc.List(ctx, items.Filter{Limit: 1})
if err != nil {
t.Fatalf("List: %v", err)
}
if len(got) != items.DefaultLimit && len(got) != 1 {
t.Fatalf("len = %d, want 1", len(got))
}
}
func TestDeleteFlow(t *testing.T) {
svc, _ := newService()
ctx := context.Background()
item, _ := svc.Create(ctx, items.CreateInput{Title: "gone"})
if err := svc.Delete(ctx, item.ID); err != nil {
t.Fatalf("Delete: %v", err)
}
if _, err := svc.Get(ctx, item.ID); !errors.Is(err, items.ErrNotFound) {
t.Fatalf("Get after delete: got %v, want ErrNotFound", err)
}
if err := svc.Delete(ctx, item.ID); !errors.Is(err, items.ErrNotFound) {
t.Fatalf("second delete: got %v, want ErrNotFound", err)
}
}
-4
View File
@@ -1,4 +0,0 @@
{
"ignorePatterns": ["node_modules", "dist", "coverage"],
"rules": {}
}
-125
View File
@@ -1,125 +0,0 @@
# Northstar Design System — agent handbook
React 19 + Vite 8 + TypeScript (strict) design-system showcase. Dark-first, token-driven UI with a light theme.
## Commands
| Task | Command |
|---|---|
| Dev server | `npm run dev` (vite.config.ts pins `0.0.0.0:5173`) |
| Full gate: lint + token contract + tests + typecheck + build | `npm run check` — run before declaring any change done |
| Lint only | `npm run lint` (oxlint) |
| Tests | `npm run test` (vitest, jsdom; suites in `tests/`) |
TypeScript is **strict** (`strict` + `noUncheckedIndexedAccess` in `tsconfig.app.json`) — implicit `any` fails the build, not just the reviewer.
## Debugging without a console
Browser tooling here has no console access. Runtime errors therefore render into a `#dev-errors` element (see `src/dev/errorSink.ts` + `src/dev/ErrorBoundary.tsx`): after loading the page, `wait_for` / `inspect` `[data-dev-error]` to read error messages, and `wait_for vite-error-overlay` for build/HMR errors. If `#root` is empty but no `[data-dev-error]` exists, check the dev-server stdout.
## Sandboxed worker dev flow
- `vite.config.ts` allows `*.preview.k6n.net`; expose port **5173** via `repo_instance_expose` for a preview URL.
- Removing `allowedHosts` breaks preview ingress with a 403 ("host not allowed"). Don't.
- `/api` is proxied to the Go backend (target `API_URL`, default `http://localhost:8080`) and forwards `X-Forwarded-Host`/`X-Forwarded-Proto`, so OIDC login works unchanged on localhost and preview hosts. The auth module is `src/auth/` (`useSession` hook, Topbar `UserMenu`, overview `SessionCard`) — server side lives in the backend's `internal/auth` + `/api/auth/*` (see backend/AGENTS.md).
## Design tokens — the contract
1. **Single source of truth: `src/tokens.css`.** Every font size, color, spacing, radius, and shadow color comes from a token. The palette is defined in modern CSS Color 4 `lab(...)` color space:
- Dark theme (`:root:not([data-theme='light'])`, `[data-theme='dark']`, `.dark`): Pitch black base (`lab(0% 0 0)`), deep cards (`lab(.903296% 0 0)`), radiant electric mint/cyan primary (`lab(89.9321% -47.9163 -15.7303)`), vivid destructive (`lab(54.5811% 71.684 50.9279)`), chart colors 1–5, and subtle dot grid (`--dot-color`).
- Light theme (`:root`, `[data-theme='light']`, `.light`): Pure white daylight base (`lab(100% 0 0)`), crisp charcoal primary (`lab(7.78201% -.0000149012 0)`), and high-contrast surfaces.
- There are **no** legacy aliases (`--panel*`, `--line*`, `--text-soft`, …) anymore — canonical names only.
2. The guard `scripts/check-tokens.mjs` (in `npm run check`) bans hardcoded colors — hex, named colors, `rgb()/hsl()/oklch()/lab()`, even across multiple lines — plus px font sizes and `var(--x)` references that no CSS file defines. It scans **CSS files and inline `style={{ … }}` in `.ts/.tsx`**. Allowed: `var(--token)`, `color-mix(in srgb, var(--token) N%, transparent)`, `light-dark(var(--a), var(--b))`, and neutral black/white rgba shines.
3. Typography scale (`--text-3xs` … `--text-2xl`, `--text-hero`) is rem-based on a 9→28px grid. Longform body `--text-md`, buttons/nav `--text-xs`, meta/eyebrows `--text-2xs`/`--text-3xs`.
4. **Global density knob: `--root-font-size`** (default `112%` readability preset, baseline 16px = `100%`). One line scales every token-driven size app-wide; spacing tokens follow.
5. **Theming:** components read only `var(--*)` and never know the theme. A pre-paint script in `index.html` applies `localStorage['ns-theme']` or `prefers-color-scheme` before first paint (no flash); `useTheme` in `Topbar.tsx` reads what it set and keeps `data-theme` **and** the `.dark`/`.light` classes in sync on toggle.
## Smart Utils & AI Workflow
- **Global app state**: `src/app/` — `AppStateProvider` (mounted in `main.tsx`) owns the few settings the whole app shares: `theme`, `viewMode`, `sidebarCollapsed`, `searchOpen`, plus the ⌘K/Ctrl+K shortcut. Read them anywhere with `useAppState()` (import from `./app` or `../app`); it throws outside the provider on purpose. Add a setting to `appState.ts` (types) + `AppStateProvider.tsx` (state, DOM mirror, `STORAGE_KEYS` persistence) — never thread a new prop chain. Section-local state (active tab, selected pattern) stays in its section. Persisted settings are written **only on user action**, so an untouched app keeps following `prefers-color-scheme`. `theme` always boots from the `<html data-theme>` attribute the pre-paint script set (storage key `ns-theme` is shared with `index.html`); `viewMode` mirrors to `<html data-view-mode>` and currently gates nothing — it is the audience preset hook for per-persona views.
- **Class helper**: `src/utils/cn.ts` — zero-dependency classnames merging utility for conditional component styles.
- **Timer helper**: `src/utils/useTimeout.ts` — `schedule(fn, ms)` / `clear()`; auto-cancels pending timers on unmount. Never fire-and-forget a raw `setTimeout` in a component.
- **Smart CSS**: `src/utils.css` — ready-to-use helpers (`.dot-canvas`, `.surface-card`, `.surface-sidebar`, `.badge-*`, `.hover-lift`, `.kbd-tag`, `.skeleton-pulse`, `.progress-*`, `.ui-table-*`, `.dialog-*`, `.sheet-*`, `.alert-*`, `.ui-tabs-*`, `.switch-*`).
- **Overlays — one primitive**: `src/components/ui/Overlay.tsx` owns focus trap, Escape-to-close, scroll lock, focus restore, and conditional `aria-labelledby`/`aria-describedby` (wired only when a title/description is actually rendered). `Dialog` and `Sheet` are thin skins over it; `SearchPalette` reuses `trapTabKey` + `useOverlayLifecycle` from `src/components/ui/overlayBehavior.ts`. Build new modals on Overlay, not from scratch.
- **Composable Primitives**: `src/components/ui/` (`Overlay`, `Dialog`, `Sheet`, `Tabs`, `Alert`, `EmptyState`, `Table`, `Progress`, `Switch`, `Badge`, `Card`, `StatCard`, `Skeleton`, `Tooltip`, `Kbd`, `Avatar`, `Spinner`; `Icon` lives at `src/components/Icon.tsx` and is re-exported from the barrel).
- **StatCard**: `trend` is sentiment (badge color), `direction` is the arrow and defaults to the sign of `change` — a "good" latency drop is `trend="up"` with `change="-4.2ms"` rendering a green `↓ -4.2ms`.
- **Reusable Application Patterns**: `src/components/examples/` (`MetricsDashboard`, `SettingsPanel`, `ServiceTable`, `DeploymentModal`).
- **Icon Library**: the registry in `src/components/icon-registry.ts` maps every `IconName` to a lucide component (strictly typed — typos fail `tsc`, unknown names at runtime **throw in dev** so the error sink surfaces them). 42 curated icons power the showcase; `lucide-react` ships 1,400+ for direct import.
- **Routing — wouter (3 kB)**: the route table is `src/app/routes.tsx` and it drives the `<Switch>` in `App.tsx`, the sidebar, the breadcrumbs, the right rail and the ⌘K palette, so navigation cannot drift from the pages that exist. Adding a page = a `src/pages/*.tsx` component plus one registry entry (`path`, `group`, `label`, `index`, `kicker`, `title`, `description`, `meta`, `page`). Navigation is real links (`<Link href>`), never `onClick` + `scrollIntoView`; the shell scrolls to top on every location change. State that belongs in the URL lives in the query (`/patterns?pattern=table`), read with `useSearchParams` and validated by a registry helper (`patternFromSearch`). Deep links work because Vite serves the SPA fallback.
## Backend data — api / hooks / page separation
Live backend pages follow three layers and never skip one:
1. **`src/api/client.ts`** — the only `fetch` in the app. It knows the Go API's error envelope `{"error":{"code","message"}}` and turns every failure into an `ApiError` (`status`, `code`, `message`). Components switch on `code` (`validation_failed`, `not_found`, `network`, …), never on message strings. Override the origin with `VITE_API_URL`.
2. **`src/api/<resource>.ts`** — typed request functions per resource; `backend/internal/<resource>` is the source of truth for the wire format (camelCase JSON, bare arrays for lists).
3. **`src/hooks/use<Resource>.ts`** — SWR data hooks (`useItems`, `useItem`) plus one actions hook: mutations call the api module, then revalidate every matching cache entry through a key-filter `mutate`. All keys come from one factory (`itemsKeys`) so lists and details invalidate consistently. Lists use `keepPreviousData` so filter switches don't flash skeletons.
Pages import hooks only — never `src/api` directly, never `fetch`. The reference wiring is `src/pages/ItemsPage.tsx` at `/items`: skeleton rows on first load, a retry alert when the backend is down (with the exact `cd backend && make migrate-up && make run` command — SQLite is the backend default, no database server), empty state with a create CTA, per-row busy spinner, and inline action errors. Dev wiring: `vite.config.ts` proxies `/api` to the Go server on :8080 (override with `API_URL=… npm run dev`).
Adding a live resource: `src/api/<res>.ts` → `src/hooks/use<Res>.ts` → a page → route registry entry (+ `SearchPalette` SEARCH_INDEX entry) → tests mock `src/api/<res>` and assert the hook wiring (see `tests/useItems.test.tsx`).
## Structure
```
src/
tokens.css ← lab() design tokens + dark & light themes (edit this first)
utils.css ← token-compliant smart CSS utility classes
index.css ← fonts, tokens & utils import, base resets
App.css ← application component styles (token-driven, plain CSS)
App.tsx ← app shell: topbar + sidebar + breadcrumbs + <Switch> routes + right rail
main.tsx ← mounts App inside the ErrorBoundary, installs the dev error sink
app/
AppStateProvider.tsx ← global settings (theme, viewMode, sidebar, ⌘K search) + persistence
appState.ts ← context, types, useAppState()
routes.tsx ← route table: paths, headings, nav groups, URL helpers
persist.ts ← guarded localStorage read/write + storage keys
auth/ ← useSession() + UserMenu + SessionCard — OIDC identity via the /api proxy
api/ backend clients: client.ts (fetch wrapper + ApiError) and
one typed module per resource (items.ts)
hooks/ SWR data hooks + mutation actions per resource
(useItems.ts) — the only consumer of src/api
pages/ ← one component per route (OverviewPage, ButtonsPage, PatternsPage, …)
dev/
ErrorBoundary.tsx, errorSink.ts ← render runtime errors into #dev-errors
utils/
cn.ts ← conditional classnames utility
useTimeout.ts ← auto-cleaning setTimeout hook
components/
ui/ ← composable UI primitives (Overlay, Dialog, Sheet, Tabs, Alert,
EmptyState, Table, Progress, Switch, Badge, Card, StatCard,
Skeleton, Tooltip, Kbd, Avatar, Spinner) + overlayBehavior.ts
icon-registry.ts← the one icon name → lucide component map (single source)
examples/ ← reusable pattern components (MetricsDashboard, SettingsPanel,
ServiceTable, DeploymentModal)
AIWorkflowSection.tsx ← interactive pattern switcher & showcase
SearchPalette.tsx ← ⌘K command palette (Overlay behavior + combobox a11y)
TypographySection.tsx ← type-scale showcase
IconMatrix.tsx ← interactive Lucide icon explorer (renders iconRegistry)
Icon.tsx ← <Icon name="…" /> component (types from icon-registry.ts)
Topbar.tsx ← owns the theme toggle (data-theme + .dark/.light)
Hero.tsx, CodeSample.tsx, RightRail.tsx, ButtonMatrix.tsx,
Sidebar.tsx, Section.tsx, FormField.tsx, TokenGrid.tsx,
MarkdownRenderer.tsx, CodeEditor.tsx, Button.tsx
data/
library.ts ← demo content (button examples, color tokens)
patterns.ts ← pattern catalogue + PatternId (feeds /patterns?pattern=…)
tests/ ← vitest suites (jsdom): overlay, icon-registry, useTimeout,
app-state, routing, check-tokens guard
scripts/check-tokens.mjs ← token contract guard (part of npm run check)
```
## Parallel subagent workflow
Multiple agents can work the same checkout concurrently if tasks are **file-disjoint**:
- Each agent gets an explicit allow-list of files; shared files (barrels like `components/ui/index.ts`, `utils.css`, `App.tsx`) are edited by the orchestrator only, after agents finish.
- Agents verify with `npx oxlint <files>`, `npx tsc --noEmit -p tsconfig.app.json`, and `node scripts/check-tokens.mjs` — never `npm run check`/`vite build` in parallel (incremental build info and `dist/` would race).
- Read-only investigation tasks use the read-only spawn mode; they can run freely alongside writers.
- New primitives: agents create self-contained files (component + own `.css` import if keyframes/classes are needed); the orchestrator registers exports and showcase usage.
## Iteration workflow for visual changes
1. Change the token (or component markup) — never a literal.
2. `npm run check` until green.
3. Verify live: dev server + preview URL; screenshot at desktop width; toggle light/dark via the topbar button.
-78
View File
@@ -1,78 +0,0 @@
# Northstar Design System
A token-driven design system showcase built for AI-assisted development workflows.
React 19 · Vite 8 · TypeScript (strict) · Vitest · oxlint.
## Quick start
```bash
npm install
npm run dev # dev server on 0.0.0.0:5173
```
## Auth and the API proxy
`/api` is proxied to the Go backend (`API_URL`, default
`http://localhost:8080`) so the browser only ever sees the vite origin —
on `localhost` or `*.preview.k6n.net`. That keeps OIDC redirects and the
session cookie first-party: the topbar's Sign in and the overview Session
card go through `/api/auth/login`, the issuer returns to
`/api/auth/callback` on the same host, and `useSession()`
([src/auth](src/auth)) polls `/api/auth/session`. The proxy forwards
`X-Forwarded-Host`/`X-Forwarded-Proto` so the backend registers the right
redirect URI per host. See the backend README for the server side.
## The quality gate
```bash
npm run check
```
One command, four gates, in order:
1. **`oxlint`** — lint over every source file.
2. **`scripts/check-tokens.mjs`** — the token contract: bans hardcoded colors
(hex / named / `rgb()` / `hsl()` / `oklch()` / `lab()`), px font sizes and
undefined `var(--x)` references — in **both** CSS files and inline
`style={{ … }}` objects. Values may span lines; comments are ignored.
3. **`vitest run`** — unit + component tests (`tests/`).
4. **`tsc -b && vite build`** — strict typecheck, then production build.
CI runs the same gate on every push and PR (`.github/workflows/ci.yml`).
## Design tokens
`src/tokens.css` is the single source of truth. Every color, font size,
spacing, radius and shadow goes through a `var(--token)` — components never
hardcode values. Themes are plain CSS: `:root[data-theme='light']` /
`.light` and `:root[data-theme='dark']` / `.dark`, with a pre-paint script in
`index.html` that applies the saved (or OS-preferred) theme before first paint.
The global density knob is `--root-font-size` — one line scales the whole app.
## Layout
```
src/
tokens.css ← lab() design tokens, dark & light themes
index.css ← fonts, token import, base resets
utils.css ← token-compliant utility classes
App.css ← application showcase styles
app/ ← AppStateProvider + useAppState(), and the route table (wouter)
auth/ ← useSession() + UserMenu / SessionCard (OIDC identity via the API proxy)
pages/ ← one page component per route
components/
ui/ ← composable primitives (Overlay, Dialog, Sheet, Tabs, Alert, …)
icon-registry.ts← the one icon name → lucide component map
examples/ ← reusable patterns (MetricsDashboard, SettingsPanel, …)
dev/ ← ErrorBoundary + dev error sink (#dev-errors)
utils/ ← cn(), useTimeout()
tests/ ← vitest suites (jsdom)
scripts/
check-tokens.mjs ← token contract guard
```
## For agents
Read [`AGENTS.md`](./AGENTS.md) before changing anything — it documents the
token contract, the overlay/icon registries, and the parallel-subagent rules.
-1
View File
@@ -1 +0,0 @@
@import "https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;500;600;700;800&display=swap";:root{--root-font-size:112%;--text-3xs:.5625rem;--text-2xs:.625rem;--text-xs:.6875rem;--text-sm:.75rem;--text-md:.875rem;--text-base:1rem;--text-lg:1.3125rem;--text-xl:1.5625rem;--text-2xl:1.75rem;--text-hero:clamp(2.625rem, 5vw, 4.375rem);--font-sans:"Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--font-mono:"DM Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;--space-1:.25rem;--space-2:.5rem;--space-3:.75rem;--space-4:1rem;--space-5:1.25rem;--space-6:1.5rem;--space-7:2rem;--space-8:2.5rem;--space-9:3rem;--space-10:4rem;--space-11:5rem;--radius-sm:5px;--radius-md:6px;--radius-lg:9px;--radius-xl:12px;--radius-2xl:14px;--radius-pill:9999px;--focus-ring:2px solid var(--ring);--focus-offset:2px}:root,:root[data-theme=light],.light{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--background:lab(100% 0 0);--foreground:lab(2.75381% 0 0);--card:lab(97.68% -.0000298023 .0000119209);--card-foreground:lab(2.75381% 0 0);--popover:lab(100% 0 0);--popover-foreground:lab(2.75381% 0 0);--primary:lab(7.78201% -.0000149012 0);--primary-foreground:lab(98.26% 0 0);--secondary:lab(94.2% 0 0);--secondary-foreground:lab(7.78201% -.0000149012 0);--muted:lab(94.2% 0 0);--muted-foreground:lab(39.68% 0 0);--accent:lab(94.2% 0 0);--accent-foreground:lab(7.78201% -.0000149012 0);--destructive:lab(48.4493% 77.4328 61.5452);--destructive-foreground:lab(98.26% 0 0);--border:lab(86.08% -.0000298023 0);--input:lab(86.08% -.0000298023 0);--ring:lab(42% 0 0);--chart-1:lab(57.1026% 64.2584 89.8886);--chart-2:lab(55.0223% -41.0774 -3.90277);--chart-3:lab(30.372% -13.1853 -18.7887);--chart-4:lab(80.1641% 16.6016 99.2089);--chart-5:lab(72.7183% 31.8672 97.9407);--sidebar:lab(96.52% -.0000298023 .0000119209);--sidebar-foreground:lab(2.75381% 0 0);--sidebar-primary:lab(7.78201% -.0000149012 0);--sidebar-primary-foreground:lab(98.26% 0 0);--sidebar-accent:lab(94.2% 0 0);--sidebar-accent-foreground:lab(7.78201% -.0000149012 0);--sidebar-border:lab(86.08% -.0000298023 0);--sidebar-ring:lab(42% 0 0);--dot-color:lab(7.78201% -.0000149012 0/.05)}:root:not([data-theme=light]),:root[data-theme=dark],.dark{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--background:lab(0% 0 0);--foreground:lab(94.78% 0 0);--card:lab(.903296% 0 0);--card-foreground:lab(94.78% 0 0);--popover:lab(.903296% 0 0);--popover-foreground:lab(94.78% 0 0);--primary:lab(89.9321% -47.9163 -15.7303);--primary-foreground:lab(0% 0 0);--secondary:lab(2.47865% 0 0);--secondary-foreground:lab(94.78% 0 0);--muted:lab(2.47865% 0 0);--muted-foreground:lab(60.56% 0 0);--accent:lab(1.98454% 0 0);--accent-foreground:lab(89.9321% -47.9163 -15.7303);--destructive:lab(54.5811% 71.684 50.9279);--destructive-foreground:lab(100% 0 0);--border:lab(4.8411% 0 0);--input:lab(1.98454% 0 0);--ring:lab(89.9321% -47.9163 -15.7303);--chart-1:lab(89.9321% -47.9163 -15.7303);--chart-2:lab(66.9756% -58.27 19.5419);--chart-3:lab(72.7183% 31.8672 97.9407);--chart-4:lab(52.0183% 66.11 -78.2316);--chart-5:lab(56.101% 79.4328 31.4532);--sidebar:lab(.498049% 0 0);--sidebar-foreground:lab(94.78% 0 0);--sidebar-primary:lab(89.9321% -47.9163 -15.7303);--sidebar-primary-foreground:lab(0% 0 0);--sidebar-accent:lab(1.98454% 0 0);--sidebar-accent-foreground:lab(89.9321% -47.9163 -15.7303);--sidebar-border:lab(4.8411% 0 0);--sidebar-ring:lab(89.9321% -47.9163 -15.7303);--dot-color:lab(89.9321% -47.9163 -15.7303/.045)}html{font-size:var(--root-font-size)}.dot-canvas{background-image:radial-gradient(var(--dot-color) 1px, transparent 1px);background-size:24px 24px}.dot-canvas-dense{background-image:radial-gradient(var(--dot-color) 1px, transparent 1px);background-size:16px 16px}.flex-center{justify-content:center;align-items:center;display:flex}.flex-between{justify-content:space-between;align-items:center;display:flex}.flex-col{flex-direction:column;display:flex}.stack-xs{gap:var(--space-1);flex-direction:column;display:flex}.stack-sm{gap:var(--space-2);flex-direction:column;display:flex}.stack-md{gap:var(--space-4);flex-direction:column;display:flex}.stack-lg{gap:var(--space-6);flex-direction:column;display:flex}.row-xs{align-items:center;gap:var(--space-1);display:flex}.row-sm{align-items:center;gap:var(--space-2);display:flex}.row-md{align-items:center;gap:var(--space-4);display:flex}.grid-2{gap:var(--space-4);grid-template-columns:repeat(2,minmax(0,1fr));display:grid}.grid-3{gap:var(--space-4);grid-template-columns:repeat(3,minmax(0,1fr));display:grid}.grid-4{gap:var(--space-4);grid-template-columns:repeat(4,minmax(0,1fr));display:grid}@media (width<=760px){.grid-2,.grid-3,.grid-4{grid-template-columns:1fr}}.surface-card{background:var(--card);color:var(--card-foreground);border:1px solid var(--border);border-radius:var(--radius-lg);padding:var(--space-5);box-shadow:0 4px 20px #0000001f}.surface-sidebar{backLine truncated
-25
View File
@@ -1,25 +0,0 @@
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,o)=>(o=n==null?{}:e(i(n)),s(r||!n||!n.__esModule||!a.call(n,`default`)?t(o,`default`,{value:n,enumerable:!0}):o,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var l=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.for(`react.view_transition`),m=Symbol.iterator;function h(e){return typeof e!=`object`||!e?null:(e=m&&e[m]||e[`@@iterator`],typeof e==`function`?e:null)}var g={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},_=Object.assign,v={};function y(e,t,n){this.props=e,this.context=t,this.refs=v,this.updater=n||g}y.prototype.isReactComponent={},y.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},y.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function b(){}b.prototype=y.prototype;function ee(e,t,n){this.props=e,this.context=t,this.refs=v,this.updater=n||g}var te=ee.prototype=new b;te.constructor=ee,_(te,y.prototype),te.isPureReactComponent=!0;var ne=Array.isArray;function re(){}var x={H:null,A:null,T:null,S:null},ie=Object.prototype.hasOwnProperty;function S(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function ae(e,t){return S(e.type,t,e.props)}function C(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function oe(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var se=/\/+/g;function ce(e,t){return typeof e==`object`&&e&&e.key!=null?oe(``+e.key):t.toString(36)}function le(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(re,re):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function ue(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,ue(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+ce(e,0):a,ne(o)?(i=``,c!=null&&(i=c.replace(se,`$&/`)+`/`),ue(o,r,i,``,function(e){return e})):o!=null&&(C(o)&&(o=ae(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(se,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(ne(e))for(var u=0;u<e.length;u++)a=e[u],s=l+ce(a,u),c+=ue(a,r,i,s,o);else if(u=h(e),typeof u==`function`)for(e=u.call(e),u=0;!(a=e.next()).done;)a=a.value,s=l+ce(a,u++),c+=ue(a,r,i,s,o);else if(s===`object`){if(typeof e.then==`function`)return ue(le(e),r,i,a,o);throw r=String(e),Error(`Objects are not valid as a React child (found: `+(r===`[object Object]`?`object with keys {`+Object.keys(e).join(`, `)+`}`:r)+`). If you meant to render a collection of children, use an array instead.`)}return c}function de(e,t,n){if(e==null)return e;var r=[],i=0;return ue(e,r,``,``,function(e){return t.call(n,e,i++)}),r}function fe(e){if(e._status===-1){var t=e._result,n=t();n.then(function(t){(e._status===0||e._status===-1)&&(e._status=1,e._result=t,n.status===void 0&&(n.status=`fulfilled`,n.value=t))},function(t){(e._status===0||e._status===-1)&&(e._status=2,e._result=t,n.status===void 0&&(n.status=`rejected`,n.reason=t))}),e._status===-1&&(e._status=Line truncated
at`)?` (<anonymous>)`:-1<e.stack.indexOf(`@`)?`@unknown:0:0`:``}return`
`+Be+e+Ve}var Ue=!1;function D(e,t){if(!e||Ue)return``;Ue=!0;var n=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{var r={DetermineComponentFrameRoot:function(){try{if(t){var n=function(){throw Error()};if(Object.defineProperty(n.prototype,"props",{set:function(){throw Error()}}),typeof Reflect==`object`&&Reflect.construct){try{Reflect.construct(n,[])}catch(e){var r=e}Reflect.construct(e,[],n)}else{try{n.call()}catch(e){r=e}n=!1;try{var i=Object.getOwnPropertyDescriptor(e.prototype,`props`);Object.defineProperty(e.prototype,"props",{configurable:!0,set:function(){throw Error()}}),n=!0,new e}finally{n&&(i===void 0?delete e.prototype.props:Object.defineProperty(e.prototype,"props",i))}}}else{try{throw Error()}catch(e){r=e}(n=e())&&typeof n.catch==`function`&&n.catch(function(){})}}catch(e){if(e&&r&&typeof e.stack==`string`)return[e.stack,r.stack]}return[null,null]}};r.DetermineComponentFrameRoot.displayName=`DetermineComponentFrameRoot`;var i=Object.getOwnPropertyDescriptor(r.DetermineComponentFrameRoot,`name`);i&&i.configurable&&Object.defineProperty(r.DetermineComponentFrameRoot,"name",{value:`DetermineComponentFrameRoot`});var a=r.DetermineComponentFrameRoot(),o=a[0],s=a[1];if(o&&s){var c=o.split(`
`),l=s.split(`
`);for(i=r=0;r<c.length&&!c[r].includes(`DetermineComponentFrameRoot`);)r++;for(;i<l.length&&!l[i].includes(`DetermineComponentFrameRoot`);)i++;if(r===c.length||i===l.length)for(r=c.length-1,i=l.length-1;1<=r&&0<=i&&c[r]!==l[i];)i--;for(;1<=r&&0<=i;r--,i--)if(c[r]!==l[i]){if(r!==1||i!==1)do if(r--,i--,0>i||c[r]!==l[i]){var u=`
`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(`<anonymous>`)&&(u=u.replace(`<anonymous>`,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{Ue=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?He(n):``}function We(e,t){switch(e.tag){case 26:case 27:case 5:return He(e.type);case 16:return He(`Lazy`);case 13:return e.child!==t&&t!==null?He(`Suspense Fallback`):He(`Suspense`);case 19:return He(`SuspenseList`);case 0:case 15:return D(e.type,!1);case 11:return D(e.type.render,!1);case 1:return D(e.type,!0);case 31:return He(`Activity`);case 30:return He(`ViewTransition`);default:return``}}function Ge(e){try{var t=``,n=null;do t+=We(e,n),n=e,e=e.return;while(e);return t}catch(e){return`
Error generating stack: `+e.message+`
`+e.stack}}var Ke=Object.prototype.hasOwnProperty,qe=t.unstable_scheduleCallback,Je=t.unstable_cancelCallback,Ye=t.unstable_shouldYield,Xe=t.unstable_requestPaint,O=t.unstable_now,Ze=t.unstable_getCurrentPriorityLevel,Qe=t.unstable_ImmediatePriority,$e=t.unstable_UserBlockingPriority,et=t.unstable_NormalPriority,tt=t.unstable_LowPriority,nt=t.unstable_IdlePriority,rt=t.log,it=t.unstable_setDisableYieldValue,at=null,ot=null;function st(e){if(typeof rt==`function`&&it(e),ot&&typeof ot.setStrictMode==`function`)try{ot.setStrictMode(at,e)}catch{}}var ct=Math.clz32?Math.clz32:dt,lt=Math.log,ut=Math.LN2;function dt(e){return e>>>=0,e===0?32:31-(lt(e)/ut|0)|0}var ft=256,pt=262144,mt=4194304;function ht(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&-e;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function gt(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=ht(n))):i=ht(o):i=ht(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=ht(n))):i=ht(o)):i=ht(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function _t(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function vt(e,t){t&8&&(t|=t&32);var n=e.entangledLanes;if(n!==0)for(e=e.entanglements,n&=t;0<n;){var r=31-ct(n),i=1<<r;t|=e[r],n&=~i}return t}function yt(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function k(){var e=mt;return mt<<=1,!(mt&62914560)&&(mt=4194304),e}function bt(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function xt(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function St(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0<n;){var u=31-ct(n),d=1<<u;s[u]=0,c[u]=-1;var f=l[u];if(f!==null)for(l[u]=null,u=0;u<f.length;u++){var p=f[u];p!==null&&(p.lane&=-536870913)}n&=~d}r!==0&&Ct(e,r,0),a!==0&&i===0&&e.tag!==0&&(e.suspendedLanes|=a&~(o&~t))}function Ct(e,t,n){e.pendingLanes|=t,e.suspendedLanes&=~t;var r=31-ct(t);e.entangledLanes|=t,e.entanglements[r]=e.entanglements[r]|1073741824|n&261930}function wt(e,t){var n=e.entangledLanes|=t;for(e=e.entanglements;n;){var r=31-ct(n),i=1<<r;i&t|e[r]&t&&(e[r]|=t),n&=~i}}function Tt(e,t){var n=t&-t;return n=n&42?1:Et(n),(n&(e.suspendedLanes|t))===0?n:0}function Et(e){switch(e){case 2:e=1;break;case 8:e=4;break;case 32:e=16;break;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:e=128;break;case 268435456:e=134217728;break;default:e=0}return e}function Dt(e){return e&=-e,2<e?8<e?e&134217727?32:268435456:8:2}function Ot(){var e=T.p;return e===0?(e=window.event,e===void 0?32:Ch(e.type)):e}function kt(e,t){var n=T.p;try{return T.p=e,t()}finally{T.p=n}}var At=Math.random().toString(36).slice(2),jt=`__reactFiber$`+At,Mt=`__reactProps$`+At,Nt=`__reactContainer$`+At,Pt=`__reactEvents$`+At,Ft=`__reactListeners$`+At,It=`__reactHandles$`+At,Lt=`__reactResources$`+At,Rt=`__reactMarker$`+At,zt=`__reactLoad$`+At;function Bt(e){delete e[jt],delete e[Mt],delete e[Ft],delete e[It]}function Vt(e){var t;if(t=e[jt])return t;for(var n=e.parentNode;n;){if(t=n[Nt]||n[jt]){if(n=t.alternate,t.child!==null||n!==null&&n.child!==null)for(e=fm(e);e!==null;){if(n=e[jt])return n;e=fm(e)}return t}e=n,n=e.parentNode}return null}function Ht(e){if(e=e[jt]||e[Nt]){var t=e.tag;if(t===5||t===6||t===13||t===31||t===26||t===27||t===3)return e}return null}function Ut(e){var t=e.tag;if(t===5||t===26||t===27||t===6)return e.stateNode;throw Error(i(33))}function Wt(e){var t=e[Lt];return t||=e[Lt]={hoistableStyles:new Map,hoistableScripts:new Map},t}function Gt(e){e[Rt]=!0}function Kt(e){e[zt]=void 0}var qt=new Set,Jt={};function Yt(e,t){Xt(e,t),Xt(e+`Capture`,t)}function Xt(e,t){for(Jt[e]=t,e=0;e<t.leng Line truncated
`).replace(Qf,``)}function ep(e,t){return t=$f(t),$f(e)===t}function $(e,t,n,r,a,o){switch(n){case`children`:if(typeof r==`string`)t===`body`||t===`textarea`&&r===``||yn(e,r);else if(typeof r==`number`||typeof r==`bigint`)t!==`body`&&yn(e,``+r);else return;break;case`className`:rn(e,`class`,r);break;case`tabIndex`:rn(e,`tabindex`,r);break;case`dir`:case`role`:case`viewBox`:case`width`:case`height`:rn(e,n,r);break;case`style`:Sn(e,r,o);return;case`data`:if(t!==`object`){rn(e,`data`,r);break}case`src`:case`href`:if(r===``&&(t!==`a`||n!==`href`)){e.removeAttribute(n);break}if(r==null||typeof r==`function`||typeof r==`symbol`||typeof r==`boolean`){e.removeAttribute(n);break}r=En(r),e.setAttribute(n,r);break;case`action`:case`formAction`:if(typeof r==`function`){e.setAttribute(n,`javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')`);break}if(typeof o==`function`&&(n===`formAction`?(t!==`input`&&$(e,t,`name`,a.name,a,null),$(e,t,`formEncType`,a.formEncType,a,null),$(e,t,`formMethod`,a.formMethod,a,null),$(e,t,`formTarget`,a.formTarget,a,null)):($(e,t,`encType`,a.encType,a,null),$(e,t,`method`,a.method,a,null),$(e,t,`target`,a.target,a,null))),r==null||typeof r==`symbol`||typeof r==`boolean`){e.removeAttribute(n);break}r=En(r),e.setAttribute(n,r);break;case`onClick`:r!=null&&(e.onclick=Dn);return;case`onScroll`:r!=null&&Q(`scroll`,e);return;case`onScrollEnd`:r!=null&&Q(`scrollend`,e);return;case`dangerouslySetInnerHTML`:if(r!=null){if(typeof r!=`object`||!(`__html`in r))throw Error(i(61));if(n=r.__html,n!=null){if(a.children!=null)throw Error(i(60));o?.__html!==n&&(e.innerHTML=n)}}break;case`multiple`:e.multiple=r&&typeof r!=`function`&&typeof r!=`symbol`;break;case`muted`:e.muted=r&&typeof r!=`function`&&typeof r!=`symbol`;break;case`suppressContentEditableWarning`:case`suppressHydrationWarning`:case`defaultValue`:case`defaultChecked`:case`innerHTML`:case`ref`:break;case`autoFocus`:break;case`xlinkHref`:if(r==null||typeof r==`function`||typeof r==`boolean`||typeof r==`symbol`){e.removeAttribute(`xlink:href`);break}n=En(r),e.setAttributeNS(`http://www.w3.org/1999/xlink`,`xlink:href`,n);break;case`contentEditable`:case`spellCheck`:case`draggable`:case`value`:case`autoReverse`:case`externalResourcesRequired`:case`focusable`:case`preserveAlpha`:r!=null&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,r):e.removeAttribute(n);break;case`inert`:case`allowFullScreen`:case`async`:case`autoPlay`:case`controls`:case`credentialless`:case`default`:case`defer`:case`disabled`:case`disablePictureInPicture`:case`disableRemotePlayback`:case`formNoValidate`:case`hidden`:case`loop`:case`noModule`:case`noValidate`:case`open`:case`playsInline`:case`readOnly`:case`required`:case`reversed`:case`scoped`:case`seamless`:case`itemScope`:r&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,``):e.removeAttribute(n);break;case`capture`:case`download`:!0===r?e.setAttribute(n,``):!1!==r&&r!=null&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,r):e.removeAttribute(n);break;case`cols`:case`rows`:case`size`:case`span`:r!=null&&typeof r!=`function`&&typeof r!=`symbol`&&!isNaN(r)&&1<=r?e.setAttribute(n,r):e.removeAttribute(n);break;case`rowSpan`:case`start`:r==null||typeof r==`function`||typeof r==`symbol`||isNaN(r)?e.removeAttribute(n):e.setAttribute(n,r);break;case`popover`:Q(`beforetoggle`,e),Q(`toggle`,e),nn(e,`popover`,r);break;case`xlinkActuate`:an(e,`http://www.w3.org/1999/xlink`,`xlink:actuate`,r);break;case`xlinkArcrole`:an(e,`http://www.w3.org/1999/xlink`,`xlink:arcrole`,r);break;case`xlinkRole`:an(e,`http://www.w3.org/1999/xlink`,`xlink:role`,r);break;case`xlinkShow`:an(e,`http://www.w3.org/1999/xlink`,`xlink:show`,r);break;case`xlinkTitle`:an(e,`http://www.w3.org/1999/xlink`,`xlink:title`,r);break;case`xlinkType`:an(e,`http://www.w3.org/1999/xlink`,`xlink:type`,r);break;case`xmlBase`:an(e,`http://www.w3.org/XML/1998/namespace`,`xml:base`,r);break;case`xmlLang`:an(e,`http://www.w3.org/XML/1998/namespace`,`xml:lang`,r);break;case`xmlSpace`:an(e,`http://www.w3.org/XML/1998/namespace`,`xml:space`,r);break;case`is`:nn(e,`is`,r);break;case`innerText`:case`textContent`:return;default:if(!(2<n.length)||n[0]!==`o`&&n[0]!==`O`||n[1]!==`n`&&n[1]!==`N`)n=wn.get(n)||n,nn(e,n,r);else return}A=!0}function tp(e,t,n,r,a,o){switch(n){case`style`:Sn(e,r,o);return;case`dangerouslySetInnerHTML`:if(r!=null){if(typeof r!=`object`||!(`__html`in r))throw Error(i(61));if(n=r.__html,n!=null){if(a.children!=null)throw Error(i(60));o?.__html!==n&&(e.innerHTML=n)}}break;case`children`:if(typeof r==`string`)yn(e,r);else if(typeof r==`number`||typeof r==`bigint`)yn(e,``+r);else return;break;case`onScroll`:r!=null&&Q(`scroll`,e);return;case`onScrollEnd`:r!=null&&Q(`scrollend`,e);return;case`onClick`:r!=null&&(e.onclick=D Line truncated
export function `],[`blue`,`DeployAction`],[`plain`,`() {
return (
`],[`plain`,` <Button variant="primary">Deploy changes</Button>
)
}`]];function Ri(){let[e,t]=(0,_.useState)(!1),{schedule:n}=nr();return(0,D.jsxs)(`div`,{className:`code-card`,children:[(0,D.jsxs)(`div`,{className:`code-head`,children:[(0,D.jsx)(`span`,{className:`mono muted`,children:`button.tsx`}),(0,D.jsxs)(`button`,{className:`copy-button`,onClick:()=>{navigator.clipboard?.writeText(Ii),t(!0),n(()=>t(!1),1600)},children:[(0,D.jsx)(j,{name:e?`check`:`copy`,size:14}),e?`Copied`:`Copy code`]})]}),(0,D.jsx)(`pre`,{children:(0,D.jsx)(`code`,{children:Li.map(([e,t],n)=>e===`plain`?(0,D.jsx)(`span`,{children:t},n):(0,D.jsx)(`span`,{className:e,children:t},n))})})]})}function zi(){return(0,D.jsx)(Ri,{})}function Bi({title:e=`Quiet power, shipped weekly.`,kicker:t=`NORTHSTAR / RELEASE NOTES`}){return(0,D.jsxs)(`div`,{className:`markdown-card`,children:[(0,D.jsxs)(`div`,{className:`markdown-toolbar`,children:[(0,D.jsx)(`span`,{className:`eyebrow`,children:`Preview`}),(0,D.jsx)(`span`,{className:`mono muted`,children:`release-notes.md`})]}),(0,D.jsxs)(`article`,{className:`markdown-body`,children:[(0,D.jsx)(`span`,{className:`md-kicker`,children:t}),(0,D.jsx)(`h3`,{children:e}),(0,D.jsx)(`p`,{children:`Our components help teams move quickly without making the interface feel rushed.`}),(0,D.jsxs)(`div`,{className:`md-callout`,children:[(0,D.jsx)(`span`,{className:`callout-mark`,children:`✦`}),(0,D.jsxs)(`p`,{children:[(0,D.jsx)(`strong`,{children:`New in 0.8.2`}),(0,D.jsx)(`br`,{}),`Button focus rings now respect your theme while keeping a dependable contrast ratio.`]})]}),(0,D.jsx)(`h4`,{children:`What changed`}),(0,D.jsxs)(`ul`,{children:[(0,D.jsxs)(`li`,{children:[`Added `,(0,D.jsx)(`code`,{children:`accent-subtle`}),` surface token`]}),(0,D.jsx)(`li`,{children:`Improved keyboard navigation in menus`}),(0,D.jsx)(`li`,{children:`Reduced layout shift during loading states`})]}),(0,D.jsxs)(`div`,{className:`md-table`,children:[(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`span`,{children:`Token`}),(0,D.jsx)(`span`,{children:`Value`}),(0,D.jsx)(`span`,{children:`Role`})]}),(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`code`,{children:`surface-1`}),(0,D.jsx)(`span`,{children:`#111118`}),(0,D.jsx)(`span`,{children:`Elevated panels`})]}),(0,D.jsxs)(`div`,{children:[(0,D.jsx)(`code`,{children:`accent`}),(0,D.jsx)(`span`,{className:`md-accent`,children:`#A69BFF`}),(0,D.jsx)(`span`,{children:`Interactive focus`})]})]})]})]})}function Vi(){return(0,D.jsx)(Bi,{})}var Hi=[1,2,3,4,5,6,7,8];function Ui({fileName:e=`theme.ts`,language:t=`TypeScript`}){return(0,D.jsxs)(`div`,{className:`editor-card`,children:[(0,D.jsxs)(`div`,{className:`editor-tabs`,children:[(0,D.jsx)(`span`,{className:`editor-dot`}),(0,D.jsx)(`span`,{className:`editor-tab active`,children:e}),(0,D.jsx)(`span`,{className:`editor-tab`,children:`tokens.css`}),(0,D.jsx)(`span`,{className:`editor-actions`,children:`● ● ●`})]}),(0,D.jsxs)(`div`,{className:`editor-body`,children:[(0,D.jsx)(`div`,{className:`line-numbers`,children:Hi.map(e=>(0,D.jsxs)(`span`,{children:[e,(0,D.jsx)(`br`,{})]},e))}),(0,D.jsx)(`pre`,{children:(0,D.jsxs)(`code`,{children:[(0,D.jsx)(`span`,{className:`cm-comment`,children:`// A calm editor for focused work`}),`
`,(0,D.jsx)(`span`,{className:`cm-keyword`,children:`export const`}),` `,(0,D.jsx)(`span`,{className:`cm-variable`,children:`theme`}),` = `,`{`,`
`,` `,(0,D.jsx)(`span`,{className:`cm-property`,children:`name`}),`: `,(0,D.jsx)(`span`,{className:`cm-string`,children:`'northstar-dark'`}),`,`,`
`,` `,(0,D.jsx)(`span`,{className:`cm-property`,children:`colors`}),`: `,`{`,`
`,` `,(0,D.jsx)(`span`,{className:`cm-property`,children:`accent`}),`: `,(0,D.jsx)(`span`,{className:`cm-string`,children:`'#a69bff'`}),`,`,`
`,` `,(0,D.jsx)(`span`,{className:`cm-property`,children:`keyword`}),`: `,(0,D.jsx)(`span`,{className:`cm-string`,children:`'#e58bff'`}),`,`,`
`,` `,(0,D.jsx)(`span`,{className:`cm-property`,children:`comment`}),`: `,(0,D.jsx)(`span`,{className:`cm-string`,children:`'#666572'`}),`
`,` `,`}`,`
`,`}`]})})]}),(0,D.jsxs)(`div`,{className:`editor-status`,children:[(0,D.jsx)(`span`,{children:`Ln 4, Col 18`}),(0,D.jsxs)(`span`,{children:[t,` · UTF-8`]}),(0,D.jsxs)(`span`,{className:`editor-ready`,children:[(0,D.jsx)(j,{name:`check`,size:12}),` No issues`]})]})]})}function Wi(){return(0,D.jsx)(Ui,{})}function Gi(e){let t=e??window.location.pathname+window.location.search;window.location.href=`/api/auth/login?returnTo=${encodeURIComponent(t)}`}function Ki(){let[e,t]=(0,_.useState)(`loading`),[n,r]=(0,_.useState)(null);return(0,_.useEffect)(()=>{let e=!0;return fetch(`/api/auth/session`).then(e=>e.ok?e.json():{authenticated:!1,user:null}).then(n=>{e&&(n.authenticated&&n.user?(r(n.user),t(`authenticated`)):(r(null),t(`anonymous`)))}).catch(()=>{e&&(r(null),t(`anonymous`))}),()=>{e=!1}},[]),{status:e,user:n,login:Gi,signOut:(0,_.useCallback)(async()=>{try{await fetch(`/api/auth/logout`,{method:`POST`})}catch{}r(null),t(`anonymous`)},[])}}var qi=class extends Error{status;code;constructor(e,t,n){super(n),this.name=`ApiError`,this.status=e,this.code=t}},Ji=``;async function Yi(e,t){let n;try{n=await fetch(`${Ji}${e}`,{...t,headers:{"Content-Type":`application/json`,...t?.headers}})}catch(e){throw new qi(0,`network`,`Could not reach the API (${e instanceof Error?e.message:String(e)}). Is the Go server running?`)}if(n.status===204)return;let r=await n.json().catch(()=>null);if(!n.ok){let e=r?.error;throw new qi(n.status,e?.code??`unknown`,e?.message??n.statusText)}return r}var Xi=Object.prototype.hasOwnProperty;function Zi(e,t){var n,r;if(e===t)return!0;if(e&&t&&(n=e.constructor)===t.constructor){if(n===Date)return e.getTime()===t.getTime();if(n===RegExp)return e.toString()===t.toString();if(n===Array){if((r=e.length)===t.length)for(;r--&&Zi(e[r],t[r]););return r===-1}if(!n||typeof e==`object`){for(n in r=0,e)if(Xi.call(e,n)&&++r&&!Xi.call(t,n)||!(n in t)||!Zi(e[n],t[n]))return!1;return Object.keys(t).length===r}}return e!==e&&t!==t}var Qi=new WeakMap,$i=()=>{},ea=Object,M=e=>e===void 0,N=e=>typeof e==`function`,ta=(e,t)=>({...e,...t}),na=e=>N(e.then),ra={},ia={},aa=`undefined`,oa=typeof window!=aa,sa=typeof document!=aa,ca=oa&&`Deno`in window,la=()=>oa&&typeof window.requestAnimationFrame!=aa,P=(e,t)=>{let n=Qi.get(e);return[()=>!M(t)&&e.get(t)||ra,r=>{if(!M(t)){let i=e.get(t);t in ia||(ia[t]=i),n[5](t,ta(i,r),i||ra)}},n[6],()=>!M(t)&&t in ia?ia[t]:!M(t)&&e.get(t)||ra]},F=!0,ua=()=>F,[da,fa]=oa&&window.addEventListener?[window.addEventListener.bind(window),window.removeEventListener.bind(window)]:[$i,$i],pa=()=>{let e=sa&&document.visibilityState;return M(e)||e!==`hidden`},ma=e=>(sa&&document.addEventListener(`visibilitychange`,e),da(`focus`,e),()=>{sa&&document.removeEventListener(`visibilitychange`,e),fa(`focus`,e)}),ha=e=>{let t=()=>{F=!0,e()},n=()=>{F=!1};return da(`online`,t),da(`offline`,n),()=>{fa(`online`,t),fa(`offline`,n)}},ga={isOnline:ua,isVisible:pa},_a={initFocus:ma,initReconnect:ha},va=!_.useId,ya=!oa||ca,ba=e=>la()?window.requestAnimationFrame(e):setTimeout(e,1),xa=ya?_.useEffect:_.useLayoutEffect,Sa=typeof navigator<`u`&&navigator.connection,Ca=!ya&&Sa&&([`slow-2g`,`2g`].includes(Sa.effectiveType)||Sa.saveData),wa=new WeakMap,Ta=e=>ea.prototype.toString.call(e),Ea=(e,t)=>e===`[object ${t}]`,Da=0,Oa=e=>{let t=typeof e,n=Ta(e),r=Ea(n,`Date`),i=Ea(n,`RegExp`),a=Ea(n,`Object`),o,s;if(ea(e)===e&&!r&&!i){if(o=wa.get(e),o)return o;if(o=++Da+`~`,wa.set(e,o),Array.isArray(e)){for(o=`@`,s=0;s<e.length;s++)o+=Oa(e[s])+`,`;wa.set(e,o)}if(a){o=`#`;let t=ea.keys(e).sort();for(;!M(s=t.pop());)M(e[s])||(o+=s+`:`+Oa(e[s])+`,`);wa.set(e,o)}}else o=r?e.toJSON():t==`symbol`?e.toString():t==`string`?JSON.stringify(e):``+e;return o},ka=e=>{if(N(e))try{e=e()}catch{e=``}let t=e;return e=typeof e==`string`?e:(Array.isArray(e)?e.length:e)?Oa(e):``,[e,t]},Aa=0,ja=()=>++Aa;async function Ma(...e){let[t,n,r,i]=e,a=ta({populateCache:!0,throwOnError:!0},typeof i==`boolean`?{revalidate:i}:i||{}),o=a.populateCache,s=a.rollbackOnError,c=a.optimisticData,l=e=>typeof s==`function`?s(e):s!==!1,u=a.throwOnError;if(N(n)){let e=n,r=[],i=t.keys();for(let n of i)!/^\$(inf|sub)\$/.test(n)&&e(t.get(n)._k)&&r.push(n);return Promise.all(r.map(d))}return d(n);async function d(n){let[i]=ka(n);if(!i)return;let[s,d]=P(t,i),[f,p,m,h]=Qi.get(t),g=()=>{let e=f[i];return(N(a.revalidate)?a.revalidate(s().data,n):a.revalidate!==!1)&&(delete m[i],delete h[i],e&&e[0])?e[0](2).then(()=>s().data):s().data};if(e.length<3)return g();let _=r,v,y=!1,b=ja();p[i]=[b,0];let ee=!M(c),te=s(),ne=te.data,re=te._c,x=M(re)?ne:re;if(ee&&(c=N(c)?c(x,ne):c,d({data:c,_c:x})),N(_))try{_=_(x)}catch(e){v=e,y=!0}if(_&&na(_)){if(_=await _.catch(e=>{v=e,y=!0}),b!==p[i][0]){if(y)throw v;return _}y&&ee&&l(v)&&(o=!0,d({data:x,_c:void 0}))}if(o&&(y||(N(o)?d({data:o(_,x),error:void 0,_c:void 0}):d({data:_,error:void 0,_c:void 0}))),p[i][1]=ja(),Promise.resolve(g()).then(()=>{d({_c:void 0})}),y){if(u)throw v;return}return _}}var Na=(e,t)=>{for(let n in e)e[n][0]&&e[n][0](t)},Pa= Line truncated
`,e.message]}):this.props.children}};(0,v.createRoot)(document.getElementById(`root`)).render((0,D.jsx)(_.StrictMode,{children:(0,D.jsx)(R,{children:(0,D.jsx)(Ye,{children:(0,D.jsx)(L,{})})})}));
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filtLine truncated

Before

Width:  |  Height:  |  Size: 9.3 KiB

-24
View File
@@ -1,24 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

-37
View File
@@ -1,37 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark light" />
<title>Northstar Design System</title>
<script>
/* Pre-paint theme: apply the saved (or OS-preferred) theme before any
CSS paints so there is no dark→light flash. Keep in sync with
applyTheme() in src/components/Topbar.tsx. */
(function () {
try {
var saved = localStorage.getItem('ns-theme')
var theme =
saved === 'light' || saved === 'dark'
? saved
: window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches
? 'light'
: 'dark'
var root = document.documentElement
root.setAttribute('data-theme', theme)
root.classList.add(theme)
} catch (e) {
/* localStorage unavailable — tokens.css defaults to dark */
}
})()
</script>
<script type="module" crossorigin src="/assets/index-cXoFpIQ4.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DGNRMRfX.css">
</head>
<body>
<div id="root"></div>
<div id="dev-errors"></div>
</body>
</html>
-36
View File
@@ -1,36 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark light" />
<title>Northstar Design System</title>
<script>
/* Pre-paint theme: apply the saved (or OS-preferred) theme before any
CSS paints so there is no dark→light flash. Keep in sync with
applyTheme() in src/components/Topbar.tsx. */
(function () {
try {
var saved = localStorage.getItem('ns-theme')
var theme =
saved === 'light' || saved === 'dark'
? saved
: window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches
? 'light'
: 'dark'
var root = document.documentElement
root.setAttribute('data-theme', theme)
root.classList.add(theme)
} catch (e) {
/* localStorage unavailable — tokens.css defaults to dark */
}
})()
</script>
</head>
<body>
<div id="root"></div>
<div id="dev-errors"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
-1
View File
@@ -1 +0,0 @@
../lz-string/bin/bin.js
-1
View File
@@ -1 +0,0 @@
../nanoid/bin/nanoid.cjs
-1
View File
@@ -1 +0,0 @@
../oxlint/bin/oxlint
-1
View File
@@ -1 +0,0 @@
../rolldown/bin/cli.mjs
-1
View File
@@ -1 +0,0 @@
../@bramus/specificity/bin/cli.js
-1
View File
@@ -1 +0,0 @@
../tldts/bin/cli.js
-1
View File
@@ -1 +0,0 @@
../typescript/bin/tsc
-1
View File
@@ -1 +0,0 @@
../typescript/bin/tsserver
-1
View File
@@ -1 +0,0 @@
../vite/bin/vite.js
-1
View File
@@ -1 +0,0 @@
../vitest/vitest.mjs
-1
View File
@@ -1 +0,0 @@
../why-is-node-running/cli.js
-1689
View File
File diff suppressed because it is too large. Load diff
-1
View File
@@ -1 +0,0 @@
{"root":["../../src/App.tsx","../../src/main.tsx","../../src/api/client.ts","../../src/api/items.ts","../../src/app/AppStateProvider.tsx","../../src/app/appState.ts","../../src/app/index.ts","../../src/app/persist.ts","../../src/app/routes.tsx","../../src/auth/SessionCard.tsx","../../src/auth/UserMenu.tsx","../../src/auth/useSession.ts","../../src/components/AIWorkflowSection.tsx","../../src/components/Button.tsx","../../src/components/ButtonMatrix.tsx","../../src/components/CodeEditor.tsx","../../src/components/CodeSample.tsx","../../src/components/FormField.tsx","../../src/components/Hero.tsx","../../src/components/Icon.tsx","../../src/components/IconMatrix.tsx","../../src/components/MarkdownRenderer.tsx","../../src/components/RightRail.tsx","../../src/components/SearchPalette.tsx","../../src/components/Section.tsx","../../src/components/Sidebar.tsx","../../src/components/TokenGrid.tsx","../../src/components/Topbar.tsx","../../src/components/TypographySection.tsx","../../src/components/icon-registry.ts","../../src/components/examples/DeploymentModal.tsx","../../src/components/examples/MetricsDashboard.tsx","../../src/components/examples/ServiceTable.tsx","../../src/components/examples/SettingsPanel.tsx","../../src/components/examples/index.ts","../../src/components/ui/Alert.tsx","../../src/components/ui/Avatar.tsx","../../src/components/ui/Badge.tsx","../../src/components/ui/Card.tsx","../../src/components/ui/Dialog.tsx","../../src/components/ui/EmptyState.tsx","../../src/components/ui/Kbd.tsx","../../src/components/ui/Overlay.tsx","../../src/components/ui/Progress.tsx","../../src/components/ui/Sheet.tsx","../../src/components/ui/Skeleton.tsx","../../src/components/ui/Spinner.tsx","../../src/components/ui/StatCard.tsx","../../src/components/ui/Switch.tsx","../../src/components/ui/Table.tsx","../../src/components/ui/Tabs.tsx","../../src/components/ui/Tooltip.tsx","../../src/components/ui/index.ts","../../src/components/ui/overlayBehavior.ts","../../src/data/library.ts","../../src/data/patterns.ts","../../src/dev/ErrorBoundary.tsx","../../src/dev/errorSink.ts","../../src/hooks/useItems.ts","../../src/pages/ButtonsPage.tsx","../../src/pages/ColorsPage.tsx","../../src/pages/EditorPage.tsx","../../src/pages/FormsPage.tsx","../../src/pages/IconsPage.tsx","../../src/pages/ItemsPage.tsx","../../src/pages/MarkdownPage.tsx","../../src/pages/NotFoundPage.tsx","../../src/pages/OverviewPage.tsx","../../src/pages/PatternsPage.tsx","../../src/pages/PrimitivesPage.tsx","../../src/pages/TypographyPage.tsx","../../src/utils/cn.ts","../../src/utils/useTimeout.ts"],"version":"6.0.3"}
-1
View File
@@ -1 +0,0 @@
{"root":["../../vite.config.ts"],"version":"6.0.3"}
-66
View File
@@ -1,66 +0,0 @@
{
"hash": "976b9c3d",
"configHash": "89e67727",
"lockfileHash": "0d4791a3",
"browserHash": "4b2fbac7",
"optimized": {
"lucide-react": {
"src": "../../lucide-react/dist/esm/lucide-react.mjs",
"file": "lucide-react.js",
"fileHash": "6c3cfb80",
"needsInterop": false
},
"react-dom": {
"src": "../../react-dom/index.js",
"file": "react-dom.js",
"fileHash": "25030b7a",
"needsInterop": true
},
"react-dom/client": {
"src": "../../react-dom/client.js",
"file": "react-dom_client.js",
"fileHash": "de15c997",
"needsInterop": true
},
"react": {
"src": "../../react/index.js",
"file": "react.js",
"fileHash": "e97c5b5e",
"needsInterop": true
},
"react/jsx-dev-runtime": {
"src": "../../react/jsx-dev-runtime.js",
"file": "react_jsx-dev-runtime.js",
"fileHash": "15b8f65b",
"needsInterop": true
},
"react/jsx-runtime": {
"src": "../../react/jsx-runtime.js",
"file": "react_jsx-runtime.js",
"fileHash": "6d9e10ca",
"needsInterop": true
},
"swr": {
"src": "../../swr/dist/index/index.mjs",
"file": "swr.js",
"fileHash": "5dedc4fa",
"needsInterop": false
},
"wouter": {
"src": "../../wouter/src/index.js",
"file": "wouter.js",
"fileHash": "9e6a9f9c",
"needsInterop": false
}
},
"chunks": {
"rolldown-runtime-DC62tzP2": {
"file": "rolldown-runtime-DC62tzP2.js",
"isDynamicEntry": false
},
"shim-BX6a9SD7": {
"file": "shim-BX6a9SD7.js",
"isDynamicEntry": false
}
}
}
-63820
View File
File diff suppressed because it is too large. Load diff
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"lucide-react.js","names":["createContext","useMemo","createElement","useContext","forwardRef","createElement","forwardRef","createElement","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__iconData","__Line truncated
-3
View File
@@ -1,3 +0,0 @@
{
"type": "module"
}
-198
View File
@@ -1,198 +0,0 @@
import { t as __commonJSMin } from "./rolldown-runtime-DC62tzP2.js";
import { t as require_react } from "./react.js";
//#region node_modules/react-dom/cjs/react-dom.development.js
/**
* @license React
* react-dom.development.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
var require_react_dom_development = /* @__PURE__ */ __commonJSMin(((exports) => {
(function() {
function noop() {}
function testStringCoercion(value) {
return "" + value;
}
function createPortal$1(children, containerInfo, implementation) {
var key = 3 < arguments.length && void 0 !== arguments[3] ? arguments[3] : null;
if (null == key) key = null;
else if (key === REACT_OPTIMISTIC_KEY) key = REACT_OPTIMISTIC_KEY;
else {
try {
testStringCoercion(key);
var JSCompiler_inline_result = !1;
} catch (e) {
JSCompiler_inline_result = !0;
}
JSCompiler_inline_result && (console.error("The provided key is an unsupported type %s. This value must be coerced to a string before using it here.", "function" === typeof Symbol && Symbol.toStringTag && key[Symbol.toStringTag] || key.constructor.name || "Object"), testStringCoercion(key));
key = "" + key;
}
return {
$$typeof: REACT_PORTAL_TYPE,
key,
children,
containerInfo,
implementation
};
}
function getCrossOriginStringAs(as, input) {
if ("font" === as) return "";
if ("string" === typeof input) return "use-credentials" === input ? input : "";
}
function getValueDescriptorExpectingObjectForWarning(thing) {
return null === thing ? "`null`" : void 0 === thing ? "`undefined`" : "" === thing ? "an empty string" : "something with type \"" + typeof thing + "\"";
}
function getValueDescriptorExpectingEnumForWarning(thing) {
return null === thing ? "`null`" : void 0 === thing ? "`undefined`" : "" === thing ? "an empty string" : "string" === typeof thing ? JSON.stringify(thing) : "number" === typeof thing ? "`" + thing + "`" : "something with type \"" + typeof thing + "\"";
}
function resolveDispatcher() {
var dispatcher = ReactSharedInternals.H;
null === dispatcher && console.error("Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.");
return dispatcher;
}
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());
var React = require_react(), Internals = {
d: {
f: noop,
r: function() {
throw Error("Invalid form element. requestFormReset must be passed a form that was rendered by React.");
},
D: noop,
C: noop,
L: noop,
m: noop,
X: noop,
S: noop,
M: noop
},
p: 0,
findDOMNode: null
}, REACT_PORTAL_TYPE = Symbol.for("react.portal"), REACT_RECOVERABLE_TYPE = Symbol.for("react.recoverable"), REACT_OPTIMISTIC_KEY = Symbol.for("react.optimistic_key"), ReactSharedInternals = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
"function" === typeof Map && null != Map.prototype && "function" === typeof Map.prototype.forEach && "function" === typeof Set && null != Set.prototype && "function" === typeof Set.prototype.clear && "function" === typeof Set.prototype.forEach || console.error("React depends on Map and Set built-in types. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills");
exports.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = Internals;
exports.browser = function(reason) {
return {
$$typeof: REACT_RECOVERABLE_TYPE,
_reason: reason
};
};
exports.createPortal = function(children, container) {
var key = 2 < arguments.length && void 0 !== arguments[2] ? arguments[2] : null;
if (!container || 1 !== container.nodeType && 9 !== container.nodeType && 11 !== container.nodeType) throw Error("Target container is not a DOM element.");
return createPortal$1(children, container, null, key);
};
exports.flushSync = function(fn) {
var previousTransition = ReactSharedInternals.T, previousUpdatePriority = Internals.p;
try {
if (ReactSharedInternals.T = null, Internals.p = 2, fn) return fn();
} finally {
ReactSharedInternals.T = previousTransition, Internals.p = previousUpdatePriority, Internals.d.f() && console.error("flushSync was called from inside a lifecycle method. React cannot flush when React is already rendering. Consider moving this call to a scheduler task or micro task.");
}
};
exports.preconnect = function(href, options) {
"string" === typeof href && href ? null != options && "object" !== typeof options ? console.error("ReactDOM.preconnect(): Expected the `options` argument (second) to be an object but encountered %s instead. The only supported option at this time is `crossOrigin` which accepts a string.", getValueDescriptorExpectingEnumForWarning(options)) : null != options && "string" !== typeof options.crossOrigin && console.error("ReactDOM.preconnect(): Expected the `crossOrigin` option (second argument) to be a string but encountered %s instead. Try removing this option or passing a string value instead.", getValueDescriptorExpectingObjectForWarning(options.crossOrigin)) : console.error("ReactDOM.preconnect(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.", getValueDescriptorExpectingObjectForWarning(href));
"string" === typeof href && (options ? (options = options.crossOrigin, options = "string" === typeof options ? "use-credentials" === options ? options : "" : void 0) : options = null, Internals.d.C(href, options));
};
exports.prefetchDNS = function(href) {
if ("string" !== typeof href || !href) console.error("ReactDOM.prefetchDNS(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.", getValueDescriptorExpectingObjectForWarning(href));
else if (1 < arguments.length) {
var options = arguments[1];
"object" === typeof options && options.hasOwnProperty("crossOrigin") ? console.error("ReactDOM.prefetchDNS(): Expected only one argument, `href`, but encountered %s as a second argument instead. This argument is reserved for future options and is currently disallowed. It looks like the you are attempting to set a crossOrigin property for this DNS lookup hint. Browsers do not perform DNS queries using CORS and setting this attribute on the resource hint has no effect. Try calling ReactDOM.prefetchDNS() with just a single string argument, `href`.", getValueDescriptorExpectingEnumForWarning(options)) : console.error("ReactDOM.prefetchDNS(): Expected only one argument, `href`, but encountered %s as a second argument instead. This argument is reserved for future options and is currently disallowed. Try calling ReactDOM.prefetchDNS() with just a single string argument, `href`.", getValueDescriptorExpectingEnumForWarning(options));
}
"string" === typeof href && Internals.d.D(href);
};
exports.preinit = function(href, options) {
"string" === typeof href && href ? null == options || "object" !== typeof options ? console.error("ReactDOM.preinit(): Expected the `options` argument (second) to be an object with an `as` property describing the type of resource to be preinitialized but encountered %s instead.", getValueDescriptorExpectingEnumForWarning(options)) : "style" !== options.as && "script" !== options.as && console.error("ReactDOM.preinit(): Expected the `as` property in the `options` argument (second) to contain a valid value describing the type of resource to be preinitialized but encountered %s instead. Valid values for `as` are \"style\" and \"script\".", getValueDescriptorExpectingEnumForWarning(options.as)) : console.error("ReactDOM.preinit(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.", getValueDescriptorExpectingObjectForWarning(href));
if ("string" === typeof href && options && "string" === typeof options.as) {
var as = options.as, crossOrigin = getCrossOriginStringAs(as, options.crossOrigin), integrity = "string" === typeof options.integrity ? options.integrity : void 0, fetchPriority = "string" === typeof options.fetchPriority ? options.fetchPriority : void 0;
"style" === as ? Internals.d.S(href, "string" === typeof options.precedence ? options.precedence : void 0, {
crossOrigin,
integrity,
fetchPriority
}) : "script" === as && Internals.d.X(href, {
crossOrigin,
integrity,
fetchPriority,
nonce: "string" === typeof options.nonce ? options.nonce : void 0
});
}
};
exports.preinitModule = function(href, options) {
var encountered = "";
"string" === typeof href && href || (encountered += " The `href` argument encountered was " + getValueDescriptorExpectingObjectForWarning(href) + ".");
void 0 !== options && "object" !== typeof options ? encountered += " The `options` argument encountered was " + getValueDescriptorExpectingObjectForWarning(options) + "." : options && "as" in options && "script" !== options.as && (encountered += " The `as` option encountered was " + getValueDescriptorExpectingEnumForWarning(options.as) + ".");
if (encountered) console.error("ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property.%s", encountered);
else switch (encountered = options && "string" === typeof options.as ? options.as : "script", encountered) {
case "script": break;
default: encountered = getValueDescriptorExpectingEnumForWarning(encountered), console.error("ReactDOM.preinitModule(): Currently the only supported \"as\" type for this function is \"script\" but received \"%s\" instead. This warning was generated for `href` \"%s\". In the future other module types will be supported, aligning with the import-attributes proposal. Learn more here: (https://github.com/tc39/proposal-import-attributes)", encountered, href);
}
if ("string" === typeof href) if ("object" === typeof options && null !== options) {
if (null == options.as || "script" === options.as) encountered = getCrossOriginStringAs(options.as, options.crossOrigin), Internals.d.M(href, {
crossOrigin: encountered,
integrity: "string" === typeof options.integrity ? options.integrity : void 0,
nonce: "string" === typeof options.nonce ? options.nonce : void 0,
fetchPriority: "string" === typeof options.fetchPriority ? options.fetchPriority : void 0
});
} else options ?? Internals.d.M(href);
};
exports.preload = function(href, options) {
var encountered = "";
"string" === typeof href && href || (encountered += " The `href` argument encountered was " + getValueDescriptorExpectingObjectForWarning(href) + ".");
null == options || "object" !== typeof options ? encountered += " The `options` argument encountered was " + getValueDescriptorExpectingObjectForWarning(options) + "." : "string" === typeof options.as && options.as || (encountered += " The `as` option encountered was " + getValueDescriptorExpectingObjectForWarning(options.as) + ".");
encountered && console.error("ReactDOM.preload(): Expected two arguments, a non-empty `href` string and an `options` object with an `as` property valid for a `<link rel=\"preload\" as=\"...\" />` tag.%s", encountered);
if ("string" === typeof href && "object" === typeof options && null !== options && "string" === typeof options.as) {
encountered = options.as;
var crossOrigin = getCrossOriginStringAs(encountered, options.crossOrigin);
Internals.d.L(href, encountered, {
crossOrigin,
integrity: "string" === typeof options.integrity ? options.integrity : void 0,
nonce: "string" === typeof options.nonce ? options.nonce : void 0,
type: "string" === typeof options.type ? options.type : void 0,
fetchPriority: "string" === typeof options.fetchPriority ? options.fetchPriority : void 0,
referrerPolicy: "string" === typeof options.referrerPolicy ? options.referrerPolicy : void 0,
imageSrcSet: "string" === typeof options.imageSrcSet ? options.imageSrcSet : void 0,
imageSizes: "string" === typeof options.imageSizes ? options.imageSizes : void 0,
media: "string" === typeof options.media ? options.media : void 0
});
}
};
exports.preloadModule = function(href, options) {
var encountered = "";
"string" === typeof href && href || (encountered += " The `href` argument encountered was " + getValueDescriptorExpectingObjectForWarning(href) + ".");
void 0 !== options && "object" !== typeof options ? encountered += " The `options` argument encountered was " + getValueDescriptorExpectingObjectForWarning(options) + "." : options && "as" in options && "string" !== typeof options.as && (encountered += " The `as` option encountered was " + getValueDescriptorExpectingObjectForWarning(options.as) + ".");
encountered && console.error("ReactDOM.preloadModule(): Expected two arguments, a non-empty `href` string and, optionally, an `options` object with an `as` property valid for a `<link rel=\"modulepreload\" as=\"...\" />` tag.%s", encountered);
"string" === typeof href && (options ? (encountered = getCrossOriginStringAs(options.as, options.crossOrigin), Internals.d.m(href, {
as: "string" === typeof options.as && "script" !== options.as ? options.as : void 0,
crossOrigin: encountered,
integrity: "string" === typeof options.integrity ? options.integrity : void 0,
nonce: "string" === typeof options.nonce ? options.nonce : void 0,
fetchPriority: "string" === typeof options.fetchPriority ? options.fetchPriority : void 0
})) : Internals.d.m(href));
};
exports.requestFormReset = function(form) {
Internals.d.r(form);
};
exports.unstable_batchedUpdates = function(fn, a) {
return fn(a);
};
exports.useFormState = function(action, initialState, permalink) {
return resolveDispatcher().useFormState(action, initialState, permalink);
};
exports.useFormStatus = function() {
return resolveDispatcher().useHostTransitionStatus();
};
exports.version = "19.3.0";
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error());
})();
}));
//#endregion
//#region node_modules/react-dom/index.js
var require_react_dom = /* @__PURE__ */ __commonJSMin(((exports, module) => {
module.exports = require_react_dom_development();
}));
//#endregion
export default require_react_dom();
export { require_react_dom as t };
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"react-dom.js","names":[],"sources":["../../react-dom/cjs/react-dom.development.js","../../react-dom/index.js"],"sourcesContent":["/**\n * @license React\n * react-dom.development.js\n *\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n\"use strict\";\n\"production\" !== process.env.NODE_ENV &&\n (function () {\n function noop() {}\n function testStringCoercion(value) {\n return \"\" + value;\n }\n function createPortal$1(children, containerInfo, implementation) {\n var key =\n 3 < arguments.length && void 0 !== arguments[3] ? arguments[3] : null;\n if (null == key) key = null;\n else if (key === REACT_OPTIMISTIC_KEY) key = REACT_OPTIMISTIC_KEY;\n else {\n try {\n testStringCoercion(key);\n var JSCompiler_inline_result = !1;\n } catch (e) {\n JSCompiler_inline_result = !0;\n }\n JSCompiler_inline_result &&\n (console.error(\n \"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.\",\n (\"function\" === typeof Symbol &&\n Symbol.toStringTag &&\n key[Symbol.toStringTag]) ||\n key.constructor.name ||\n \"Object\"\n ),\n testStringCoercion(key));\n key = \"\" + key;\n }\n return {\n $$typeof: REACT_PORTAL_TYPE,\n key: key,\n children: children,\n containerInfo: containerInfo,\n implementation: implementation\n };\n }\n function getCrossOriginStringAs(as, input) {\n if (\"font\" === as) return \"\";\n if (\"string\" === typeof input)\n return \"use-credentials\" === input ? input : \"\";\n }\n function getValueDescriptorExpectingObjectForWarning(thing) {\n return null === thing\n ? \"`null`\"\n : void 0 === thing\n ? \"`undefined`\"\n : \"\" === thing\n ? \"an empty string\"\n : 'something with type \"' + typeof thing + '\"';\n }\n function getValueDescriptorExpectingEnumForWarning(thing) {\n return null === thing\n ? \"`null`\"\n : void 0 === thing\n ? \"`undefined`\"\n : \"\" === thing\n ? \"an empty string\"\n : \"string\" === typeof thing\n ? JSON.stringify(thing)\n : \"number\" === typeof thing\n ? \"`\" + thing + \"`\"\n : 'something with type \"' + typeof thing + '\"';\n }\n function resolveDispatcher() {\n var dispatcher = ReactSharedInternals.H;\n null === dispatcher &&\n console.error(\n \"Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\\n1. You might have mismatching versions of React and the renderer (such as React DOM)\\n2. You might be breaking the Rules of Hooks\\n3. You might have more than one copy of React in the same app\\nSee https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.\"\n );\n return dispatcher;\n }\n \"undefined\" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&\n \"function\" ===\n typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart &&\n __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());\n var React = require(\"react\"),\n Internals = {\n d: {\n f: noop,\n r: function () {\n throw Error(\n \"Invalid form element. requestFormReset must be passed a form that was rendered by React.\"\n );\n },\n D: noop,\n C: noop,\n L: noop,\n m: noop,\n X: noop,\n S: noop,\n M: noop\n },\n p: 0,\n findDOMNode: null\n },\n REACT_PORTAL_TYPE = Symbol.for(\"react.portal\"),\n REACT_RECOVERABLE_TYPE = Symbol.for(\"react.recoverable\"),\n REACT_OPTIMISTIC_KEY = Symbol.for(\"react.optimistic_key\"),\n ReactSharedInternals =\n React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;\n (\"function\" === typeof Map &&\n null != Map.prototype &&\n \"function\" === typeof Map.prototype.forEach &&\n \"function\" === typeof Set &&\n null != Set.prototype &&\n \"function\" === typeof Set.prototype.clear &&\n \"function\" === typeof Set.prototype.forEach) ||\n console.error(\n \"React depends on Map and Set built-in types. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills\"\n );\n exports.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE =\n Internals;\n exports.browser = function (reason) {\n retLine truncated
File diff suppressed because it is too large. Load diff
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"react-dom_client.js","names":[],"sources":["../../scheduler/cjs/scheduler.development.js","../../scheduler/index.js","../../react-dom/cjs/react-dom-client.development.js","../../react-dom/client.js"],"sourcesContent":["/**\n * @license React\n * scheduler.development.js\n *\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n\"use strict\";\n\"production\" !== process.env.NODE_ENV &&\n (function () {\n function performWorkUntilDeadline() {\n needsPaint = !1;\n if (isMessageLoopRunning) {\n var currentTime = exports.unstable_now();\n startTime = currentTime;\n var hasMoreWork = !0;\n try {\n a: {\n isHostCallbackScheduled = !1;\n isHostTimeoutScheduled &&\n ((isHostTimeoutScheduled = !1),\n localClearTimeout(taskTimeoutID),\n (taskTimeoutID = -1));\n isPerformingWork = !0;\n var previousPriorityLevel = currentPriorityLevel;\n try {\n b: {\n advanceTimers(currentTime);\n for (\n currentTask = peek(taskQueue);\n null !== currentTask &&\n !(\n currentTask.expirationTime > currentTime &&\n shouldYieldToHost()\n );\n\n ) {\n var callback = currentTask.callback;\n if (\"function\" === typeof callback) {\n currentTask.callback = null;\n currentPriorityLevel = currentTask.priorityLevel;\n var continuationCallback = callback(\n currentTask.expirationTime <= currentTime\n );\n currentTime = exports.unstable_now();\n if (\"function\" === typeof continuationCallback) {\n currentTask.callback = continuationCallback;\n advanceTimers(currentTime);\n hasMoreWork = !0;\n break b;\n }\n currentTask === peek(taskQueue) && pop(taskQueue);\n advanceTimers(currentTime);\n } else pop(taskQueue);\n currentTask = peek(taskQueue);\n }\n if (null !== currentTask) hasMoreWork = !0;\n else {\n var firstTimer = peek(timerQueue);\n null !== firstTimer &&\n requestHostTimeout(\n handleTimeout,\n firstTimer.startTime - currentTime\n );\n hasMoreWork = !1;\n }\n }\n break a;\n } finally {\n (currentTask = null),\n (currentPriorityLevel = previousPriorityLevel),\n (isPerformingWork = !1);\n }\n hasMoreWork = void 0;\n }\n } finally {\n hasMoreWork\n ? schedulePerformWorkUntilDeadline()\n : (isMessageLoopRunning = !1);\n }\n }\n }\n function push(heap, node) {\n var index = heap.length;\n heap.push(node);\n a: for (; 0 < index; ) {\n var parentIndex = (index - 1) >>> 1,\n parent = heap[parentIndex];\n if (0 < compare(parent, node))\n (heap[parentIndex] = node),\n (heap[index] = parent),\n (index = parentIndex);\n else break a;\n }\n }\n function peek(heap) {\n return 0 === heap.length ? null : heap[0];\n }\n function pop(heap) {\n if (0 === heap.length) return null;\n var first = heap[0],\n last = heap.pop();\n if (last !== first) {\n heap[0] = last;\n a: for (\n var index = 0, length = heap.length, halfLength = length >>> 1;\n index < halfLength;\n\n ) {\n var leftIndex = 2 * (index + 1) - 1,\n left = heap[leftIndex],\n rightIndex = leftIndex + 1,\n right = heap[rightIndex];\n if (0 > compare(left, last))\n rightIndex < length && 0 > compare(right, left)\n ? ((heap[index] = right),\n (heap[rightIndex] = last),\n (index = rightIndex))\n : ((heap[index] = left),\n (heap[leftIndex] = last),\n (index = leftIndex));\n else if (rightIndex < length && 0 > compare(right, last))\n (heap[index] = right),\n (heap[rightIndex] = last),\n (index = rightIndex);\n else break a;\n }\n }\n return first;\n }\n function compare(a, b) {\n var diff = a.sortIndex - b.sortIndex;\n return 0 !== diff ? diff : a.id - b.id;\n Line truncated
-788
View File
@@ -1,788 +0,0 @@
import { t as __commonJSMin } from "./rolldown-runtime-DC62tzP2.js";
//#region node_modules/react/cjs/react.development.js
/**
* @license React
* react.development.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
var require_react_development = /* @__PURE__ */ __commonJSMin(((exports, module) => {
(function() {
function defineDeprecationWarning(methodName, info) {
Object.defineProperty(Component.prototype, methodName, { get: function() {
console.warn("%s(...) is deprecated in plain JavaScript React classes. %s", info[0], info[1]);
} });
}
function getIteratorFn(maybeIterable) {
if (null === maybeIterable || "object" !== typeof maybeIterable) return null;
maybeIterable = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable["@@iterator"];
return "function" === typeof maybeIterable ? maybeIterable : null;
}
function warnNoop(publicInstance, callerName) {
publicInstance = (publicInstance = publicInstance.constructor) && (publicInstance.displayName || publicInstance.name) || "ReactClass";
var warningKey = publicInstance + "." + callerName;
didWarnStateUpdateForUnmountedComponent[warningKey] || (console.error("Can't call %s on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to `this.state` directly or define a `state = {};` class property with the desired state in the %s component.", callerName, publicInstance), didWarnStateUpdateForUnmountedComponent[warningKey] = !0);
}
function Component(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
function ComponentDummy() {}
function PureComponent(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
function noop() {}
function testStringCoercion(value) {
return "" + value;
}
function checkKeyStringCoercion(value) {
try {
testStringCoercion(value);
var JSCompiler_inline_result = !1;
} catch (e) {
JSCompiler_inline_result = !0;
}
if (JSCompiler_inline_result) {
JSCompiler_inline_result = console;
var JSCompiler_temp_const = JSCompiler_inline_result.error;
var JSCompiler_inline_result$jscomp$0 = "function" === typeof Symbol && Symbol.toStringTag && value[Symbol.toStringTag] || value.constructor.name || "Object";
JSCompiler_temp_const.call(JSCompiler_inline_result, "The provided key is an unsupported type %s. This value must be coerced to a string before using it here.", JSCompiler_inline_result$jscomp$0);
return testStringCoercion(value);
}
}
function getComponentNameFromType(type) {
if (null == type) return null;
if ("function" === typeof type) return type.$$typeof === REACT_CLIENT_REFERENCE ? null : type.displayName || type.name || null;
if ("string" === typeof type) return type;
switch (type) {
case REACT_FRAGMENT_TYPE: return "Fragment";
case REACT_PROFILER_TYPE: return "Profiler";
case REACT_STRICT_MODE_TYPE: return "StrictMode";
case REACT_SUSPENSE_TYPE: return "Suspense";
case REACT_SUSPENSE_LIST_TYPE: return "SuspenseList";
case REACT_ACTIVITY_TYPE: return "Activity";
case REACT_VIEW_TRANSITION_TYPE: return "ViewTransition";
}
if ("object" === typeof type) switch ("number" === typeof type.tag && console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."), type.$$typeof) {
case REACT_PORTAL_TYPE: return "Portal";
case REACT_CONTEXT_TYPE: return type.displayName || "Context";
case REACT_CONSUMER_TYPE: return (type._context.displayName || "Context") + ".Consumer";
case REACT_FORWARD_REF_TYPE:
var innerType = type.render;
type = type.displayName;
type || (type = innerType.displayName || innerType.name || "", type = "" !== type ? "ForwardRef(" + type + ")" : "ForwardRef");
return type;
case REACT_MEMO_TYPE: return innerType = type.displayName || null, null !== innerType ? innerType : getComponentNameFromType(type.type) || "Memo";
case REACT_LAZY_TYPE:
innerType = type._payload;
type = type._init;
try {
return getComponentNameFromType(type(innerType));
} catch (x) {}
}
return null;
}
function getTaskName(type) {
if (type === REACT_FRAGMENT_TYPE) return "<>";
if ("object" === typeof type && null !== type && type.$$typeof === REACT_LAZY_TYPE) return "<...>";
try {
var name = getComponentNameFromType(type);
return name ? "<" + name + ">" : "<...>";
} catch (x) {
return "<...>";
}
}
function getOwner() {
var dispatcher = ReactSharedInternals.A;
return null === dispatcher ? null : dispatcher.getOwner();
}
function UnknownOwner() {
return Error("react-stack-top-frame");
}
function hasValidKey(config) {
if (hasOwnProperty.call(config, "key")) {
var getter = Object.getOwnPropertyDescriptor(config, "key").get;
if (getter && getter.isReactWarning) return !1;
}
return void 0 !== config.key;
}
function defineKeyPropWarningGetter(props, displayName) {
function warnAboutAccessingKey() {
specialPropKeyWarningShown || (specialPropKeyWarningShown = !0, console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)", displayName));
}
warnAboutAccessingKey.isReactWarning = !0;
Object.defineProperty(props, "key", {
get: warnAboutAccessingKey,
configurable: !0
});
}
function elementRefGetterWithDeprecationWarning() {
var componentName = getComponentNameFromType(this.type);
didWarnAboutElementRef[componentName] || (didWarnAboutElementRef[componentName] = !0, console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release."));
componentName = this.props.ref;
return void 0 !== componentName ? componentName : null;
}
function ReactElement(type, key, props, owner, debugStack, debugTask) {
var refProp = props.ref;
type = {
$$typeof: REACT_ELEMENT_TYPE,
type,
key,
props,
_owner: owner
};
null !== (void 0 !== refProp ? refProp : null) ? Object.defineProperty(type, "ref", {
enumerable: !1,
get: elementRefGetterWithDeprecationWarning
}) : Object.defineProperty(type, "ref", {
enumerable: !1,
value: null
});
type._store = {};
Object.defineProperty(type._store, "validated", {
configurable: !1,
enumerable: !1,
writable: !0,
value: 0
});
Object.defineProperty(type, "_debugInfo", {
configurable: !1,
enumerable: !1,
writable: !0,
value: null
});
Object.defineProperty(type, "_debugStack", {
configurable: !1,
enumerable: !1,
writable: !0,
value: debugStack
});
Object.defineProperty(type, "_debugTask", {
configurable: !1,
enumerable: !1,
writable: !0,
value: debugTask
});
Object.freeze && (Object.freeze(type.props), Object.freeze(type));
return type;
}
function cloneAndReplaceKey(oldElement, newKey) {
newKey = ReactElement(oldElement.type, newKey, oldElement.props, oldElement._owner, oldElement._debugStack, oldElement._debugTask);
oldElement._store && (newKey._store.validated = oldElement._store.validated);
return newKey;
}
function validateChildKeys(node) {
isValidElement(node) ? node._store && (node._store.validated = 1) : "object" === typeof node && null !== node && node.$$typeof === REACT_LAZY_TYPE && ("fulfilled" === node._payload.status ? isValidElement(node._payload.value) && node._payload.value._store && (node._payload.value._store.validated = 1) : node._store && (node._store.validated = 1));
}
function isValidElement(object) {
return "object" === typeof object && null !== object && object.$$typeof === REACT_ELEMENT_TYPE;
}
function escape(key) {
var escaperLookup = {
"=": "=0",
":": "=2"
};
return "$" + key.replace(/[=:]/g, function(match) {
return escaperLookup[match];
});
}
function getElementKey(element, index) {
return "object" === typeof element && null !== element && null != element.key ? (checkKeyStringCoercion(element.key), escape("" + element.key)) : index.toString(36);
}
function resolveThenable(thenable) {
switch (thenable.status) {
case "fulfilled": return thenable.value;
case "rejected": throw thenable.reason;
default: switch ("string" === typeof thenable.status ? thenable.then(noop, noop) : (thenable.status = "pending", thenable.then(function(fulfilledValue) {
"pending" === thenable.status && (thenable.status = "fulfilled", thenable.value = fulfilledValue);
}, function(error) {
"pending" === thenable.status && (thenable.status = "rejected", thenable.reason = error);
})), thenable.status) {
case "fulfilled": return thenable.value;
case "rejected": throw thenable.reason;
}
}
throw thenable;
}
function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) {
var type = typeof children;
if ("undefined" === type || "boolean" === type) children = null;
var invokeCallback = !1;
if (null === children) invokeCallback = !0;
else switch (type) {
case "bigint":
case "string":
case "number":
invokeCallback = !0;
break;
case "object": switch (children.$$typeof) {
case REACT_ELEMENT_TYPE:
case REACT_PORTAL_TYPE:
invokeCallback = !0;
break;
case REACT_LAZY_TYPE: return invokeCallback = children._init, mapIntoArray(invokeCallback(children._payload), array, escapedPrefix, nameSoFar, callback);
}
}
if (invokeCallback) {
invokeCallback = children;
callback = callback(invokeCallback);
var childKey = "" === nameSoFar ? "." + getElementKey(invokeCallback, 0) : nameSoFar;
isArrayImpl(callback) ? (escapedPrefix = "", null != childKey && (escapedPrefix = childKey.replace(userProvidedKeyEscapeRegex, "$&/") + "/"), mapIntoArray(callback, array, escapedPrefix, "", function(c) {
return c;
})) : null != callback && (isValidElement(callback) && (null != callback.key && (invokeCallback && invokeCallback.key === callback.key || checkKeyStringCoercion(callback.key)), escapedPrefix = cloneAndReplaceKey(callback, escapedPrefix + (null == callback.key || invokeCallback && invokeCallback.key === callback.key ? "" : ("" + callback.key).replace(userProvidedKeyEscapeRegex, "$&/") + "/") + childKey), "" !== nameSoFar && null != invokeCallback && isValidElement(invokeCallback) && null == invokeCallback.key && invokeCallback._store && !invokeCallback._store.validated && (escapedPrefix._store.validated = 2), callback = escapedPrefix), array.push(callback));
return 1;
}
invokeCallback = 0;
childKey = "" === nameSoFar ? "." : nameSoFar + ":";
if (isArrayImpl(children)) for (var i = 0; i < children.length; i++) nameSoFar = children[i], type = childKey + getElementKey(nameSoFar, i), invokeCallback += mapIntoArray(nameSoFar, array, escapedPrefix, type, callback);
else if (i = getIteratorFn(children), "function" === typeof i) for (i === children.entries && (didWarnAboutMaps || console.warn("Using Maps as children is not supported. Use an array of keyed ReactElements instead."), didWarnAboutMaps = !0), children = i.call(children), i = 0; !(nameSoFar = children.next()).done;) nameSoFar = nameSoFar.value, type = childKey + getElementKey(nameSoFar, i++), invokeCallback += mapIntoArray(nameSoFar, array, escapedPrefix, type, callback);
else if ("object" === type) {
if ("function" === typeof children.then) return mapIntoArray(resolveThenable(children), array, escapedPrefix, nameSoFar, callback);
array = String(children);
throw Error("Objects are not valid as a React child (found: " + ("[object Object]" === array ? "object with keys {" + Object.keys(children).join(", ") + "}" : array) + "). If you meant to render a collection of children, use an array instead.");
}
return invokeCallback;
}
function mapChildren(children, func, context) {
if (null == children) return children;
var result = [], count = 0;
mapIntoArray(children, result, "", "", function(child) {
return func.call(context, child, count++);
});
return result;
}
function lazyInitializer(payload) {
if (-1 === payload._status) {
var resolveDebugValue = null, rejectDebugValue = null, ioInfo = payload._ioInfo;
null != ioInfo && (ioInfo.start = ioInfo.end = performance.now(), ioInfo.value = new Promise(function(resolve, reject) {
resolveDebugValue = resolve;
rejectDebugValue = reject;
}));
ioInfo = payload._result;
var thenable = ioInfo();
thenable.then(function(moduleObject) {
if (0 === payload._status || -1 === payload._status) {
payload._status = 1;
payload._result = moduleObject;
var _ioInfo = payload._ioInfo;
if (null != _ioInfo) {
_ioInfo.end = performance.now();
var debugValue = null == moduleObject ? void 0 : moduleObject.default;
resolveDebugValue(debugValue);
_ioInfo.value.status = "fulfilled";
_ioInfo.value.value = debugValue;
}
void 0 === thenable.status && (thenable.status = "fulfilled", thenable.value = moduleObject);
}
}, function(error) {
if (0 === payload._status || -1 === payload._status) {
payload._status = 2;
payload._result = error;
var _ioInfo2 = payload._ioInfo;
null != _ioInfo2 && (_ioInfo2.end = performance.now(), _ioInfo2.value.then(noop, noop), rejectDebugValue(error), _ioInfo2.value.status = "rejected", _ioInfo2.value.reason = error);
void 0 === thenable.status && (thenable.status = "rejected", thenable.reason = error);
}
});
ioInfo = payload._ioInfo;
if (null != ioInfo) {
var displayName = thenable.displayName;
"string" === typeof displayName && (ioInfo.name = displayName);
}
-1 === payload._status && (payload._status = 0, payload._result = thenable);
}
if (1 === payload._status) return ioInfo = payload._result, void 0 === ioInfo && console.error("lazy: Expected the result of a dynamic import() call. Instead received: %s\n\nYour code should look like: \n const MyComponent = lazy(() => import('./MyComponent'))\n\nDid you accidentally put curly braces around the import?", ioInfo), "default" in ioInfo || console.error("lazy: Expected the result of a dynamic import() call. Instead received: %s\n\nYour code should look like: \n const MyComponent = lazy(() => import('./MyComponent'))", ioInfo), ioInfo.default;
throw payload._result;
}
function resolveDispatcher() {
var dispatcher = ReactSharedInternals.H;
null === dispatcher && console.error("Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.");
return dispatcher;
}
function releaseAsyncTransition() {
ReactSharedInternals.asyncTransitions--;
}
function startTransition(scope) {
var prevTransition = ReactSharedInternals.T, currentTransition = {};
currentTransition.types = null !== prevTransition ? prevTransition.types : null;
currentTransition._updatedFibers = /* @__PURE__ */ new Set();
ReactSharedInternals.T = currentTransition;
try {
var returnValue = scope(), onStartTransitionFinish = ReactSharedInternals.S;
null !== onStartTransitionFinish && onStartTransitionFinish(currentTransition, returnValue);
"object" === typeof returnValue && null !== returnValue && "function" === typeof returnValue.then && (ReactSharedInternals.asyncTransitions++, returnValue.then(releaseAsyncTransition, releaseAsyncTransition), returnValue.then(noop, reportGlobalError));
} catch (error) {
reportGlobalError(error);
} finally {
null === prevTransition && currentTransition._updatedFibers && (scope = currentTransition._updatedFibers.size, currentTransition._updatedFibers.clear(), 10 < scope && console.warn("Detected a large number of updates inside startTransition. If this is due to a subscription please re-write it to use React provided hooks. Otherwise concurrent mode guarantees are off the table.")), null !== prevTransition && null !== currentTransition.types && (null !== prevTransition.types && prevTransition.types !== currentTransition.types && console.error("We expected inner Transitions to have transferred the outer types set and that you cannot add to the outer Transition while inside the inner.This is a bug in React."), prevTransition.types = currentTransition.types), ReactSharedInternals.T = prevTransition;
}
}
function addTransitionType(type) {
var transition = ReactSharedInternals.T;
if (null !== transition) {
var transitionTypes = transition.types;
null === transitionTypes ? transition.types = [type] : -1 === transitionTypes.indexOf(type) && transitionTypes.push(type);
} else 0 === ReactSharedInternals.asyncTransitions && console.error("addTransitionType can only be called inside a `startTransition()` callback. It must be associated with a specific Transition."), startTransition(addTransitionType.bind(null, type));
}
function enqueueTask(task) {
if (null === enqueueTaskImpl) try {
var requireString = ("require" + Math.random()).slice(0, 7);
enqueueTaskImpl = (module && module[requireString]).call(module, "timers").setImmediate;
} catch (_err) {
enqueueTaskImpl = function(callback) {
!1 === didWarnAboutMessageChannel && (didWarnAboutMessageChannel = !0, "undefined" === typeof MessageChannel && console.error("This browser does not have a MessageChannel implementation, so enqueuing tasks via await act(async () => ...) will fail. Please file an issue at https://github.com/facebook/react/issues if you encounter this warning."));
var channel = new MessageChannel();
channel.port1.onmessage = callback;
channel.port2.postMessage(void 0);
};
}
return enqueueTaskImpl(task);
}
function aggregateErrors(errors) {
return 1 < errors.length && "function" === typeof AggregateError ? new AggregateError(errors) : errors[0];
}
function popActScope(prevActQueue, prevActScopeDepth) {
prevActScopeDepth !== actScopeDepth - 1 && console.error("You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one. ");
actScopeDepth = prevActScopeDepth;
}
function recursivelyFlushAsyncActWork(returnValue, resolve, reject) {
var queue = ReactSharedInternals.actQueue;
if (null !== queue) if (0 !== queue.length) try {
flushActQueue(queue);
enqueueTask(function() {
return recursivelyFlushAsyncActWork(returnValue, resolve, reject);
});
return;
} catch (error) {
ReactSharedInternals.thrownErrors.push(error);
}
else ReactSharedInternals.actQueue = null;
0 < ReactSharedInternals.thrownErrors.length ? (queue = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, reject(queue)) : resolve(returnValue);
}
function flushActQueue(queue) {
if (!isFlushing) {
isFlushing = !0;
var i = 0;
try {
for (; i < queue.length; i++) {
var callback = queue[i];
do {
ReactSharedInternals.didUsePromise = !1;
var continuation = callback(!1);
if (null !== continuation) {
if (ReactSharedInternals.didUsePromise) {
queue[i] = callback;
queue.splice(0, i);
return;
}
callback = continuation;
} else break;
} while (1);
}
queue.length = 0;
} catch (error) {
queue.splice(0, i + 1), ReactSharedInternals.thrownErrors.push(error);
} finally {
isFlushing = !1;
}
}
}
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());
var REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = Symbol.for("react.profiler"), REACT_CONSUMER_TYPE = Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = Symbol.for("react.memo"), REACT_LAZY_TYPE = Symbol.for("react.lazy"), REACT_ACTIVITY_TYPE = Symbol.for("react.activity"), REACT_VIEW_TRANSITION_TYPE = Symbol.for("react.view_transition"), MAYBE_ITERATOR_SYMBOL = Symbol.iterator, didWarnStateUpdateForUnmountedComponent = {}, ReactNoopUpdateQueue = {
isMounted: function() {
return !1;
},
enqueueForceUpdate: function(publicInstance) {
warnNoop(publicInstance, "forceUpdate");
},
enqueueReplaceState: function(publicInstance) {
warnNoop(publicInstance, "replaceState");
},
enqueueSetState: function(publicInstance) {
warnNoop(publicInstance, "setState");
}
}, assign = Object.assign, emptyObject = {};
Object.freeze(emptyObject);
Component.prototype.isReactComponent = {};
Component.prototype.setState = function(partialState, callback) {
if ("object" !== typeof partialState && "function" !== typeof partialState && null != partialState) throw Error("takes an object of state variables to update or a function which returns an object of state variables.");
this.updater.enqueueSetState(this, partialState, callback, "setState");
};
Component.prototype.forceUpdate = function(callback) {
this.updater.enqueueForceUpdate(this, callback, "forceUpdate");
};
var deprecatedAPIs = {
isMounted: ["isMounted", "Instead, make sure to clean up subscriptions and pending requests in componentWillUnmount to prevent memory leaks."],
replaceState: ["replaceState", "Refactor your code to use setState instead (see https://github.com/facebook/react/issues/3236)."]
};
for (fnName in deprecatedAPIs) deprecatedAPIs.hasOwnProperty(fnName) && defineDeprecationWarning(fnName, deprecatedAPIs[fnName]);
ComponentDummy.prototype = Component.prototype;
deprecatedAPIs = PureComponent.prototype = new ComponentDummy();
deprecatedAPIs.constructor = PureComponent;
assign(deprecatedAPIs, Component.prototype);
deprecatedAPIs.isPureReactComponent = !0;
var isArrayImpl = Array.isArray, REACT_CLIENT_REFERENCE = Symbol.for("react.client.reference"), ReactSharedInternals = {
H: null,
A: null,
T: null,
S: null,
actQueue: null,
asyncTransitions: 0,
isBatchingLegacy: !1,
didScheduleLegacyUpdate: !1,
didUsePromise: !1,
thrownErrors: [],
getCurrentStack: null,
recentlyCreatedOwnerStacks: 0
}, hasOwnProperty = Object.prototype.hasOwnProperty, createTask = console.createTask ? console.createTask : function() {
return null;
};
deprecatedAPIs = { react_stack_bottom_frame: function(callStackForError) {
return callStackForError();
} };
var specialPropKeyWarningShown, didWarnAboutOldJSXRuntime;
var didWarnAboutElementRef = {};
var unknownOwnerDebugStack = deprecatedAPIs.react_stack_bottom_frame.bind(deprecatedAPIs, UnknownOwner)();
var unknownOwnerDebugTask = createTask(getTaskName(UnknownOwner));
var didWarnAboutMaps = !1, userProvidedKeyEscapeRegex = /\/+/g, reportGlobalError = "function" === typeof reportError ? reportError : function(error) {
if ("object" === typeof window && "function" === typeof window.ErrorEvent) {
var event = new window.ErrorEvent("error", {
bubbles: !0,
cancelable: !0,
message: "object" === typeof error && null !== error && "string" === typeof error.message ? String(error.message) : String(error),
error
});
if (!window.dispatchEvent(event)) return;
} else if ("object" === typeof process && "function" === typeof process.emit) {
process.emit("uncaughtException", error);
return;
}
console.error(error);
}, didWarnAboutMessageChannel = !1, enqueueTaskImpl = null, actScopeDepth = 0, didWarnNoAwaitAct = !1, isFlushing = !1, queueSeveralMicrotasks = "function" === typeof queueMicrotask ? function(callback) {
queueMicrotask(function() {
return queueMicrotask(callback);
});
} : enqueueTask;
deprecatedAPIs = Object.freeze({
__proto__: null,
c: function(size) {
return resolveDispatcher().useMemoCache(size);
}
});
var fnName = {
map: mapChildren,
forEach: function(children, forEachFunc, forEachContext) {
mapChildren(children, function() {
forEachFunc.apply(this, arguments);
}, forEachContext);
},
count: function(children) {
var n = 0;
mapChildren(children, function() {
n++;
});
return n;
},
toArray: function(children) {
return mapChildren(children, function(child) {
return child;
}) || [];
},
only: function(children) {
if (!isValidElement(children)) throw Error("React.Children.only expected to receive a single React element child.");
return children;
}
};
exports.Activity = REACT_ACTIVITY_TYPE;
exports.Children = fnName;
exports.Component = Component;
exports.Fragment = REACT_FRAGMENT_TYPE;
exports.Profiler = REACT_PROFILER_TYPE;
exports.PureComponent = PureComponent;
exports.StrictMode = REACT_STRICT_MODE_TYPE;
exports.Suspense = REACT_SUSPENSE_TYPE;
exports.ViewTransition = REACT_VIEW_TRANSITION_TYPE;
exports.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = ReactSharedInternals;
exports.__COMPILER_RUNTIME = deprecatedAPIs;
exports.act = function(callback) {
var prevActQueue = ReactSharedInternals.actQueue, prevActScopeDepth = actScopeDepth;
actScopeDepth++;
var queue = ReactSharedInternals.actQueue = null !== prevActQueue ? prevActQueue : [], didAwaitActCall = !1;
try {
var result = callback();
} catch (error) {
ReactSharedInternals.thrownErrors.push(error);
}
if (0 < ReactSharedInternals.thrownErrors.length) throw popActScope(prevActQueue, prevActScopeDepth), callback = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, callback;
if (null !== result && "object" === typeof result && "function" === typeof result.then) {
var thenable = result;
queueSeveralMicrotasks(function() {
didAwaitActCall || didWarnNoAwaitAct || (didWarnNoAwaitAct = !0, console.error("You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);"));
});
return { then: function(resolve, reject) {
didAwaitActCall = !0;
thenable.then(function(returnValue) {
popActScope(prevActQueue, prevActScopeDepth);
if (0 === prevActScopeDepth) {
try {
flushActQueue(queue), enqueueTask(function() {
return recursivelyFlushAsyncActWork(returnValue, resolve, reject);
});
} catch (error$0) {
ReactSharedInternals.thrownErrors.push(error$0);
}
if (0 < ReactSharedInternals.thrownErrors.length) {
var _thrownError = aggregateErrors(ReactSharedInternals.thrownErrors);
ReactSharedInternals.thrownErrors.length = 0;
reject(_thrownError);
}
} else resolve(returnValue);
}, function(error) {
popActScope(prevActQueue, prevActScopeDepth);
0 < ReactSharedInternals.thrownErrors.length ? (error = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, reject(error)) : reject(error);
});
} };
}
var returnValue$jscomp$0 = result;
popActScope(prevActQueue, prevActScopeDepth);
0 === prevActScopeDepth && (flushActQueue(queue), 0 !== queue.length && queueSeveralMicrotasks(function() {
didAwaitActCall || didWarnNoAwaitAct || (didWarnNoAwaitAct = !0, console.error("A component suspended inside an `act` scope, but the `act` call was not awaited. When testing React components that depend on asynchronous data, you must await the result:\n\nawait act(() => ...)"));
}), ReactSharedInternals.actQueue = null);
if (0 < ReactSharedInternals.thrownErrors.length) throw callback = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, callback;
return { then: function(resolve, reject) {
didAwaitActCall = !0;
0 === prevActScopeDepth ? (ReactSharedInternals.actQueue = queue, enqueueTask(function() {
return recursivelyFlushAsyncActWork(returnValue$jscomp$0, resolve, reject);
})) : resolve(returnValue$jscomp$0);
} };
};
exports.addTransitionType = addTransitionType;
exports.cache = function(fn) {
return function() {
return fn.apply(null, arguments);
};
};
exports.cacheSignal = function() {
return null;
};
exports.captureOwnerStack = function() {
var getCurrentStack = ReactSharedInternals.getCurrentStack;
return null === getCurrentStack ? null : getCurrentStack();
};
exports.cloneElement = function(element, config, children) {
if (null === element || void 0 === element) throw Error("The argument must be a React element, but you passed " + element + ".");
var props = assign({}, element.props), key = element.key, owner = element._owner;
if (null != config) {
var JSCompiler_inline_result;
a: {
if (hasOwnProperty.call(config, "ref") && (JSCompiler_inline_result = Object.getOwnPropertyDescriptor(config, "ref").get) && JSCompiler_inline_result.isReactWarning) {
JSCompiler_inline_result = !1;
break a;
}
JSCompiler_inline_result = void 0 !== config.ref;
}
JSCompiler_inline_result && (owner = getOwner());
hasValidKey(config) && (checkKeyStringCoercion(config.key), key = "" + config.key);
for (propName in config) !hasOwnProperty.call(config, propName) || "key" === propName || "__self" === propName || "__source" === propName || "ref" === propName && void 0 === config.ref || (props[propName] = config[propName]);
}
var propName = arguments.length - 2;
if (1 === propName) props.children = children;
else if (1 < propName) {
JSCompiler_inline_result = Array(propName);
for (var i = 0; i < propName; i++) JSCompiler_inline_result[i] = arguments[i + 2];
props.children = JSCompiler_inline_result;
}
props = ReactElement(element.type, key, props, owner, element._debugStack, element._debugTask);
for (key = 2; key < arguments.length; key++) validateChildKeys(arguments[key]);
return props;
};
exports.createContext = function(defaultValue) {
defaultValue = {
$$typeof: REACT_CONTEXT_TYPE,
_currentValue: defaultValue,
_currentValue2: defaultValue,
_threadCount: 0,
Provider: null,
Consumer: null
};
defaultValue.Provider = defaultValue;
defaultValue.Consumer = {
$$typeof: REACT_CONSUMER_TYPE,
_context: defaultValue
};
defaultValue._currentRenderer = null;
defaultValue._currentRenderer2 = null;
return defaultValue;
};
exports.createElement = function(type, config, children) {
for (var i = 2; i < arguments.length; i++) validateChildKeys(arguments[i]);
var propName;
i = {};
var key = null;
if (null != config) for (propName in didWarnAboutOldJSXRuntime || !("__self" in config) || "key" in config || (didWarnAboutOldJSXRuntime = !0, console.warn("Your app (or one of its dependencies) is using an outdated JSX transform. Update to the modern JSX transform for faster performance: https://react.dev/link/new-jsx-transform")), hasValidKey(config) && (checkKeyStringCoercion(config.key), key = "" + config.key), config) hasOwnProperty.call(config, propName) && "key" !== propName && "__self" !== propName && "__source" !== propName && (i[propName] = config[propName]);
var childrenLength = arguments.length - 2;
if (1 === childrenLength) i.children = children;
else if (1 < childrenLength) {
for (var childArray = Array(childrenLength), _i = 0; _i < childrenLength; _i++) childArray[_i] = arguments[_i + 2];
Object.freeze && Object.freeze(childArray);
i.children = childArray;
}
if (type && type.defaultProps) for (propName in childrenLength = type.defaultProps, childrenLength) void 0 === i[propName] && (i[propName] = childrenLength[propName]);
key && defineKeyPropWarningGetter(i, "function" === typeof type ? type.displayName || type.name || "Unknown" : type);
(propName = 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++) ? (childArray = Error.stackTraceLimit, Error.stackTraceLimit = 10, childrenLength = Error("react-stack-top-frame"), Error.stackTraceLimit = childArray) : childrenLength = unknownOwnerDebugStack;
return ReactElement(type, key, i, getOwner(), childrenLength, propName ? createTask(getTaskName(type)) : unknownOwnerDebugTask);
};
exports.createRef = function() {
var refObject = { current: null };
Object.seal(refObject);
return refObject;
};
exports.forwardRef = function(render) {
null != render && render.$$typeof === REACT_MEMO_TYPE ? console.error("forwardRef requires a render function but received a `memo` component. Instead of forwardRef(memo(...)), use memo(forwardRef(...)).") : "function" !== typeof render ? console.error("forwardRef requires a render function but was given %s.", null === render ? "null" : typeof render) : 0 !== render.length && 2 !== render.length && console.error("forwardRef render functions accept exactly two parameters: props and ref. %s", 1 === render.length ? "Did you forget to use the ref parameter?" : "Any additional parameter will be undefined.");
null != render && null != render.defaultProps && console.error("forwardRef render functions do not support defaultProps. Did you accidentally pass a React component?");
var elementType = {
$$typeof: REACT_FORWARD_REF_TYPE,
render
}, ownName;
Object.defineProperty(elementType, "displayName", {
enumerable: !1,
configurable: !0,
get: function() {
return ownName;
},
set: function(name) {
ownName = name;
render.name || render.displayName || (Object.defineProperty(render, "name", { value: name }), render.displayName = name);
}
});
return elementType;
};
exports.isValidElement = isValidElement;
exports.lazy = function(ctor) {
ctor = {
_status: -1,
_result: ctor
};
var lazyType = {
$$typeof: REACT_LAZY_TYPE,
_payload: ctor,
_init: lazyInitializer
}, ioInfo = {
name: "lazy",
start: -1,
end: -1,
value: null,
owner: null,
debugStack: Error("react-stack-top-frame"),
debugTask: console.createTask ? console.createTask("lazy()") : null
};
ctor._ioInfo = ioInfo;
lazyType._debugInfo = [{ awaited: ioInfo }];
return lazyType;
};
exports.memo = function(type, compare) {
type ?? console.error("memo: The first argument must be a component. Instead received: %s", null === type ? "null" : typeof type);
compare = {
$$typeof: REACT_MEMO_TYPE,
type,
compare: void 0 === compare ? null : compare
};
var ownName;
Object.defineProperty(compare, "displayName", {
enumerable: !1,
configurable: !0,
get: function() {
return ownName;
},
set: function(name) {
ownName = name;
type.name || type.displayName || (Object.defineProperty(type, "name", { value: name }), type.displayName = name);
}
});
return compare;
};
exports.startTransition = startTransition;
exports.unstable_useCacheRefresh = function() {
return resolveDispatcher().useCacheRefresh();
};
exports.use = function(usable) {
return resolveDispatcher().use(usable);
};
exports.useActionState = function(action, initialState, permalink) {
return resolveDispatcher().useActionState(action, initialState, permalink);
};
exports.useCallback = function(callback, deps) {
return resolveDispatcher().useCallback(callback, deps);
};
exports.useContext = function(Context) {
var dispatcher = resolveDispatcher();
Context.$$typeof === REACT_CONSUMER_TYPE && console.error("Calling useContext(Context.Consumer) is not supported and will cause bugs. Did you mean to call useContext(Context) instead?");
return dispatcher.useContext(Context);
};
exports.useDebugValue = function(value, formatterFn) {
return resolveDispatcher().useDebugValue(value, formatterFn);
};
exports.useDeferredValue = function(value, initialValue) {
return resolveDispatcher().useDeferredValue(value, initialValue);
};
exports.useEffect = function(create, deps) {
create ?? console.warn("React Hook useEffect requires an effect callback. Did you forget to pass a callback to the hook?");
return resolveDispatcher().useEffect(create, deps);
};
exports.useEffectEvent = function(callback) {
return resolveDispatcher().useEffectEvent(callback);
};
exports.useId = function() {
return resolveDispatcher().useId();
};
exports.useImperativeHandle = function(ref, create, deps) {
return resolveDispatcher().useImperativeHandle(ref, create, deps);
};
exports.useInsertionEffect = function(create, deps) {
create ?? console.warn("React Hook useInsertionEffect requires an effect callback. Did you forget to pass a callback to the hook?");
return resolveDispatcher().useInsertionEffect(create, deps);
};
exports.useLayoutEffect = function(create, deps) {
create ?? console.warn("React Hook useLayoutEffect requires an effect callback. Did you forget to pass a callback to the hook?");
return resolveDispatcher().useLayoutEffect(create, deps);
};
exports.useMemo = function(create, deps) {
return resolveDispatcher().useMemo(create, deps);
};
exports.useOptimistic = function(passthrough, reducer) {
return resolveDispatcher().useOptimistic(passthrough, reducer);
};
exports.useReducer = function(reducer, initialArg, init) {
return resolveDispatcher().useReducer(reducer, initialArg, init);
};
exports.useRef = function(initialValue) {
return resolveDispatcher().useRef(initialValue);
};
exports.useState = function(initialState) {
return resolveDispatcher().useState(initialState);
};
exports.useSyncExternalStore = function(subscribe, getSnapshot, getServerSnapshot) {
return resolveDispatcher().useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
};
exports.useTransition = function() {
return resolveDispatcher().useTransition();
};
exports.version = "19.3.0";
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error());
})();
}));
//#endregion
//#region node_modules/react/index.js
var require_react = /* @__PURE__ */ __commonJSMin(((exports, module) => {
module.exports = require_react_development();
}));
//#endregion
export default require_react();
export { require_react as t };
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"react.js","names":[],"sources":["../../react/cjs/react.development.js","../../react/index.js"],"sourcesContent":["/**\n * @license React\n * react.development.js\n *\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n\"use strict\";\n\"production\" !== process.env.NODE_ENV &&\n (function () {\n function defineDeprecationWarning(methodName, info) {\n Object.defineProperty(Component.prototype, methodName, {\n get: function () {\n console.warn(\n \"%s(...) is deprecated in plain JavaScript React classes. %s\",\n info[0],\n info[1]\n );\n }\n });\n }\n function getIteratorFn(maybeIterable) {\n if (null === maybeIterable || \"object\" !== typeof maybeIterable)\n return null;\n maybeIterable =\n (MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL]) ||\n maybeIterable[\"@@iterator\"];\n return \"function\" === typeof maybeIterable ? maybeIterable : null;\n }\n function warnNoop(publicInstance, callerName) {\n publicInstance =\n ((publicInstance = publicInstance.constructor) &&\n (publicInstance.displayName || publicInstance.name)) ||\n \"ReactClass\";\n var warningKey = publicInstance + \".\" + callerName;\n didWarnStateUpdateForUnmountedComponent[warningKey] ||\n (console.error(\n \"Can't call %s on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to `this.state` directly or define a `state = {};` class property with the desired state in the %s component.\",\n callerName,\n publicInstance\n ),\n (didWarnStateUpdateForUnmountedComponent[warningKey] = !0));\n }\n function Component(props, context, updater) {\n this.props = props;\n this.context = context;\n this.refs = emptyObject;\n this.updater = updater || ReactNoopUpdateQueue;\n }\n function ComponentDummy() {}\n function PureComponent(props, context, updater) {\n this.props = props;\n this.context = context;\n this.refs = emptyObject;\n this.updater = updater || ReactNoopUpdateQueue;\n }\n function noop() {}\n function testStringCoercion(value) {\n return \"\" + value;\n }\n function checkKeyStringCoercion(value) {\n try {\n testStringCoercion(value);\n var JSCompiler_inline_result = !1;\n } catch (e) {\n JSCompiler_inline_result = !0;\n }\n if (JSCompiler_inline_result) {\n JSCompiler_inline_result = console;\n var JSCompiler_temp_const = JSCompiler_inline_result.error;\n var JSCompiler_inline_result$jscomp$0 =\n (\"function\" === typeof Symbol &&\n Symbol.toStringTag &&\n value[Symbol.toStringTag]) ||\n value.constructor.name ||\n \"Object\";\n JSCompiler_temp_const.call(\n JSCompiler_inline_result,\n \"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.\",\n JSCompiler_inline_result$jscomp$0\n );\n return testStringCoercion(value);\n }\n }\n function getComponentNameFromType(type) {\n if (null == type) return null;\n if (\"function\" === typeof type)\n return type.$$typeof === REACT_CLIENT_REFERENCE\n ? null\n : type.displayName || type.name || null;\n if (\"string\" === typeof type) return type;\n switch (type) {\n case REACT_FRAGMENT_TYPE:\n return \"Fragment\";\n case REACT_PROFILER_TYPE:\n return \"Profiler\";\n case REACT_STRICT_MODE_TYPE:\n return \"StrictMode\";\n case REACT_SUSPENSE_TYPE:\n return \"Suspense\";\n case REACT_SUSPENSE_LIST_TYPE:\n return \"SuspenseList\";\n case REACT_ACTIVITY_TYPE:\n return \"Activity\";\n case REACT_VIEW_TRANSITION_TYPE:\n return \"ViewTransition\";\n }\n if (\"object\" === typeof type)\n switch (\n (\"number\" === typeof type.tag &&\n console.error(\n \"Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue.\"\n ),\n type.$$typeof)\n ) {\n case REACT_PORTAL_TYPE:\n return \"Portal\";\n case REACT_CONTEXT_TYPE:\n return type.displayName || \"Context\";\n case REACT_CONSUMER_TYPE:\n return (type._context.displayName || \"Context\") + \".Consumer\";\n case REACT_FORWARD_REF_TYPE:\n var innerType = type.render;\n type = type.displayName;\n type ||\n ((type = innerType.displayNaLine truncated
-210
View File
@@ -1,210 +0,0 @@
import { t as __commonJSMin } from "./rolldown-runtime-DC62tzP2.js";
import { t as require_react } from "./react.js";
//#region node_modules/react/cjs/react-jsx-dev-runtime.development.js
/**
* @license React
* react-jsx-dev-runtime.development.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
var require_react_jsx_dev_runtime_development = /* @__PURE__ */ __commonJSMin(((exports) => {
(function() {
function getComponentNameFromType(type) {
if (null == type) return null;
if ("function" === typeof type) return type.$$typeof === REACT_CLIENT_REFERENCE ? null : type.displayName || type.name || null;
if ("string" === typeof type) return type;
switch (type) {
case REACT_FRAGMENT_TYPE: return "Fragment";
case REACT_PROFILER_TYPE: return "Profiler";
case REACT_STRICT_MODE_TYPE: return "StrictMode";
case REACT_SUSPENSE_TYPE: return "Suspense";
case REACT_SUSPENSE_LIST_TYPE: return "SuspenseList";
case REACT_ACTIVITY_TYPE: return "Activity";
case REACT_VIEW_TRANSITION_TYPE: return "ViewTransition";
}
if ("object" === typeof type) switch ("number" === typeof type.tag && console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."), type.$$typeof) {
case REACT_PORTAL_TYPE: return "Portal";
case REACT_CONTEXT_TYPE: return type.displayName || "Context";
case REACT_CONSUMER_TYPE: return (type._context.displayName || "Context") + ".Consumer";
case REACT_FORWARD_REF_TYPE:
var innerType = type.render;
type = type.displayName;
type || (type = innerType.displayName || innerType.name || "", type = "" !== type ? "ForwardRef(" + type + ")" : "ForwardRef");
return type;
case REACT_MEMO_TYPE: return innerType = type.displayName || null, null !== innerType ? innerType : getComponentNameFromType(type.type) || "Memo";
case REACT_LAZY_TYPE:
innerType = type._payload;
type = type._init;
try {
return getComponentNameFromType(type(innerType));
} catch (x) {}
}
return null;
}
function testStringCoercion(value) {
return "" + value;
}
function checkKeyStringCoercion(value) {
try {
testStringCoercion(value);
var JSCompiler_inline_result = !1;
} catch (e) {
JSCompiler_inline_result = !0;
}
if (JSCompiler_inline_result) {
JSCompiler_inline_result = console;
var JSCompiler_temp_const = JSCompiler_inline_result.error;
var JSCompiler_inline_result$jscomp$0 = "function" === typeof Symbol && Symbol.toStringTag && value[Symbol.toStringTag] || value.constructor.name || "Object";
JSCompiler_temp_const.call(JSCompiler_inline_result, "The provided key is an unsupported type %s. This value must be coerced to a string before using it here.", JSCompiler_inline_result$jscomp$0);
return testStringCoercion(value);
}
}
function getTaskName(type) {
if (type === REACT_FRAGMENT_TYPE) return "<>";
if ("object" === typeof type && null !== type && type.$$typeof === REACT_LAZY_TYPE) return "<...>";
try {
var name = getComponentNameFromType(type);
return name ? "<" + name + ">" : "<...>";
} catch (x) {
return "<...>";
}
}
function getOwner() {
var dispatcher = ReactSharedInternals.A;
return null === dispatcher ? null : dispatcher.getOwner();
}
function UnknownOwner() {
return Error("react-stack-top-frame");
}
function hasValidKey(config) {
if (hasOwnProperty.call(config, "key")) {
var getter = Object.getOwnPropertyDescriptor(config, "key").get;
if (getter && getter.isReactWarning) return !1;
}
return void 0 !== config.key;
}
function defineKeyPropWarningGetter(props, displayName) {
function warnAboutAccessingKey() {
specialPropKeyWarningShown || (specialPropKeyWarningShown = !0, console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)", displayName));
}
warnAboutAccessingKey.isReactWarning = !0;
Object.defineProperty(props, "key", {
get: warnAboutAccessingKey,
configurable: !0
});
}
function elementRefGetterWithDeprecationWarning() {
var componentName = getComponentNameFromType(this.type);
didWarnAboutElementRef[componentName] || (didWarnAboutElementRef[componentName] = !0, console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release."));
componentName = this.props.ref;
return void 0 !== componentName ? componentName : null;
}
function ReactElement(type, key, props, owner, debugStack, debugTask) {
var refProp = props.ref;
type = {
$$typeof: REACT_ELEMENT_TYPE,
type,
key,
props,
_owner: owner
};
null !== (void 0 !== refProp ? refProp : null) ? Object.defineProperty(type, "ref", {
enumerable: !1,
get: elementRefGetterWithDeprecationWarning
}) : Object.defineProperty(type, "ref", {
enumerable: !1,
value: null
});
type._store = {};
Object.defineProperty(type._store, "validated", {
configurable: !1,
enumerable: !1,
writable: !0,
value: 0
});
Object.defineProperty(type, "_debugInfo", {
configurable: !1,
enumerable: !1,
writable: !0,
value: null
});
Object.defineProperty(type, "_debugStack", {
configurable: !1,
enumerable: !1,
writable: !0,
value: debugStack
});
Object.defineProperty(type, "_debugTask", {
configurable: !1,
enumerable: !1,
writable: !0,
value: debugTask
});
Object.freeze && (Object.freeze(type.props), Object.freeze(type));
return type;
}
function jsxDEVImpl(type, config, maybeKey, isStaticChildren, debugStack, debugTask) {
var children = config.children;
if (void 0 !== children) if (isStaticChildren) if (isArrayImpl(children)) {
for (isStaticChildren = 0; isStaticChildren < children.length; isStaticChildren++) validateChildKeys(children[isStaticChildren]);
Object.freeze && Object.freeze(children);
} else console.error("React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.");
else validateChildKeys(children);
if (hasOwnProperty.call(config, "key")) {
children = getComponentNameFromType(type);
var keys = Object.keys(config).filter(function(k) {
return "key" !== k;
});
isStaticChildren = 0 < keys.length ? "{key: someKey, " + keys.join(": ..., ") + ": ...}" : "{key: someKey}";
didWarnAboutKeySpread[children + isStaticChildren] || (keys = 0 < keys.length ? "{" + keys.join(": ..., ") + ": ...}" : "{}", console.error("A props object containing a \"key\" prop is being spread into JSX:\n let props = %s;\n <%s {...props} />\nReact keys must be passed directly to JSX without using spread:\n let props = %s;\n <%s key={someKey} {...props} />", isStaticChildren, children, keys, children), didWarnAboutKeySpread[children + isStaticChildren] = !0);
}
children = null;
void 0 !== maybeKey && (checkKeyStringCoercion(maybeKey), children = "" + maybeKey);
hasValidKey(config) && (checkKeyStringCoercion(config.key), children = "" + config.key);
if ("key" in config) {
maybeKey = {};
for (var propName in config) "key" !== propName && (maybeKey[propName] = config[propName]);
} else maybeKey = config;
children && defineKeyPropWarningGetter(maybeKey, "function" === typeof type ? type.displayName || type.name || "Unknown" : type);
return ReactElement(type, children, maybeKey, getOwner(), debugStack, debugTask);
}
function validateChildKeys(node) {
isValidElement(node) ? node._store && (node._store.validated = 1) : "object" === typeof node && null !== node && node.$$typeof === REACT_LAZY_TYPE && ("fulfilled" === node._payload.status ? isValidElement(node._payload.value) && node._payload.value._store && (node._payload.value._store.validated = 1) : node._store && (node._store.validated = 1));
}
function isValidElement(object) {
return "object" === typeof object && null !== object && object.$$typeof === REACT_ELEMENT_TYPE;
}
var React = require_react(), REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = Symbol.for("react.profiler"), REACT_CONSUMER_TYPE = Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = Symbol.for("react.memo"), REACT_LAZY_TYPE = Symbol.for("react.lazy"), REACT_ACTIVITY_TYPE = Symbol.for("react.activity"), REACT_VIEW_TRANSITION_TYPE = Symbol.for("react.view_transition"), REACT_CLIENT_REFERENCE = Symbol.for("react.client.reference"), ReactSharedInternals = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, hasOwnProperty = Object.prototype.hasOwnProperty, isArrayImpl = Array.isArray, createTask = console.createTask ? console.createTask : function() {
return null;
};
React = { react_stack_bottom_frame: function(callStackForError) {
return callStackForError();
} };
var specialPropKeyWarningShown;
var didWarnAboutElementRef = {};
var unknownOwnerDebugStack = React.react_stack_bottom_frame.bind(React, UnknownOwner)();
var unknownOwnerDebugTask = createTask(getTaskName(UnknownOwner));
var didWarnAboutKeySpread = {};
exports.Fragment = REACT_FRAGMENT_TYPE;
exports.jsxDEV = function(type, config, maybeKey, isStaticChildren) {
var trackActualOwner = 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++;
if (trackActualOwner) {
var previousStackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 10;
var debugStackDEV = Error("react-stack-top-frame");
Error.stackTraceLimit = previousStackTraceLimit;
} else debugStackDEV = unknownOwnerDebugStack;
return jsxDEVImpl(type, config, maybeKey, isStaticChildren, debugStackDEV, trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask);
};
})();
}));
//#endregion
//#region node_modules/react/jsx-dev-runtime.js
var require_jsx_dev_runtime = /* @__PURE__ */ __commonJSMin(((exports, module) => {
module.exports = require_react_jsx_dev_runtime_development();
}));
//#endregion
export default require_jsx_dev_runtime();
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"react_jsx-dev-runtime.js","names":[],"sources":["../../react/cjs/react-jsx-dev-runtime.development.js","../../react/jsx-dev-runtime.js"],"sourcesContent":["/**\n * @license React\n * react-jsx-dev-runtime.development.js\n *\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n\"use strict\";\n\"production\" !== process.env.NODE_ENV &&\n (function () {\n function getComponentNameFromType(type) {\n if (null == type) return null;\n if (\"function\" === typeof type)\n return type.$$typeof === REACT_CLIENT_REFERENCE\n ? null\n : type.displayName || type.name || null;\n if (\"string\" === typeof type) return type;\n switch (type) {\n case REACT_FRAGMENT_TYPE:\n return \"Fragment\";\n case REACT_PROFILER_TYPE:\n return \"Profiler\";\n case REACT_STRICT_MODE_TYPE:\n return \"StrictMode\";\n case REACT_SUSPENSE_TYPE:\n return \"Suspense\";\n case REACT_SUSPENSE_LIST_TYPE:\n return \"SuspenseList\";\n case REACT_ACTIVITY_TYPE:\n return \"Activity\";\n case REACT_VIEW_TRANSITION_TYPE:\n return \"ViewTransition\";\n }\n if (\"object\" === typeof type)\n switch (\n (\"number\" === typeof type.tag &&\n console.error(\n \"Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue.\"\n ),\n type.$$typeof)\n ) {\n case REACT_PORTAL_TYPE:\n return \"Portal\";\n case REACT_CONTEXT_TYPE:\n return type.displayName || \"Context\";\n case REACT_CONSUMER_TYPE:\n return (type._context.displayName || \"Context\") + \".Consumer\";\n case REACT_FORWARD_REF_TYPE:\n var innerType = type.render;\n type = type.displayName;\n type ||\n ((type = innerType.displayName || innerType.name || \"\"),\n (type = \"\" !== type ? \"ForwardRef(\" + type + \")\" : \"ForwardRef\"));\n return type;\n case REACT_MEMO_TYPE:\n return (\n (innerType = type.displayName || null),\n null !== innerType\n ? innerType\n : getComponentNameFromType(type.type) || \"Memo\"\n );\n case REACT_LAZY_TYPE:\n innerType = type._payload;\n type = type._init;\n try {\n return getComponentNameFromType(type(innerType));\n } catch (x) {}\n }\n return null;\n }\n function testStringCoercion(value) {\n return \"\" + value;\n }\n function checkKeyStringCoercion(value) {\n try {\n testStringCoercion(value);\n var JSCompiler_inline_result = !1;\n } catch (e) {\n JSCompiler_inline_result = !0;\n }\n if (JSCompiler_inline_result) {\n JSCompiler_inline_result = console;\n var JSCompiler_temp_const = JSCompiler_inline_result.error;\n var JSCompiler_inline_result$jscomp$0 =\n (\"function\" === typeof Symbol &&\n Symbol.toStringTag &&\n value[Symbol.toStringTag]) ||\n value.constructor.name ||\n \"Object\";\n JSCompiler_temp_const.call(\n JSCompiler_inline_result,\n \"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.\",\n JSCompiler_inline_result$jscomp$0\n );\n return testStringCoercion(value);\n }\n }\n function getTaskName(type) {\n if (type === REACT_FRAGMENT_TYPE) return \"<>\";\n if (\n \"object\" === typeof type &&\n null !== type &&\n type.$$typeof === REACT_LAZY_TYPE\n )\n return \"<...>\";\n try {\n var name = getComponentNameFromType(type);\n return name ? \"<\" + name + \">\" : \"<...>\";\n } catch (x) {\n return \"<...>\";\n }\n }\n function getOwner() {\n var dispatcher = ReactSharedInternals.A;\n return null === dispatcher ? null : dispatcher.getOwner();\n }\n function UnknownOwner() {\n return Error(\"react-stack-top-frame\");\n }\n function hasValidKey(config) {\n if (hasOwnProperty.call(config, \"key\")) {\n var getter = Object.getOwnPropertyDescriptor(config, \"key\").get;\n if (getter && getter.isReactWarning) return !1;\n }\n return void 0 !== config.key;\n }\n function defineKeyPropWarningGetter(props, displayName) {\n function warnAboutAccessingKey() {\n specialPropKeyWarningShown ||\n ((specialPropKeyWarningShown = !0),\n console.error(\n \"%s: `key` is not a prop. Trying to access it will result in Line truncated
-220
View File
@@ -1,220 +0,0 @@
import { t as __commonJSMin } from "./rolldown-runtime-DC62tzP2.js";
import { t as require_react } from "./react.js";
//#region node_modules/react/cjs/react-jsx-runtime.development.js
/**
* @license React
* react-jsx-runtime.development.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
var require_react_jsx_runtime_development = /* @__PURE__ */ __commonJSMin(((exports) => {
(function() {
function getComponentNameFromType(type) {
if (null == type) return null;
if ("function" === typeof type) return type.$$typeof === REACT_CLIENT_REFERENCE ? null : type.displayName || type.name || null;
if ("string" === typeof type) return type;
switch (type) {
case REACT_FRAGMENT_TYPE: return "Fragment";
case REACT_PROFILER_TYPE: return "Profiler";
case REACT_STRICT_MODE_TYPE: return "StrictMode";
case REACT_SUSPENSE_TYPE: return "Suspense";
case REACT_SUSPENSE_LIST_TYPE: return "SuspenseList";
case REACT_ACTIVITY_TYPE: return "Activity";
case REACT_VIEW_TRANSITION_TYPE: return "ViewTransition";
}
if ("object" === typeof type) switch ("number" === typeof type.tag && console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."), type.$$typeof) {
case REACT_PORTAL_TYPE: return "Portal";
case REACT_CONTEXT_TYPE: return type.displayName || "Context";
case REACT_CONSUMER_TYPE: return (type._context.displayName || "Context") + ".Consumer";
case REACT_FORWARD_REF_TYPE:
var innerType = type.render;
type = type.displayName;
type || (type = innerType.displayName || innerType.name || "", type = "" !== type ? "ForwardRef(" + type + ")" : "ForwardRef");
return type;
case REACT_MEMO_TYPE: return innerType = type.displayName || null, null !== innerType ? innerType : getComponentNameFromType(type.type) || "Memo";
case REACT_LAZY_TYPE:
innerType = type._payload;
type = type._init;
try {
return getComponentNameFromType(type(innerType));
} catch (x) {}
}
return null;
}
function testStringCoercion(value) {
return "" + value;
}
function checkKeyStringCoercion(value) {
try {
testStringCoercion(value);
var JSCompiler_inline_result = !1;
} catch (e) {
JSCompiler_inline_result = !0;
}
if (JSCompiler_inline_result) {
JSCompiler_inline_result = console;
var JSCompiler_temp_const = JSCompiler_inline_result.error;
var JSCompiler_inline_result$jscomp$0 = "function" === typeof Symbol && Symbol.toStringTag && value[Symbol.toStringTag] || value.constructor.name || "Object";
JSCompiler_temp_const.call(JSCompiler_inline_result, "The provided key is an unsupported type %s. This value must be coerced to a string before using it here.", JSCompiler_inline_result$jscomp$0);
return testStringCoercion(value);
}
}
function getTaskName(type) {
if (type === REACT_FRAGMENT_TYPE) return "<>";
if ("object" === typeof type && null !== type && type.$$typeof === REACT_LAZY_TYPE) return "<...>";
try {
var name = getComponentNameFromType(type);
return name ? "<" + name + ">" : "<...>";
} catch (x) {
return "<...>";
}
}
function getOwner() {
var dispatcher = ReactSharedInternals.A;
return null === dispatcher ? null : dispatcher.getOwner();
}
function UnknownOwner() {
return Error("react-stack-top-frame");
}
function hasValidKey(config) {
if (hasOwnProperty.call(config, "key")) {
var getter = Object.getOwnPropertyDescriptor(config, "key").get;
if (getter && getter.isReactWarning) return !1;
}
return void 0 !== config.key;
}
function defineKeyPropWarningGetter(props, displayName) {
function warnAboutAccessingKey() {
specialPropKeyWarningShown || (specialPropKeyWarningShown = !0, console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)", displayName));
}
warnAboutAccessingKey.isReactWarning = !0;
Object.defineProperty(props, "key", {
get: warnAboutAccessingKey,
configurable: !0
});
}
function elementRefGetterWithDeprecationWarning() {
var componentName = getComponentNameFromType(this.type);
didWarnAboutElementRef[componentName] || (didWarnAboutElementRef[componentName] = !0, console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release."));
componentName = this.props.ref;
return void 0 !== componentName ? componentName : null;
}
function ReactElement(type, key, props, owner, debugStack, debugTask) {
var refProp = props.ref;
type = {
$$typeof: REACT_ELEMENT_TYPE,
type,
key,
props,
_owner: owner
};
null !== (void 0 !== refProp ? refProp : null) ? Object.defineProperty(type, "ref", {
enumerable: !1,
get: elementRefGetterWithDeprecationWarning
}) : Object.defineProperty(type, "ref", {
enumerable: !1,
value: null
});
type._store = {};
Object.defineProperty(type._store, "validated", {
configurable: !1,
enumerable: !1,
writable: !0,
value: 0
});
Object.defineProperty(type, "_debugInfo", {
configurable: !1,
enumerable: !1,
writable: !0,
value: null
});
Object.defineProperty(type, "_debugStack", {
configurable: !1,
enumerable: !1,
writable: !0,
value: debugStack
});
Object.defineProperty(type, "_debugTask", {
configurable: !1,
enumerable: !1,
writable: !0,
value: debugTask
});
Object.freeze && (Object.freeze(type.props), Object.freeze(type));
return type;
}
function jsxDEVImpl(type, config, maybeKey, isStaticChildren, debugStack, debugTask) {
var children = config.children;
if (void 0 !== children) if (isStaticChildren) if (isArrayImpl(children)) {
for (isStaticChildren = 0; isStaticChildren < children.length; isStaticChildren++) validateChildKeys(children[isStaticChildren]);
Object.freeze && Object.freeze(children);
} else console.error("React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.");
else validateChildKeys(children);
if (hasOwnProperty.call(config, "key")) {
children = getComponentNameFromType(type);
var keys = Object.keys(config).filter(function(k) {
return "key" !== k;
});
isStaticChildren = 0 < keys.length ? "{key: someKey, " + keys.join(": ..., ") + ": ...}" : "{key: someKey}";
didWarnAboutKeySpread[children + isStaticChildren] || (keys = 0 < keys.length ? "{" + keys.join(": ..., ") + ": ...}" : "{}", console.error("A props object containing a \"key\" prop is being spread into JSX:\n let props = %s;\n <%s {...props} />\nReact keys must be passed directly to JSX without using spread:\n let props = %s;\n <%s key={someKey} {...props} />", isStaticChildren, children, keys, children), didWarnAboutKeySpread[children + isStaticChildren] = !0);
}
children = null;
void 0 !== maybeKey && (checkKeyStringCoercion(maybeKey), children = "" + maybeKey);
hasValidKey(config) && (checkKeyStringCoercion(config.key), children = "" + config.key);
if ("key" in config) {
maybeKey = {};
for (var propName in config) "key" !== propName && (maybeKey[propName] = config[propName]);
} else maybeKey = config;
children && defineKeyPropWarningGetter(maybeKey, "function" === typeof type ? type.displayName || type.name || "Unknown" : type);
return ReactElement(type, children, maybeKey, getOwner(), debugStack, debugTask);
}
function validateChildKeys(node) {
isValidElement(node) ? node._store && (node._store.validated = 1) : "object" === typeof node && null !== node && node.$$typeof === REACT_LAZY_TYPE && ("fulfilled" === node._payload.status ? isValidElement(node._payload.value) && node._payload.value._store && (node._payload.value._store.validated = 1) : node._store && (node._store.validated = 1));
}
function isValidElement(object) {
return "object" === typeof object && null !== object && object.$$typeof === REACT_ELEMENT_TYPE;
}
var React = require_react(), REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = Symbol.for("react.profiler"), REACT_CONSUMER_TYPE = Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = Symbol.for("react.memo"), REACT_LAZY_TYPE = Symbol.for("react.lazy"), REACT_ACTIVITY_TYPE = Symbol.for("react.activity"), REACT_VIEW_TRANSITION_TYPE = Symbol.for("react.view_transition"), REACT_CLIENT_REFERENCE = Symbol.for("react.client.reference"), ReactSharedInternals = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, hasOwnProperty = Object.prototype.hasOwnProperty, isArrayImpl = Array.isArray, createTask = console.createTask ? console.createTask : function() {
return null;
};
React = { react_stack_bottom_frame: function(callStackForError) {
return callStackForError();
} };
var specialPropKeyWarningShown;
var didWarnAboutElementRef = {};
var unknownOwnerDebugStack = React.react_stack_bottom_frame.bind(React, UnknownOwner)();
var unknownOwnerDebugTask = createTask(getTaskName(UnknownOwner));
var didWarnAboutKeySpread = {};
exports.Fragment = REACT_FRAGMENT_TYPE;
exports.jsx = function(type, config, maybeKey) {
var trackActualOwner = 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++;
if (trackActualOwner) {
var previousStackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 10;
var debugStackDEV = Error("react-stack-top-frame");
Error.stackTraceLimit = previousStackTraceLimit;
} else debugStackDEV = unknownOwnerDebugStack;
return jsxDEVImpl(type, config, maybeKey, !1, debugStackDEV, trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask);
};
exports.jsxs = function(type, config, maybeKey) {
var trackActualOwner = 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++;
if (trackActualOwner) {
var previousStackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 10;
var debugStackDEV = Error("react-stack-top-frame");
Error.stackTraceLimit = previousStackTraceLimit;
} else debugStackDEV = unknownOwnerDebugStack;
return jsxDEVImpl(type, config, maybeKey, !0, debugStackDEV, trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask);
};
})();
}));
//#endregion
//#region node_modules/react/jsx-runtime.js
var require_jsx_runtime = /* @__PURE__ */ __commonJSMin(((exports, module) => {
module.exports = require_react_jsx_runtime_development();
}));
//#endregion
export default require_jsx_runtime();
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"react_jsx-runtime.js","names":[],"sources":["../../react/cjs/react-jsx-runtime.development.js","../../react/jsx-runtime.js"],"sourcesContent":["/**\n * @license React\n * react-jsx-runtime.development.js\n *\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n\"use strict\";\n\"production\" !== process.env.NODE_ENV &&\n (function () {\n function getComponentNameFromType(type) {\n if (null == type) return null;\n if (\"function\" === typeof type)\n return type.$$typeof === REACT_CLIENT_REFERENCE\n ? null\n : type.displayName || type.name || null;\n if (\"string\" === typeof type) return type;\n switch (type) {\n case REACT_FRAGMENT_TYPE:\n return \"Fragment\";\n case REACT_PROFILER_TYPE:\n return \"Profiler\";\n case REACT_STRICT_MODE_TYPE:\n return \"StrictMode\";\n case REACT_SUSPENSE_TYPE:\n return \"Suspense\";\n case REACT_SUSPENSE_LIST_TYPE:\n return \"SuspenseList\";\n case REACT_ACTIVITY_TYPE:\n return \"Activity\";\n case REACT_VIEW_TRANSITION_TYPE:\n return \"ViewTransition\";\n }\n if (\"object\" === typeof type)\n switch (\n (\"number\" === typeof type.tag &&\n console.error(\n \"Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue.\"\n ),\n type.$$typeof)\n ) {\n case REACT_PORTAL_TYPE:\n return \"Portal\";\n case REACT_CONTEXT_TYPE:\n return type.displayName || \"Context\";\n case REACT_CONSUMER_TYPE:\n return (type._context.displayName || \"Context\") + \".Consumer\";\n case REACT_FORWARD_REF_TYPE:\n var innerType = type.render;\n type = type.displayName;\n type ||\n ((type = innerType.displayName || innerType.name || \"\"),\n (type = \"\" !== type ? \"ForwardRef(\" + type + \")\" : \"ForwardRef\"));\n return type;\n case REACT_MEMO_TYPE:\n return (\n (innerType = type.displayName || null),\n null !== innerType\n ? innerType\n : getComponentNameFromType(type.type) || \"Memo\"\n );\n case REACT_LAZY_TYPE:\n innerType = type._payload;\n type = type._init;\n try {\n return getComponentNameFromType(type(innerType));\n } catch (x) {}\n }\n return null;\n }\n function testStringCoercion(value) {\n return \"\" + value;\n }\n function checkKeyStringCoercion(value) {\n try {\n testStringCoercion(value);\n var JSCompiler_inline_result = !1;\n } catch (e) {\n JSCompiler_inline_result = !0;\n }\n if (JSCompiler_inline_result) {\n JSCompiler_inline_result = console;\n var JSCompiler_temp_const = JSCompiler_inline_result.error;\n var JSCompiler_inline_result$jscomp$0 =\n (\"function\" === typeof Symbol &&\n Symbol.toStringTag &&\n value[Symbol.toStringTag]) ||\n value.constructor.name ||\n \"Object\";\n JSCompiler_temp_const.call(\n JSCompiler_inline_result,\n \"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.\",\n JSCompiler_inline_result$jscomp$0\n );\n return testStringCoercion(value);\n }\n }\n function getTaskName(type) {\n if (type === REACT_FRAGMENT_TYPE) return \"<>\";\n if (\n \"object\" === typeof type &&\n null !== type &&\n type.$$typeof === REACT_LAZY_TYPE\n )\n return \"<...>\";\n try {\n var name = getComponentNameFromType(type);\n return name ? \"<\" + name + \">\" : \"<...>\";\n } catch (x) {\n return \"<...>\";\n }\n }\n function getOwner() {\n var dispatcher = ReactSharedInternals.A;\n return null === dispatcher ? null : dispatcher.getOwner();\n }\n function UnknownOwner() {\n return Error(\"react-stack-top-frame\");\n }\n function hasValidKey(config) {\n if (hasOwnProperty.call(config, \"key\")) {\n var getter = Object.getOwnPropertyDescriptor(config, \"key\").get;\n if (getter && getter.isReactWarning) return !1;\n }\n return void 0 !== config.key;\n }\n function defineKeyPropWarningGetter(props, displayName) {\n function warnAboutAccessingKey() {\n specialPropKeyWarningShown ||\n ((specialPropKeyWarningShown = !0),\n console.error(\n \"%s: `key` is not a prop. Trying to access it will result in `undefined` beinLine truncated
-33
View File
@@ -1,33 +0,0 @@
//#region \0rolldown/runtime.js
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
var __exportAll = (all, no_symbols) => {
let target = {};
for (var name in all) __defProp(target, name, {
get: all[name],
enumerable: true
});
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
return target;
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
export { __exportAll as n, __toESM as r, __commonJSMin as t };
-73
View File
@@ -1,73 +0,0 @@
import { t as __commonJSMin } from "./rolldown-runtime-DC62tzP2.js";
import { t as require_react } from "./react.js";
//#region node_modules/use-sync-external-store/cjs/use-sync-external-store-shim.development.js
/**
* @license React
* use-sync-external-store-shim.development.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
var require_use_sync_external_store_shim_development = /* @__PURE__ */ __commonJSMin(((exports) => {
(function() {
function is(x, y) {
return x === y && (0 !== x || 1 / x === 1 / y) || x !== x && y !== y;
}
function useSyncExternalStore$2(subscribe, getSnapshot) {
didWarnOld18Alpha || void 0 === React.startTransition || (didWarnOld18Alpha = !0, console.error("You are using an outdated, pre-release alpha of React 18 that does not support useSyncExternalStore. The use-sync-external-store shim will not work correctly. Upgrade to a newer pre-release."));
var value = getSnapshot();
if (!didWarnUncachedGetSnapshot) {
var cachedValue = getSnapshot();
objectIs(value, cachedValue) || (console.error("The result of getSnapshot should be cached to avoid an infinite loop"), didWarnUncachedGetSnapshot = !0);
}
cachedValue = useState({ inst: {
value,
getSnapshot
} });
var inst = cachedValue[0].inst, forceUpdate = cachedValue[1];
useLayoutEffect(function() {
inst.value = value;
inst.getSnapshot = getSnapshot;
checkIfSnapshotChanged(inst) && forceUpdate({ inst });
}, [
subscribe,
value,
getSnapshot
]);
useEffect(function() {
checkIfSnapshotChanged(inst) && forceUpdate({ inst });
return subscribe(function() {
checkIfSnapshotChanged(inst) && forceUpdate({ inst });
});
}, [subscribe]);
useDebugValue(value);
return value;
}
function checkIfSnapshotChanged(inst) {
var latestGetSnapshot = inst.getSnapshot;
inst = inst.value;
try {
var nextValue = latestGetSnapshot();
return !objectIs(inst, nextValue);
} catch (error) {
return !0;
}
}
function useSyncExternalStore$1(subscribe, getSnapshot) {
return getSnapshot();
}
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());
var React = require_react(), objectIs = "function" === typeof Object.is ? Object.is : is, useState = React.useState, useEffect = React.useEffect, useLayoutEffect = React.useLayoutEffect, useDebugValue = React.useDebugValue, didWarnOld18Alpha = !1, didWarnUncachedGetSnapshot = !1, shim = "undefined" === typeof window || "undefined" === typeof window.document || "undefined" === typeof window.document.createElement ? useSyncExternalStore$1 : useSyncExternalStore$2;
exports.useSyncExternalStore = void 0 !== React.useSyncExternalStore ? React.useSyncExternalStore : shim;
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error());
})();
}));
//#endregion
//#region node_modules/use-sync-external-store/shim/index.js
var require_shim = /* @__PURE__ */ __commonJSMin(((exports, module) => {
module.exports = require_use_sync_external_store_shim_development();
}));
//#endregion
export { require_shim as t };
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"shim-BX6a9SD7.js","names":[],"sources":["../../use-sync-external-store/cjs/use-sync-external-store-shim.development.js","../../use-sync-external-store/shim/index.js"],"sourcesContent":["/**\n * @license React\n * use-sync-external-store-shim.development.js\n *\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n\"use strict\";\n\"production\" !== process.env.NODE_ENV &&\n (function () {\n function is(x, y) {\n return (x === y && (0 !== x || 1 / x === 1 / y)) || (x !== x && y !== y);\n }\n function useSyncExternalStore$2(subscribe, getSnapshot) {\n didWarnOld18Alpha ||\n void 0 === React.startTransition ||\n ((didWarnOld18Alpha = !0),\n console.error(\n \"You are using an outdated, pre-release alpha of React 18 that does not support useSyncExternalStore. The use-sync-external-store shim will not work correctly. Upgrade to a newer pre-release.\"\n ));\n var value = getSnapshot();\n if (!didWarnUncachedGetSnapshot) {\n var cachedValue = getSnapshot();\n objectIs(value, cachedValue) ||\n (console.error(\n \"The result of getSnapshot should be cached to avoid an infinite loop\"\n ),\n (didWarnUncachedGetSnapshot = !0));\n }\n cachedValue = useState({\n inst: { value: value, getSnapshot: getSnapshot }\n });\n var inst = cachedValue[0].inst,\n forceUpdate = cachedValue[1];\n useLayoutEffect(\n function () {\n inst.value = value;\n inst.getSnapshot = getSnapshot;\n checkIfSnapshotChanged(inst) && forceUpdate({ inst: inst });\n },\n [subscribe, value, getSnapshot]\n );\n useEffect(\n function () {\n checkIfSnapshotChanged(inst) && forceUpdate({ inst: inst });\n return subscribe(function () {\n checkIfSnapshotChanged(inst) && forceUpdate({ inst: inst });\n });\n },\n [subscribe]\n );\n useDebugValue(value);\n return value;\n }\n function checkIfSnapshotChanged(inst) {\n var latestGetSnapshot = inst.getSnapshot;\n inst = inst.value;\n try {\n var nextValue = latestGetSnapshot();\n return !objectIs(inst, nextValue);\n } catch (error) {\n return !0;\n }\n }\n function useSyncExternalStore$1(subscribe, getSnapshot) {\n return getSnapshot();\n }\n \"undefined\" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&\n \"function\" ===\n typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart &&\n __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());\n var React = require(\"react\"),\n objectIs = \"function\" === typeof Object.is ? Object.is : is,\n useState = React.useState,\n useEffect = React.useEffect,\n useLayoutEffect = React.useLayoutEffect,\n useDebugValue = React.useDebugValue,\n didWarnOld18Alpha = !1,\n didWarnUncachedGetSnapshot = !1,\n shim =\n \"undefined\" === typeof window ||\n \"undefined\" === typeof window.document ||\n \"undefined\" === typeof window.document.createElement\n ? useSyncExternalStore$1\n : useSyncExternalStore$2;\n exports.useSyncExternalStore =\n void 0 !== React.useSyncExternalStore ? React.useSyncExternalStore : shim;\n \"undefined\" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&\n \"function\" ===\n typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&\n __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error());\n })();\n","'use strict';\n\nif (process.env.NODE_ENV === 'production') {\n module.exports = require('../cjs/use-sync-external-store-shim.production.js');\n} else {\n module.exports = require('../cjs/use-sync-external-store-shim.development.js');\n}\n"],"mappings":";;;;;;;;;;;;;CAWA,CACG,WAAY;EACX,SAAS,GAAG,GAAG,GAAG;GAChB,OAAQ,MAAM,MAAM,MAAM,KAAK,IAAI,MAAM,IAAI,MAAQ,MAAM,KAAK,MAAM;EACxE;EACA,SAAS,uBAAuB,WAAW,aAAa;GACtD,qBACE,KAAK,MAAM,MAAM,oBACf,oBAAoB,CAAC,GACvB,QAAQ,MACN,gMACF;GACF,IAAI,QAAQ,YAAY;GACxB,IAAI,CAAC,4BAA4B;IAC/B,IAAI,cAAc,YAAY;IAC9B,SAAS,OAAO,WAAW,MACxB,QAAQ,MACP,sEACF,GACC,6BAA6B,CAAC;GACnC;GACA,cAAc,SAAS,EACrB,MAAM;IAAS;IAAoB;GAAY,EACjD,CAAC;GACD,IAAI,OAAO,YAAY,EAAE,CAAC,MACxB,cAAc,YAAY;GAC5B,gBACE,WAAY;IACV,KAAK,QAAQ;IACb,KAAK,cAAc;IACnB,uBAAuB,IAAI,KAAK,YAAY,EAAQ,KAAK,CAAC;GAC5D,GACA;IAAC;IAAW;IAAO;GAAW,CAChC;GACA,UACE,WAAY;IACV,uBAAuB,IAAI,KAAK,YAAY,EAAQ,KAAK,CAAC;IAC1D,OAAO,UAAU,WAAY;KAC3B,uBAAuB,IAAI,KAAK,YAAY,EAAQ,KAAK,CAAC;IAC5D,CAAC;GACH,GACA,CAAC,SAAS,CACZ;GACA,cAAc,KAAK;GACnB,OAAO;EACT;EACA,SAAS,uBAAuB,MAAM;GACpC,IAAI,oBAAoB,KAAK;GAC7B,OAAO,KAAK;GACZ,IAAI;IACF,IAAI,YAAY,kBAAkB;IAClC,OAAO,CAAC,SAAS,MAAM,SAAS;GAClC,SAAS,OAAO;IACd,OAAO,CAAC;GACV;EACF;EACA,SAAS,uBAAuB,WAAW,aAAa;Line truncated
-923
View File
@@ -1,923 +0,0 @@
import { r as __toESM } from "./rolldown-runtime-DC62tzP2.js";
import { t as require_react } from "./react.js";
import { t as require_shim } from "./shim-BX6a9SD7.js";
//#region node_modules/dequal/lite/index.mjs
var import_react = /* @__PURE__ */ __toESM(require_react(), 1);
var import_shim = require_shim();
var has = Object.prototype.hasOwnProperty;
function dequal(foo, bar) {
var ctor, len;
if (foo === bar) return true;
if (foo && bar && (ctor = foo.constructor) === bar.constructor) {
if (ctor === Date) return foo.getTime() === bar.getTime();
if (ctor === RegExp) return foo.toString() === bar.toString();
if (ctor === Array) {
if ((len = foo.length) === bar.length) while (len-- && dequal(foo[len], bar[len]));
return len === -1;
}
if (!ctor || typeof foo === "object") {
len = 0;
for (ctor in foo) {
if (has.call(foo, ctor) && ++len && !has.call(bar, ctor)) return false;
if (!(ctor in bar) || !dequal(foo[ctor], bar[ctor])) return false;
}
return Object.keys(bar).length === len;
}
}
return foo !== foo && bar !== bar;
}
var SWRGlobalState = /* @__PURE__ */ new WeakMap();
var noop = () => {};
var OBJECT = Object;
var isUndefined = (v) => v === void 0;
var isFunction = (v) => typeof v == "function";
var mergeObjects = (a, b) => ({
...a,
...b
});
var isPromiseLike = (x) => isFunction(x.then);
var EMPTY_CACHE = {};
var INITIAL_CACHE = {};
var STR_UNDEFINED = "undefined";
var isWindowDefined = typeof window != STR_UNDEFINED;
var isDocumentDefined = typeof document != STR_UNDEFINED;
var isLegacyDeno = isWindowDefined && "Deno" in window;
var hasRequestAnimationFrame = () => isWindowDefined && typeof window["requestAnimationFrame"] != STR_UNDEFINED;
var createCacheHelper = (cache, key) => {
const state = SWRGlobalState.get(cache);
return [
() => !isUndefined(key) && cache.get(key) || EMPTY_CACHE,
(info) => {
if (!isUndefined(key)) {
const prev = cache.get(key);
if (!(key in INITIAL_CACHE)) INITIAL_CACHE[key] = prev;
state[5](key, mergeObjects(prev, info), prev || EMPTY_CACHE);
}
},
state[6],
() => {
if (!isUndefined(key)) {
if (key in INITIAL_CACHE) return INITIAL_CACHE[key];
}
return !isUndefined(key) && cache.get(key) || EMPTY_CACHE;
}
];
};
/**
* Due to the bug https://bugs.chromium.org/p/chromium/issues/detail?id=678075,
* it's not reliable to detect if the browser is currently online or offline
* based on `navigator.onLine`.
* As a workaround, we always assume it's online on the first load, and change
* the status upon `online` or `offline` events.
*/ var online = true;
var isOnline = () => online;
var [onWindowEvent, offWindowEvent] = isWindowDefined && window.addEventListener ? [window.addEventListener.bind(window), window.removeEventListener.bind(window)] : [noop, noop];
var isVisible = () => {
const visibilityState = isDocumentDefined && document.visibilityState;
return isUndefined(visibilityState) || visibilityState !== "hidden";
};
var initFocus = (callback) => {
if (isDocumentDefined) document.addEventListener("visibilitychange", callback);
onWindowEvent("focus", callback);
return () => {
if (isDocumentDefined) document.removeEventListener("visibilitychange", callback);
offWindowEvent("focus", callback);
};
};
var initReconnect = (callback) => {
const onOnline = () => {
online = true;
callback();
};
const onOffline = () => {
online = false;
};
onWindowEvent("online", onOnline);
onWindowEvent("offline", onOffline);
return () => {
offWindowEvent("online", onOnline);
offWindowEvent("offline", onOffline);
};
};
var preset = {
isOnline,
isVisible
};
var defaultConfigOptions = {
initFocus,
initReconnect
};
var IS_REACT_LEGACY = !import_react.useId;
var IS_SERVER = !isWindowDefined || isLegacyDeno;
var rAF = (f) => hasRequestAnimationFrame() ? window["requestAnimationFrame"](f) : setTimeout(f, 1);
var useIsomorphicLayoutEffect = IS_SERVER ? import_react.useEffect : import_react.useLayoutEffect;
var navigatorConnection = typeof navigator !== "undefined" && navigator.connection;
var slowConnection = !IS_SERVER && navigatorConnection && (["slow-2g", "2g"].includes(navigatorConnection.effectiveType) || navigatorConnection.saveData);
var table = /* @__PURE__ */ new WeakMap();
var getTypeName = (value) => OBJECT.prototype.toString.call(value);
var isObjectTypeName = (typeName, type) => typeName === `[object ${type}]`;
var counter = 0;
var stableHash = (arg) => {
const type = typeof arg;
const typeName = getTypeName(arg);
const isDate = isObjectTypeName(typeName, "Date");
const isRegex = isObjectTypeName(typeName, "RegExp");
const isPlainObject = isObjectTypeName(typeName, "Object");
let result;
let index;
if (OBJECT(arg) === arg && !isDate && !isRegex) {
result = table.get(arg);
if (result) return result;
result = ++counter + "~";
table.set(arg, result);
if (Array.isArray(arg)) {
result = "@";
for (index = 0; index < arg.length; index++) result += stableHash(arg[index]) + ",";
table.set(arg, result);
}
if (isPlainObject) {
result = "#";
const keys = OBJECT.keys(arg).sort();
while (!isUndefined(index = keys.pop())) if (!isUndefined(arg[index])) result += index + ":" + stableHash(arg[index]) + ",";
table.set(arg, result);
}
} else result = isDate ? arg.toJSON() : type == "symbol" ? arg.toString() : type == "string" ? JSON.stringify(arg) : "" + arg;
return result;
};
var serialize = (key) => {
if (isFunction(key)) try {
key = key();
} catch (err) {
key = "";
}
const args = key;
key = typeof key == "string" ? key : (Array.isArray(key) ? key.length : key) ? stableHash(key) : "";
return [key, args];
};
var __timestamp = 0;
var getTimestamp = () => ++__timestamp;
async function internalMutate(...args) {
const [cache, _key, _data, _opts] = args;
const options = mergeObjects({
populateCache: true,
throwOnError: true
}, typeof _opts === "boolean" ? { revalidate: _opts } : _opts || {});
let populateCache = options.populateCache;
const rollbackOnErrorOption = options.rollbackOnError;
let optimisticData = options.optimisticData;
const rollbackOnError = (error) => {
return typeof rollbackOnErrorOption === "function" ? rollbackOnErrorOption(error) : rollbackOnErrorOption !== false;
};
const throwOnError = options.throwOnError;
if (isFunction(_key)) {
const keyFilter = _key;
const matchedKeys = [];
const it = cache.keys();
for (const key of it) if (!/^\$(inf|sub)\$/.test(key) && keyFilter(cache.get(key)._k)) matchedKeys.push(key);
return Promise.all(matchedKeys.map(mutateByKey));
}
return mutateByKey(_key);
async function mutateByKey(_k) {
const [key] = serialize(_k);
if (!key) return;
const [get, set] = createCacheHelper(cache, key);
const [EVENT_REVALIDATORS, MUTATION, FETCH, PRELOAD] = SWRGlobalState.get(cache);
const startRevalidate = () => {
const revalidators = EVENT_REVALIDATORS[key];
if (isFunction(options.revalidate) ? options.revalidate(get().data, _k) : options.revalidate !== false) {
delete FETCH[key];
delete PRELOAD[key];
if (revalidators && revalidators[0]) return revalidators[0](2).then(() => get().data);
}
return get().data;
};
if (args.length < 3) return startRevalidate();
let data = _data;
let error;
let isError = false;
const beforeMutationTs = getTimestamp();
MUTATION[key] = [beforeMutationTs, 0];
const hasOptimisticData = !isUndefined(optimisticData);
const state = get();
const displayedData = state.data;
const currentData = state._c;
const committedData = isUndefined(currentData) ? displayedData : currentData;
if (hasOptimisticData) {
optimisticData = isFunction(optimisticData) ? optimisticData(committedData, displayedData) : optimisticData;
set({
data: optimisticData,
_c: committedData
});
}
if (isFunction(data)) try {
data = data(committedData);
} catch (err) {
error = err;
isError = true;
}
if (data && isPromiseLike(data)) {
data = await data.catch((err) => {
error = err;
isError = true;
});
if (beforeMutationTs !== MUTATION[key][0]) {
if (isError) throw error;
return data;
} else if (isError && hasOptimisticData && rollbackOnError(error)) {
populateCache = true;
set({
data: committedData,
_c: void 0
});
}
}
if (populateCache) {
if (!isError) {
if (isFunction(populateCache)) set({
data: populateCache(data, committedData),
error: void 0,
_c: void 0
});
else set({
data,
error: void 0,
_c: void 0
});
}
}
MUTATION[key][1] = getTimestamp();
Promise.resolve(startRevalidate()).then(() => {
set({ _c: void 0 });
});
if (isError) {
if (throwOnError) throw error;
return;
}
return data;
}
}
var revalidateAllKeys = (revalidators, type) => {
for (const key in revalidators) if (revalidators[key][0]) revalidators[key][0](type);
};
var initCache = (provider, options) => {
if (!SWRGlobalState.has(provider)) {
const opts = mergeObjects(defaultConfigOptions, options);
const EVENT_REVALIDATORS = Object.create(null);
const mutate = internalMutate.bind(void 0, provider);
let unmount = noop;
const subscriptions = Object.create(null);
const subscribe = (key, callback) => {
const subs = subscriptions[key] || [];
subscriptions[key] = subs;
subs.push(callback);
return () => {
const index = subs.indexOf(callback);
if (index >= 0) {
subs[index] = subs[subs.length - 1];
subs.pop();
}
};
};
const setter = (key, value, prev) => {
provider.set(key, value);
const subs = subscriptions[key];
if (subs) for (const fn of subs) fn(value, prev);
};
const unload = (unloadOptions) => {
const state = SWRGlobalState.get(provider);
const [, MUTATION, FETCH, PRELOAD] = state;
const ts = getTimestamp();
state[8]++;
for (const key in FETCH) delete FETCH[key];
for (const key in PRELOAD) delete PRELOAD[key];
for (const key in MUTATION) MUTATION[key] = [ts, ts];
const emptyState = {};
for (const key of [...provider.keys()]) {
const prev = provider.get(key);
provider.delete(key);
const subs = subscriptions[key];
if (subs) for (const fn of subs) fn(emptyState, prev);
}
const revalidate = !unloadOptions || unloadOptions.revalidate !== false;
for (const key in EVENT_REVALIDATORS) {
const revalidators = EVENT_REVALIDATORS[key];
for (let i = 0; i < revalidators.length; i++) revalidators[i](4, { revalidate: revalidate && !i });
}
};
const initProvider = () => {
if (!SWRGlobalState.has(provider)) {
SWRGlobalState.set(provider, [
EVENT_REVALIDATORS,
Object.create(null),
Object.create(null),
Object.create(null),
mutate,
setter,
subscribe,
unload,
0
]);
if (!IS_SERVER) {
const releaseFocus = opts.initFocus(setTimeout.bind(void 0, revalidateAllKeys.bind(void 0, EVENT_REVALIDATORS, 0)));
const releaseReconnect = opts.initReconnect(setTimeout.bind(void 0, revalidateAllKeys.bind(void 0, EVENT_REVALIDATORS, 1)));
unmount = () => {
releaseFocus && releaseFocus();
releaseReconnect && releaseReconnect();
SWRGlobalState.delete(provider);
};
}
}
};
initProvider();
return [
provider,
mutate,
initProvider,
unmount,
unload
];
}
const state = SWRGlobalState.get(provider);
return [
provider,
state[4],
void 0,
void 0,
state[7]
];
};
var onErrorRetry = (_, __, config, revalidate, opts) => {
const maxRetryCount = config.errorRetryCount;
const currentRetryCount = opts.retryCount;
const timeout = ~~((Math.random() + .5) * (1 << (currentRetryCount < 8 ? currentRetryCount : 8))) * config.errorRetryInterval;
if (!isUndefined(maxRetryCount) && currentRetryCount > maxRetryCount) return;
setTimeout(revalidate, timeout, opts);
};
var compare = dequal;
var [cache, mutate, , , unload] = initCache(/* @__PURE__ */ new Map());
var defaultConfig = mergeObjects({
onLoadingSlow: noop,
onSuccess: noop,
onError: noop,
onErrorRetry,
onDiscarded: noop,
revalidateOnFocus: true,
revalidateOnReconnect: true,
revalidateIfStale: true,
shouldRetryOnError: true,
errorRetryInterval: slowConnection ? 1e4 : 5e3,
focusThrottleInterval: 5e3,
dedupingInterval: 2e3,
loadingTimeout: slowConnection ? 5e3 : 3e3,
compare,
isPaused: () => false,
cache,
mutate,
unload,
fallback: {}
}, preset);
var mergeConfigs = (a, b) => {
const v = mergeObjects(a, b);
if (b) {
const { use: u1, fallback: f1, cacheData: c1 } = a;
const { use: u2, fallback: f2, cacheData: c2 } = b;
if (u1 && u2) v.use = u1.concat(u2);
if (f1 && f2) v.fallback = mergeObjects(f1, f2);
if (c1 && c2) v.cacheData = mergeObjects(c1, c2);
}
return v;
};
var SWRConfigContext = (0, import_react.createContext)({});
var SWRConfig$1 = (props) => {
const { value } = props;
const parentConfig = (0, import_react.useContext)(SWRConfigContext);
const isFunctionalConfig = isFunction(value);
const config = (0, import_react.useMemo)(() => isFunctionalConfig ? value(parentConfig) : value, [
isFunctionalConfig,
parentConfig,
value
]);
const extendedConfig = (0, import_react.useMemo)(() => isFunctionalConfig ? config : mergeConfigs(parentConfig, config), [
isFunctionalConfig,
parentConfig,
config
]);
const provider = config && config.provider;
const cacheContextRef = (0, import_react.useRef)(void 0);
if (provider && !cacheContextRef.current) cacheContextRef.current = initCache(provider(extendedConfig.cache || cache), config);
const cacheContext = cacheContextRef.current;
if (cacheContext) {
extendedConfig.cache = cacheContext[0];
extendedConfig.mutate = cacheContext[1];
extendedConfig.unload = cacheContext[4];
}
useIsomorphicLayoutEffect(() => {
if (cacheContext) {
cacheContext[2] && cacheContext[2]();
return cacheContext[3];
}
}, []);
return (0, import_react.createElement)(SWRConfigContext.Provider, mergeObjects(props, { value: extendedConfig }));
};
//#endregion
//#region node_modules/swr/dist/_internal/index.mjs
var enableDevtools = isWindowDefined && window.__SWR_DEVTOOLS_USE__;
var use$1 = enableDevtools ? window.__SWR_DEVTOOLS_USE__ : [];
var setupDevTools = () => {
if (enableDevtools) window.__SWR_DEVTOOLS_REACT__ = import_react.default;
};
var normalize = (args) => {
return isFunction(args[1]) ? [
args[0],
args[1],
args[2] || {}
] : [
args[0],
null,
(args[1] === null ? args[2] : args[1]) || {}
];
};
var useSWRConfig = () => {
const parentConfig = (0, import_react.useContext)(SWRConfigContext);
return (0, import_react.useMemo)(() => mergeObjects(defaultConfig, parentConfig), [parentConfig]);
};
var preload = (key_, fetcher) => {
if (IS_SERVER) return;
const [key, fnArg] = serialize(key_);
const [, , , PRELOAD] = SWRGlobalState.get(cache);
if (PRELOAD[key]) return PRELOAD[key];
const req = fetcher(fnArg);
PRELOAD[key] = req;
return req;
};
var middleware = (useSWRNext) => (key_, fetcher_, config) => {
return useSWRNext(key_, fetcher_ && ((...args) => {
const [key] = serialize(key_);
const [, , , PRELOAD] = SWRGlobalState.get(cache);
if (key.startsWith("$inf$")) return fetcher_(...args);
const req = PRELOAD[key];
if (isUndefined(req)) return fetcher_(...args);
delete PRELOAD[key];
return req;
}), config);
};
var BUILT_IN_MIDDLEWARE = use$1.concat(middleware);
var withArgs = (hook) => {
return function useSWRArgs(...args) {
const fallbackConfig = useSWRConfig();
const [key, fn, _config] = normalize(args);
const config = mergeConfigs(fallbackConfig, _config);
let next = hook;
const { use } = config;
const middleware = (use || []).concat(BUILT_IN_MIDDLEWARE);
for (let i = middleware.length; i--;) next = middleware[i](next);
return next(key, fn || config.fetcher || null, config);
};
};
var subscribeCallback = (key, callbacks, callback) => {
const keyedRevalidators = callbacks[key] || (callbacks[key] = []);
keyedRevalidators.push(callback);
return () => {
const index = keyedRevalidators.indexOf(callback);
if (index >= 0) {
keyedRevalidators[index] = keyedRevalidators[keyedRevalidators.length - 1];
keyedRevalidators.pop();
}
};
};
setupDevTools();
//#endregion
//#region node_modules/swr/dist/use-swr-emll9s78.mjs
var use = import_react.use || ((thenable) => {
switch (thenable.status) {
case "pending": throw thenable;
case "fulfilled": return thenable.value;
case "rejected": throw thenable.reason;
default:
thenable.status = "pending";
thenable.then((v) => {
thenable.status = "fulfilled";
thenable.value = v;
}, (e) => {
thenable.status = "rejected";
thenable.reason = e;
});
throw thenable;
}
});
var WITH_DEDUPE = { dedupe: true };
var isCacheDataConsumed = (consumedCacheData, cacheData, key) => {
var _consumedCacheData_get;
return cacheData ? ((_consumedCacheData_get = consumedCacheData.get(cacheData)) == null ? void 0 : _consumedCacheData_get.has(key)) === true : false;
};
var markCacheDataConsumed = (consumedCacheData, cacheData, key) => {
if (!cacheData) return;
let consumedKeys = consumedCacheData.get(cacheData);
if (!consumedKeys) {
consumedKeys = /* @__PURE__ */ new Set();
consumedCacheData.set(cacheData, consumedKeys);
}
consumedKeys.add(key);
};
var commitCacheData = ({ value, getCacheData, canCommit, setCache }) => {
const commit = (state) => {
if (canCommit() && isUndefined(getCacheData())) setCache(state);
};
Promise.resolve(value).then((data) => {
commit({
data,
error: void 0
});
}, (error) => {
commit({ error });
});
};
var resolvedUndef = Promise.resolve(void 0);
resolvedUndef.status = "fulfilled";
resolvedUndef.value = void 0;
var sub = () => noop;
/**
* The core implementation of the useSWR hook.
*
* This is the main handler function that implements all SWR functionality including
* data fetching, caching, revalidation, error handling, and state management.
* It manages the complete lifecycle of SWR requests from initialization through
* cleanup.
*
* Key responsibilities:
* - Key serialization and normalization
* - Cache state management and synchronization
* - Automatic and manual revalidation
* - Error handling and retry logic
* - Suspense integration
* - Loading state management
* - Effect cleanup and memory management
*
* @template Data - The type of data returned by the fetcher
* @template Error - The type of error that can be thrown
*
* @param _key - The SWR key (string, array, object, function, or falsy)
* @param fetcher - The fetcher function to retrieve data, or null to disable fetching
* @param config - Complete SWR configuration object with both public and internal options
*
* @returns SWRResponse object containing data, error, mutate function, and loading states
*
* @internal This is the internal implementation. Use `useSWR` instead.
*/ var useSWRHandler = (_key, fetcher, config) => {
const { cache, compare, suspense, fallbackData, revalidateOnMount, revalidateIfStale, refreshInterval, refreshWhenHidden, refreshWhenOffline, keepPreviousData, strictServerPrefetchWarning } = config;
const [EVENT_REVALIDATORS, MUTATION, FETCH, PRELOAD] = SWRGlobalState.get(cache);
const [key, fnArg] = serialize(_key);
const initialMountedRef = (0, import_react.useRef)(false);
const unmountedRef = (0, import_react.useRef)(false);
const keyRef = (0, import_react.useRef)(key);
const fetcherRef = (0, import_react.useRef)(fetcher);
const configRef = (0, import_react.useRef)(config);
const getConfig = () => configRef.current;
const isActive = () => getConfig().isVisible() && getConfig().isOnline();
const [getCache, setCache, subscribeCache, getInitialCache] = createCacheHelper(cache, key);
const stateDependencies = (0, import_react.useRef)({}).current;
const fallback = isUndefined(fallbackData) ? isUndefined(config.fallback) ? void 0 : config.fallback[key] : fallbackData;
const serverCacheData = config.cacheData;
const configCacheData = !key ? void 0 : serverCacheData == null ? void 0 : serverCacheData[key];
const req = key ? PRELOAD[key] : void 0;
const hasCacheData = isUndefined(req) && !isUndefined(configCacheData);
const preloadedData = hasCacheData ? configCacheData : req;
const isEqual = (prev, current) => {
for (const _ in stateDependencies) {
const t = _;
if (t === "data") {
if (!compare(prev[t], current[t])) {
if (!isUndefined(prev[t])) return false;
if (!compare(returnedData, current[t])) return false;
}
} else if (current[t] !== prev[t]) return false;
}
return true;
};
const isInitialMount = !initialMountedRef.current;
const getSnapshot = (0, import_react.useMemo)(() => {
const cachedData = getCache();
const initialData = getInitialCache();
const getSelectedCache = (state) => {
const snapshot = mergeObjects(state);
delete snapshot._k;
if (!(() => {
if (!key) return false;
if (!fetcher) return false;
if (getConfig().isPaused()) return false;
if (isInitialMount && !isUndefined(revalidateOnMount)) return revalidateOnMount;
const data = !isUndefined(fallback) ? fallback : snapshot.data;
if (suspense && hasCacheData && isUndefined(data)) return false;
if (suspense) return isUndefined(data) || revalidateIfStale;
return isUndefined(data) || revalidateIfStale;
})()) return snapshot;
return {
isValidating: true,
isLoading: true,
...snapshot
};
};
const clientSnapshot = getSelectedCache(cachedData);
const serverSnapshot = cachedData === initialData ? clientSnapshot : getSelectedCache(initialData);
let memorizedSnapshot = clientSnapshot;
return [() => {
const newSnapshot = getSelectedCache(getCache());
if (isEqual(newSnapshot, memorizedSnapshot)) {
memorizedSnapshot.data = newSnapshot.data;
memorizedSnapshot.isLoading = newSnapshot.isLoading;
memorizedSnapshot.isValidating = newSnapshot.isValidating;
memorizedSnapshot.error = newSnapshot.error;
return memorizedSnapshot;
} else {
memorizedSnapshot = newSnapshot;
return newSnapshot;
}
}, () => serverSnapshot];
}, [cache, key]);
const cached = (0, import_shim.useSyncExternalStore)((0, import_react.useCallback)((callback) => subscribeCache(key, (current, prev) => {
if (!isEqual(prev, current)) callback();
}), [cache, key]), getSnapshot[0], getSnapshot[1]);
const hasRevalidator = EVENT_REVALIDATORS[key] && EVENT_REVALIDATORS[key].length > 0;
const cachedData = cached.data;
let data = isUndefined(cachedData) ? fallback && isPromiseLike(fallback) ? use(fallback) : fallback : cachedData;
const error = cached.error;
const laggyDataRef = (0, import_react.useRef)(data);
const consumedCacheDataRef = (0, import_react.useRef)(void 0);
let consumedCacheData = consumedCacheDataRef.current;
if (!consumedCacheData) {
consumedCacheData = /* @__PURE__ */ new WeakMap();
consumedCacheDataRef.current = consumedCacheData;
}
const preloadCacheRef = (0, import_react.useRef)(null);
let returnedData = keepPreviousData ? isUndefined(cachedData) ? isUndefined(laggyDataRef.current) ? data : laggyDataRef.current : cachedData : data;
const hasKeyButNoData = key && isUndefined(data);
const hydrationRef = (0, import_react.useRef)(null);
!IS_SERVER && (0, import_shim.useSyncExternalStore)(sub, () => {
hydrationRef.current = false;
return hydrationRef;
}, () => {
hydrationRef.current = true;
return hydrationRef;
});
const isHydration = hydrationRef.current;
if (strictServerPrefetchWarning && isHydration && !suspense && hasKeyButNoData) console.warn(`Missing pre-initiated data for serialized key "${key}" during server-side rendering. Data fetching should be initiated on the server and provided to SWR via fallback data. You can set "strictServerPrefetchWarning: false" to disable this warning.`);
const shouldDoInitialRevalidation = (() => {
if (!key || !fetcher) return false;
if (getConfig().isPaused()) return false;
if (hasRevalidator && !isUndefined(error)) return false;
if (isInitialMount && !isUndefined(revalidateOnMount)) return revalidateOnMount;
if (suspense && hasCacheData && hasKeyButNoData) return false;
if (suspense) return isUndefined(data) ? false : revalidateIfStale;
return isUndefined(data) || revalidateIfStale;
})();
const defaultValidatingState = isInitialMount && shouldDoInitialRevalidation;
const isValidating = isUndefined(cached.isValidating) ? defaultValidatingState : cached.isValidating;
const isLoading = isUndefined(cached.isLoading) ? defaultValidatingState : cached.isLoading;
const revalidate = (0, import_react.useCallback)(async (revalidateOpts) => {
const currentFetcher = fetcherRef.current;
if (!key || !currentFetcher || unmountedRef.current || getConfig().isPaused()) return false;
let newData;
let startAt;
let loading = true;
const opts = revalidateOpts || {};
const shouldStartNewRequest = !FETCH[key] || !opts.dedupe;
const shouldUseRSCPreload = hasCacheData && !isCacheDataConsumed(consumedCacheData, serverCacheData, key) && !isUndefined(preloadedData) && isUndefined(getCache().data);
const callbackSafeguard = () => {
if (IS_REACT_LEGACY) return !unmountedRef.current && key === keyRef.current && initialMountedRef.current;
return key === keyRef.current;
};
const finalState = {
isValidating: false,
isLoading: false
};
const finishRequestAndUpdateState = () => {
setCache(finalState);
};
const cleanupState = () => {
const requestInfo = FETCH[key];
if (requestInfo && requestInfo[1] === startAt) delete FETCH[key];
};
const initialState = { isValidating: true };
if (isUndefined(getCache().data)) initialState.isLoading = true;
try {
if (shouldStartNewRequest) {
setCache(initialState);
if (config.loadingTimeout && isUndefined(getCache().data)) setTimeout(() => {
if (loading && callbackSafeguard()) getConfig().onLoadingSlow(key, config);
}, config.loadingTimeout);
if (shouldUseRSCPreload) markCacheDataConsumed(consumedCacheData, serverCacheData, key);
FETCH[key] = [shouldUseRSCPreload ? preloadedData : currentFetcher(fnArg), getTimestamp()];
if (shouldUseRSCPreload && PRELOAD[key]) delete PRELOAD[key];
}
[newData, startAt] = FETCH[key];
newData = await newData;
if (shouldStartNewRequest) setTimeout(cleanupState, config.dedupingInterval);
if (!FETCH[key] || FETCH[key][1] !== startAt) {
if (shouldStartNewRequest) {
if (callbackSafeguard()) getConfig().onDiscarded(key);
}
return false;
}
finalState.error = void 0;
const mutationInfo = MUTATION[key];
if (!isUndefined(mutationInfo) && (startAt <= mutationInfo[0] || startAt <= mutationInfo[1] || mutationInfo[1] === 0)) {
finishRequestAndUpdateState();
if (shouldStartNewRequest) {
if (callbackSafeguard()) getConfig().onDiscarded(key);
}
return false;
}
const cacheData = getCache().data;
finalState.data = compare(cacheData, newData) ? cacheData : newData;
if (shouldStartNewRequest) {
if (callbackSafeguard()) getConfig().onSuccess(newData, key, config);
}
} catch (err) {
cleanupState();
const currentConfig = getConfig();
const { shouldRetryOnError } = currentConfig;
if (!currentConfig.isPaused()) {
finalState.error = err;
if (shouldStartNewRequest && callbackSafeguard()) {
currentConfig.onError(err, key, currentConfig);
if (shouldRetryOnError === true || isFunction(shouldRetryOnError) && shouldRetryOnError(err)) {
if (!getConfig().revalidateOnFocus || !getConfig().revalidateOnReconnect || isActive()) currentConfig.onErrorRetry(err, key, currentConfig, (_opts) => {
const revalidators = EVENT_REVALIDATORS[key];
if (revalidators && revalidators[0]) revalidators[0](3, _opts);
}, {
retryCount: (opts.retryCount || 0) + 1,
dedupe: true
});
}
}
}
}
loading = false;
finishRequestAndUpdateState();
return true;
}, [key, cache]);
const boundMutate = (0, import_react.useCallback)((...args) => {
return internalMutate(cache, keyRef.current, ...args);
}, []);
useIsomorphicLayoutEffect(() => {
const preloaded = preloadCacheRef.current;
if (!preloaded) return;
preloadCacheRef.current = null;
markCacheDataConsumed(consumedCacheData, preloaded.cacheData, preloaded.key);
if (isUndefined(getCache().data)) setCache({
data: preloaded.data,
error: void 0,
_k: preloaded._k
});
if (PRELOAD[preloaded.key]) delete PRELOAD[preloaded.key];
});
useIsomorphicLayoutEffect(() => {
fetcherRef.current = fetcher;
configRef.current = config;
if (!isUndefined(cachedData)) laggyDataRef.current = cachedData;
});
useIsomorphicLayoutEffect(() => {
if (fetcher || !hasCacheData || isUndefined(preloadedData) || isCacheDataConsumed(consumedCacheData, serverCacheData, key) || !isUndefined(getCache().data)) return;
markCacheDataConsumed(consumedCacheData, serverCacheData, key);
const mutation = MUTATION[key];
commitCacheData({
value: preloadedData,
getCacheData: () => getCache().data,
canCommit: () => !unmountedRef.current && key === keyRef.current && MUTATION[key] === mutation,
setCache: (state) => setCache({
...state,
_k: fnArg
})
});
});
useIsomorphicLayoutEffect(() => {
if (!key) return;
const softRevalidate = revalidate.bind(void 0, WITH_DEDUPE);
let nextFocusRevalidatedAt = 0;
if (getConfig().revalidateOnFocus) nextFocusRevalidatedAt = Date.now() + getConfig().focusThrottleInterval;
const onRevalidate = (type, opts = {}) => {
if (type == 0) {
const now = Date.now();
if (getConfig().revalidateOnFocus && now > nextFocusRevalidatedAt && isActive()) {
nextFocusRevalidatedAt = now + getConfig().focusThrottleInterval;
softRevalidate();
}
} else if (type == 1) {
if (getConfig().revalidateOnReconnect && isActive()) softRevalidate();
} else if (type == 2) return revalidate();
else if (type == 3) return revalidate(opts);
else if (type == 4) {
laggyDataRef.current = void 0;
if (opts.revalidate) return revalidate();
}
};
const unsubEvents = subscribeCallback(key, EVENT_REVALIDATORS, onRevalidate);
unmountedRef.current = false;
keyRef.current = key;
initialMountedRef.current = true;
setCache({ _k: fnArg });
if (shouldDoInitialRevalidation) {
if (!FETCH[key]) {
if (isUndefined(data) || IS_SERVER) softRevalidate();
else rAF(softRevalidate);
}
}
return () => {
unmountedRef.current = true;
unsubEvents();
};
}, [key]);
useIsomorphicLayoutEffect(() => {
let timer;
function next() {
const interval = isFunction(refreshInterval) ? refreshInterval(getCache().data) : refreshInterval;
if (interval && timer !== -1) timer = setTimeout(execute, interval);
}
function execute() {
if (!getCache().error && (refreshWhenHidden || getConfig().isVisible()) && (refreshWhenOffline || getConfig().isOnline())) revalidate(WITH_DEDUPE).then(next);
else next();
}
next();
return () => {
if (timer) {
clearTimeout(timer);
timer = -1;
}
};
}, [
refreshInterval,
refreshWhenHidden,
refreshWhenOffline,
key
]);
(0, import_react.useDebugValue)(returnedData);
if (suspense) {
if (!IS_REACT_LEGACY && IS_SERVER && hasKeyButNoData && isUndefined(preloadedData)) throw new Error("Fallback data is required when using Suspense in SSR.");
if (hasKeyButNoData) {
fetcherRef.current = fetcher;
configRef.current = config;
unmountedRef.current = false;
}
const shouldConsumePreload = !isUndefined(preloadedData) && hasKeyButNoData;
let preloadData = void 0;
if (shouldConsumePreload && hasCacheData) {
preloadData = preloadedData && isPromiseLike(preloadedData) ? use(preloadedData) : preloadedData;
data = preloadData;
returnedData = preloadData;
if (!IS_SERVER) preloadCacheRef.current = {
data: preloadData,
_k: fnArg,
key,
cacheData: serverCacheData
};
} else use(shouldConsumePreload ? boundMutate(preloadedData) : resolvedUndef);
if (!isUndefined(error) && hasKeyButNoData) throw error;
const revalidation = hasKeyButNoData && isUndefined(preloadData) ? revalidate(WITH_DEDUPE) : resolvedUndef;
if (!isUndefined(returnedData) && hasKeyButNoData) {
revalidation.status = "fulfilled";
revalidation.value = true;
}
use(revalidation);
}
return {
mutate: boundMutate,
get data() {
stateDependencies.data = true;
return returnedData;
},
get error() {
stateDependencies.error = true;
return error;
},
get isValidating() {
stateDependencies.isValidating = true;
return isValidating;
},
get isLoading() {
stateDependencies.isLoading = true;
return isLoading;
}
};
};
var SWRConfig = OBJECT.defineProperty(SWRConfig$1, "defaultValue", { value: defaultConfig });
/**
* A hook to fetch data.
*
* @see {@link https://swr.vercel.app}
*
* @example
* ```jsx
* import useSWR from 'swr'
* function Profile() {
* const { data, error, isLoading } = useSWR('/api/user', fetcher)
* if (error) return <div>failed to load</div>
* if (isLoading) return <div>loading...</div>
* return <div>hello {data.name}!</div>
* }
* ```
*/ var useSWR = withArgs(useSWRHandler);
//#endregion
//#region node_modules/swr/dist/index/index.mjs
var unstable_serialize = (key) => serialize(key)[0];
//#endregion
export { SWRConfig, useSWR as default, mutate, preload, unload, unstable_serialize, useSWRConfig };
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"swr.js","names":["useEffect","useLayoutEffect","createContext","SWRConfig","useContext","useMemo","useRef","createElement","use","React","useContext","useMemo","useRef","useMemo","useSyncExternalStore","useCallback"],"sources":["../../dequal/lite/index.mjs","../../swr/dist/config-context-ext53wcz.mjs","../../swr/dist/_internal/constants.mjs","../../swr/dist/_internal/index.mjs","../../swr/dist/use-swr-emll9s78.mjs","../../swr/dist/index/index.mjs"],"sourcesContent":["var has = Object.prototype.hasOwnProperty;\n\nexport function dequal(foo, bar) {\n\tvar ctor, len;\n\tif (foo === bar) return true;\n\n\tif (foo && bar && (ctor=foo.constructor) === bar.constructor) {\n\t\tif (ctor === Date) return foo.getTime() === bar.getTime();\n\t\tif (ctor === RegExp) return foo.toString() === bar.toString();\n\n\t\tif (ctor === Array) {\n\t\t\tif ((len=foo.length) === bar.length) {\n\t\t\t\twhile (len-- && dequal(foo[len], bar[len]));\n\t\t\t}\n\t\t\treturn len === -1;\n\t\t}\n\n\t\tif (!ctor || typeof foo === 'object') {\n\t\t\tlen = 0;\n\t\t\tfor (ctor in foo) {\n\t\t\t\tif (has.call(foo, ctor) && ++len && !has.call(bar, ctor)) return false;\n\t\t\t\tif (!(ctor in bar) || !dequal(foo[ctor], bar[ctor])) return false;\n\t\t\t}\n\t\t\treturn Object.keys(bar).length === len;\n\t\t}\n\t}\n\n\treturn foo !== foo && bar !== bar;\n}\n","'use client';\nimport React, { useEffect, useLayoutEffect, createContext, useContext, useMemo, useRef, createElement } from 'react';\nimport { dequal } from 'dequal/lite';\n\nconst FOCUS_EVENT = 0;\nconst RECONNECT_EVENT = 1;\nconst MUTATE_EVENT = 2;\nconst ERROR_REVALIDATE_EVENT = 3;\nconst UNLOAD_EVENT = 4;\n\nvar events = {\n __proto__: null,\n ERROR_REVALIDATE_EVENT: ERROR_REVALIDATE_EVENT,\n FOCUS_EVENT: FOCUS_EVENT,\n MUTATE_EVENT: MUTATE_EVENT,\n RECONNECT_EVENT: RECONNECT_EVENT,\n UNLOAD_EVENT: UNLOAD_EVENT\n};\n\n// Global state used to deduplicate requests and store listeners\nconst SWRGlobalState = new WeakMap();\n\n// Shared state between server components and client components\nconst noop = ()=>{};\n// Using noop() as the undefined value as undefined can be replaced\n// by something else. Prettier ignore and extra parentheses are necessary here\n// to ensure that tsc doesn't remove the __NOINLINE__ comment.\n// prettier-ignore\nconst UNDEFINED = /*#__NOINLINE__*/ noop();\nconst OBJECT = Object;\nconst isUndefined = (v)=>v === UNDEFINED;\nconst isFunction = (v)=>typeof v == 'function';\nconst mergeObjects = (a, b)=>({\n ...a,\n ...b\n });\nconst isPromiseLike = (x)=>isFunction(x.then);\n\nconst EMPTY_CACHE = {};\nconst INITIAL_CACHE = {};\nconst STR_UNDEFINED = 'undefined';\n// NOTE: Use the function to guarantee it's re-evaluated between jsdom and node runtime for tests.\nconst isWindowDefined = typeof window != STR_UNDEFINED;\nconst isDocumentDefined = typeof document != STR_UNDEFINED;\nconst isLegacyDeno = isWindowDefined && 'Deno' in window;\nconst hasRequestAnimationFrame = ()=>isWindowDefined && typeof window['requestAnimationFrame'] != STR_UNDEFINED;\nconst createCacheHelper = (cache, key)=>{\n const state = SWRGlobalState.get(cache);\n return [\n // Getter\n ()=>!isUndefined(key) && cache.get(key) || EMPTY_CACHE,\n // Setter\n (info)=>{\n if (!isUndefined(key)) {\n const prev = cache.get(key);\n // Before writing to the store, we keep the value in the initial cache\n // if it's not there yet.\n if (!(key in INITIAL_CACHE)) {\n INITIAL_CACHE[key] = prev;\n }\n state[5](key, mergeObjects(prev, info), prev || EMPTY_CACHE);\n }\n },\n // Subscriber\n state[6],\n // Get server cache snapshot\n ()=>{\n if (!isUndefined(key)) {\n // If the cache was updated on the client, we return the stored initial value.\n if (key in INITIAL_CACHE) return INITIAL_CACHE[key];\n }\n // If we haven't done any client-side updates, we return the current value.\n return !isUndefined(key) && cache.get(key) || EMPTY_CACHE;\n }\n ];\n} // export { UNDEFINED, OBJECT, isUndefined, isFunction, mergeObjects, isPromiseLike }\n;\n\n/**\n * Due to the bug https://bugs.chromium.org/p/chromium/issues/detail?id=678075,\n * it's not reliable to detect if the browser is currently online or offline\n * based on `navigator.onLine`.\n * As a workaround, we always assume it's online on the first load, and change\n * the status upon `online` or `offline` events.\n */ let online = true;\nconst isOnline = ()=>online;\n// For node and React Native, `add/removeEventListener` doesn't exist on window.\nconst [onWindowEvent, offWindowEvent] = isWindowDefined && window.addEventListener ? [\n window.addEventListener.bind(window),\n window.removeEventListener.bind(window)\n] : [\n noop,\n noop\n];Line truncated
-259
View File
@@ -1,259 +0,0 @@
import { r as __toESM } from "./rolldown-runtime-DC62tzP2.js";
import { t as require_react } from "./react.js";
import { t as require_shim } from "./shim-BX6a9SD7.js";
//#region node_modules/regexparam/dist/index.mjs
/**
* @param {string|RegExp} input The route pattern
* @param {boolean} [loose] Allow open-ended matching. Ignored with `RegExp` input.
*/
function parse(input, loose) {
if (input instanceof RegExp) return {
keys: false,
pattern: input
};
var c, o, tmp, ext, keys = [], pattern = "", arr = input.split("/");
arr[0] || arr.shift();
while (tmp = arr.shift()) {
c = tmp[0];
if (c === "*") {
keys.push(c);
pattern += tmp[1] === "?" ? "(?:/(.*))?" : "/(.*)";
} else if (c === ":") {
o = tmp.indexOf("?", 1);
ext = tmp.indexOf(".", 1);
keys.push(tmp.substring(1, !!~o ? o : !!~ext ? ext : tmp.length));
pattern += !!~o && !~ext ? "(?:/([^/]+?))?" : "/([^/]+?)";
if (!!~ext) pattern += (!!~o ? "?" : "") + "\\" + tmp.substring(ext);
} else pattern += "/" + tmp;
}
return {
keys,
pattern: new RegExp("^" + pattern + (loose ? "(?=$|/)" : "/?$"), "i")
};
}
//#endregion
//#region node_modules/wouter/src/use-sync-external-store.js
var import_react = /* @__PURE__ */ __toESM(require_react(), 1);
var import_shim = require_shim();
//#endregion
//#region node_modules/wouter/src/react-deps.js
var useBuiltinInsertionEffect = import_react.useInsertionEffect;
var useIsomorphicLayoutEffect = !!(typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined") ? import_react.useLayoutEffect : import_react.useEffect;
var useInsertionEffect = useBuiltinInsertionEffect || useIsomorphicLayoutEffect;
var useEvent = (fn) => {
const ref = import_react.useRef([fn, (...args) => ref[0](...args)]).current;
useInsertionEffect(() => {
ref[0] = fn;
});
return ref[1];
};
//#endregion
//#region node_modules/wouter/src/use-browser-location.js
/**
* History API docs @see https://developer.mozilla.org/en-US/docs/Web/API/History
*/
var events = [
"popstate",
"pushState",
"replaceState",
"hashchange"
];
var listeners = [];
var onLocationChange = () => listeners.forEach((callback) => callback());
var subscribeToLocationUpdates = (callback) => {
if (listeners.push(callback) === 1) events.forEach((event) => addEventListener(event, onLocationChange));
return () => {
listeners = listeners.filter((listener) => listener !== callback);
if (!listeners.length) events.forEach((event) => removeEventListener(event, onLocationChange));
};
};
var useLocationProperty = (fn, ssrFn) => (0, import_shim.useSyncExternalStore)(subscribeToLocationUpdates, fn, ssrFn);
var currentSearch = () => location.search;
var useSearch$1 = ({ ssrSearch } = {}) => useLocationProperty(currentSearch, ssrSearch != null ? () => ssrSearch : currentSearch);
var currentPathname = () => location.pathname;
var usePathname = ({ ssrPath } = {}) => useLocationProperty(currentPathname, ssrPath != null ? () => ssrPath : currentPathname);
var navigate = (to, { replace = false, state = null } = {}) => history[replace ? "replaceState" : "pushState"](state, "", to);
var useBrowserLocation = (opts) => [usePathname(opts), navigate];
var patchKey = Symbol.for("wouter_v3");
if (typeof history !== "undefined" && typeof window[patchKey] === "undefined") {
for (const type of ["pushState", "replaceState"]) {
const original = history[type];
history[type] = function() {
const result = original.apply(this, arguments);
const event = new Event(type);
event.arguments = arguments;
dispatchEvent(event);
return result;
};
}
Object.defineProperty(window, patchKey, { value: true });
}
//#endregion
//#region node_modules/wouter/src/paths.js
/**
* When basepath is `undefined` or '/' it is ignored (we assume it's empty string)
*/
var baseDefaults = (base = "") => base === "/" ? "" : base;
var absolutePath = (to, base) => to[0] === "~" ? to.slice(1) : baseDefaults(base) + to;
var relativePath = (base, path) => {
base = unescape(baseDefaults(base));
path = unescape(path);
return !base || !path.toLowerCase().indexOf(base.toLowerCase()) ? path.slice(base.length) || "/" : "~" + path;
};
var unescape = (str) => {
try {
return str.includes("%") ? decodeURI(str) : str;
} catch (_e) {
return str;
}
};
var sanitizeSearch = (search) => unescape(search[0] === "?" ? search.slice(1) : search);
//#endregion
//#region node_modules/wouter/src/index.js
var defaultRouter = {
hook: useBrowserLocation,
searchHook: useSearch$1,
parser: parse,
base: "",
ssrPath: void 0,
ssrSearch: void 0,
ssrContext: void 0,
hrefs: (x) => x,
aroundNav: (n, t, o) => n(t, o)
};
var RouterCtx = (0, import_react.createContext)(defaultRouter);
var useRouter = () => (0, import_react.useContext)(RouterCtx);
/**
* Parameters context. Used by `useParams()` to get the
* matched params from the innermost `Route` component.
*/
var Params0 = {};
var ParamsCtx = (0, import_react.createContext)(Params0);
var useParams = () => (0, import_react.useContext)(ParamsCtx);
var usePathnameFromRouter = (router, base = router.base) => relativePath(base, router.hook(router)[0]);
var useLocationFromRouter = (router) => {
const [location, navigate] = router.hook(router);
return [relativePath(router.base, location), useEvent((to, opts) => router.aroundNav(navigate, absolutePath(to, router.base), opts))];
};
var useLocation = () => useLocationFromRouter(useRouter());
var useSearch = () => {
const router = useRouter();
return sanitizeSearch(router.searchHook(router));
};
var matchRoute = (parser, route, path, loose) => {
const { pattern, keys } = route instanceof RegExp ? { pattern: route } : parser(route || "*", loose);
const result = pattern.exec(path);
if (!result) return [false, null];
const params = {};
for (let i = 1; i < result.length; i++) params[i - 1] = result[i];
if (keys) for (let i = 0; i < keys.length; i++) params[keys[i]] = result[i + 1];
else Object.assign(params, result.groups);
return loose ? [
true,
params,
result[0]
] : [true, params];
};
var useRoute = (pattern) => {
const router = useRouter();
const absolute = /^~\//.test(pattern);
return matchRoute(router.parser, absolute ? pattern.slice(1) : pattern, usePathnameFromRouter(router, absolute ? "" : router.base));
};
var Router = ({ children, ...props }) => {
const parent_ = useRouter();
const parent = props.hook ? defaultRouter : parent_;
let value = parent;
if (props.ssrPath) {
const [path, search = props.ssrSearch ?? ""] = props.ssrPath.split("?");
if (path) props.ssrSearch = search, props.ssrPath = path;
}
props.hrefs = props.hrefs ?? props.hook?.hrefs;
props.searchHook = props.searchHook ?? props.hook?.searchHook;
let ref = (0, import_react.useRef)(parent), prev = ref.current, next = prev;
for (let k in parent) {
const option = k === "base" ? parent[k] + (props[k] ?? "") : props[k] ?? parent[k];
if (option !== next[k]) {
if (prev === next) ref.current = next = { ...next };
next[k] = option;
}
if (option !== parent[k] || option !== value[k]) value = next;
}
return (0, import_react.createElement)(RouterCtx.Provider, { value }, children);
};
var h_route = ({ children, component }, params) => {
if (component) return (0, import_react.createElement)(component, { params });
return typeof children === "function" ? children(params) : children;
};
var useCachedParams = (value) => {
let prev = (0, import_react.useRef)(Params0);
const curr = prev.current, keys = Object.keys(value);
return prev.current = keys.length !== Object.keys(curr).length || keys.some((k) => value[k] !== curr[k]) ? value : curr;
};
function useSearchParams() {
const [location, navigate] = useLocation();
const search = useSearch();
const searchParams = (0, import_react.useMemo)(() => new URLSearchParams(search), [search]);
let tempSearchParams = searchParams;
return [searchParams, useEvent((nextInit, options) => {
tempSearchParams = new URLSearchParams(typeof nextInit === "function" ? nextInit(tempSearchParams) : nextInit);
const paramsStr = tempSearchParams.toString();
navigate(location + (paramsStr ? "?" + paramsStr : ""), options);
})];
}
var Route = ({ path, nest, match, ...renderProps }) => {
const router = useRouter();
const location = usePathnameFromRouter(router);
const [matches, routeParams, base] = match ?? matchRoute(router.parser, path, location, nest);
const params = useCachedParams({
...useParams(),
...routeParams
});
if (!matches) return null;
const children = h_route(renderProps, params);
return (0, import_react.createElement)(ParamsCtx.Provider, { value: params }, base ? (0, import_react.createElement)(Router, { base }, children) : children);
};
var Link = (0, import_react.forwardRef)((props, ref) => {
const router = useRouter();
const [currentPath, navigate] = useLocationFromRouter(router);
const { to = "", href: targetPath = to, onClick: _onClick, asChild, children, className: cls, replace, state, transition, ...linkProps } = props;
linkProps.onClick = useEvent((event) => {
if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey || event.button !== 0) return;
_onClick?.(event);
if (!event.defaultPrevented) {
event.preventDefault();
navigate(targetPath, props);
}
});
linkProps.href = router.hrefs(targetPath[0] === "~" ? targetPath.slice(1) : router.base + targetPath, router);
if (cls !== void 0) linkProps.className = cls?.call ? cls(currentPath === targetPath) : cls;
if (ref) linkProps.ref = ref;
return asChild && (0, import_react.isValidElement)(children) ? (0, import_react.cloneElement)(children, linkProps) : (0, import_react.createElement)("a", linkProps, children);
});
var flattenChildren = (children, result = []) => {
if (Array.isArray(children)) for (const c of children) flattenChildren(c && c.type === import_react.Fragment ? c.props.children : c, result);
else result.push(children);
return result;
};
var Switch = ({ children, location }) => {
const router = useRouter();
const originalLocation = usePathnameFromRouter(router);
for (const element of flattenChildren(children)) {
let match = 0;
if ((0, import_react.isValidElement)(element) && (match = matchRoute(router.parser, element.props.path, location || originalLocation, element.props.nest))[0]) return (0, import_react.cloneElement)(element, { match });
}
return null;
};
var Redirect = (props) => {
const { to, href = to } = props;
const router = useRouter();
const [, navigate] = useLocationFromRouter(router);
const redirect = useEvent(() => navigate(to || href, props));
const { ssrContext } = router;
useIsomorphicLayoutEffect(() => {
redirect();
}, []);
if (ssrContext) ssrContext.redirectTo = to;
return null;
};
//#endregion
export { Link, Redirect, Route, Router, Switch, matchRoute, useLocation, useParams, useRoute, useRouter, useSearch, useSearchParams };
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"wouter.js","names":["\"useInsertion\" + \"Effect\"","useSyncExternalStore","useSearch","useBrowserSearch","parsePattern","createContext","useContext","useRef","h","useMemo","forwardRef","isValidElement","cloneElement","Fragment"],"sources":["../../regexparam/dist/index.mjs","../../wouter/src/use-sync-external-store.js","../../wouter/src/react-deps.js","../../wouter/src/use-browser-location.js","../../wouter/src/paths.js","../../wouter/src/index.js"],"sourcesContent":["/**\n * @param {string|RegExp} input The route pattern\n * @param {boolean} [loose] Allow open-ended matching. Ignored with `RegExp` input.\n */\nexport function parse(input, loose) {\n\tif (input instanceof RegExp) return { keys:false, pattern:input };\n\tvar c, o, tmp, ext, keys=[], pattern='', arr = input.split('/');\n\tarr[0] || arr.shift();\n\n\twhile (tmp = arr.shift()) {\n\t\tc = tmp[0];\n\t\tif (c === '*') {\n\t\t\tkeys.push(c);\n\t\t\tpattern += tmp[1] === '?' ? '(?:/(.*))?' : '/(.*)';\n\t\t} else if (c === ':') {\n\t\t\to = tmp.indexOf('?', 1);\n\t\t\text = tmp.indexOf('.', 1);\n\t\t\tkeys.push( tmp.substring(1, !!~o ? o : !!~ext ? ext : tmp.length) );\n\t\t\tpattern += !!~o && !~ext ? '(?:/([^/]+?))?' : '/([^/]+?)';\n\t\t\tif (!!~ext) pattern += (!!~o ? '?' : '') + '\\\\' + tmp.substring(ext);\n\t\t} else {\n\t\t\tpattern += '/' + tmp;\n\t\t}\n\t}\n\n\treturn {\n\t\tkeys: keys,\n\t\tpattern: new RegExp('^' + pattern + (loose ? '(?=$|\\/)' : '\\/?$'), 'i')\n\t};\n}\n\nvar RGX = /(\\/|^)([:*][^/]*?)(\\?)?(?=[/.]|$)/g;\n\n// error if key missing?\nexport function inject(route, values) {\n\treturn route.replace(RGX, (x, lead, key, optional) => {\n\t\tx = values[key=='*' ? key : key.substring(1)];\n\t\treturn x ? '/'+x : (optional || key=='*') ? '' : '/' + key;\n\t});\n}\n","export { useSyncExternalStore } from \"use-sync-external-store/shim/index.js\";\n","import * as React from \"react\";\n\n// React.useInsertionEffect is not available in React <18\n// This hack fixes a transpilation issue on some apps\nconst useBuiltinInsertionEffect = React[\"useInsertion\" + \"Effect\"];\n\nexport {\n useMemo,\n useRef,\n useState,\n useContext,\n createContext,\n isValidElement,\n cloneElement,\n createElement,\n Fragment,\n forwardRef,\n} from \"react\";\n\n// To resolve webpack 5 errors, while not presenting problems for native,\n// we copy the approaches from https://github.com/TanStack/query/pull/3561\n// and https://github.com/TanStack/query/pull/3601\n// ~ Show this aging PR some love to remove the need for this hack:\n// https://github.com/facebook/react/pull/25231 ~\nexport { useSyncExternalStore } from \"./use-sync-external-store.js\";\n\n// Copied from:\n// https://github.com/facebook/react/blob/main/packages/shared/ExecutionEnvironment.js\nconst canUseDOM = !!(\n typeof window !== \"undefined\" &&\n typeof window.document !== \"undefined\" &&\n typeof window.document.createElement !== \"undefined\"\n);\n\n// Copied from:\n// https://github.com/reduxjs/react-redux/blob/master/src/utils/useIsomorphicLayoutEffect.ts\n// \"React currently throws a warning when using useLayoutEffect on the server.\n// To get around it, we can conditionally useEffect on the server (no-op) and\n// useLayoutEffect in the browser.\"\nexport const useIsomorphicLayoutEffect = canUseDOM\n ? React.useLayoutEffect\n : React.useEffect;\n\n// useInsertionEffect is already a noop on the server.\n// See: https://github.com/facebook/react/blob/main/packages/react-server/src/ReactFizzHooks.js\nexport const useInsertionEffect =\n useBuiltinInsertionEffect || useIsomorphicLayoutEffect;\n\n// Userland polyfill while we wait for the forthcoming\n// https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md\n// Note: \"A high-fidelity polyfill for useEvent is not possible because\n// there is no lifecycle or Hook in React that we can use to switch\n// .current at the right timing.\"\n// So we will have to make do with this \"close enough\" approach for now.\nexport const useEvent = (fn) => {\n const ref = React.useRef([fn, (...args) => ref[0](...args)]).current;\n // Per Dan Abramov: useInsertionEffect executes marginally closer to the\n // correct timing for ref synchronization than useLayoutEffect on React 18.\n // See: https://github.com/facebook/react/pull/25881#issuecomment-1356244360\n useInsertionEffect(() => {\n ref[0] = fn;\n });\n return ref[1];\n};\n","import { useSyncExternalStore } from \"./react-deps.js\";\n\n/**\n * History API docs @see https://developer.mozilla.org/en-US/docs/Web/API/History\n */\nconst events = [\"popstate\", \"pushState\", \"replaceState\", \"hashchange\"];\n\nlet listeners = [];\nconst onLocationChange = () => listeners.forEach((callback) => callback());\n\n// Native events can run microtasks between listeners. Notify all subscribers\n// together so React can process parent and child updates in the same batch.\nconst subscribeToLocationUpdates = (callback) => {\n if (listeners.push(cLine truncated
@@ -1 +0,0 @@
{"version":"5.0.0","results":[[":tests/auth.test.tsx",{"duration":491.67805499999986,"failed":false}],[":tests/api.test.ts",{"duration":14.961915999999974,"failed":false}],[":tests/overlay.test.tsx",{"duration":728.093953,"failed":false}],[":tests/check-tokens.test.mjs",{"duration":998.8133460000001,"failed":false}],[":tests/routing.test.tsx",{"duration":1758.9155749999995,"failed":false}],[":tests/appState.test.tsx",{"duration":423.0312829999998,"failed":false}],[":tests/icon-registry.test.tsx",{"duration":75.86277799999993,"failed":false}],[":tests/useTimeout.test.tsx",{"duration":43.350607999999966,"failed":false}],[":tests/useItems.test.tsx",{"duration":415.8608149999998,"failed":false}],[":tests/itemsPage.test.tsx",{"duration":698.4244530000001,"failed":false}]]}
-10
View File
@@ -1,10 +0,0 @@
(The MIT License)
Copyright (c) 2012 TJ Holowaychuk <tj@vision-media.ca>
Copyright (c) 2022 Jean-Philippe Zolesio <holblin@gmail.com>
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.
-146
View File
@@ -1,146 +0,0 @@
# @adobe/css-tools
> A modern CSS parser and stringifier with TypeScript support
[![npm version](https://badge.fury.io/js/%40adobe%2Fcss-tools.svg)](https://badge.fury.io/js/%40adobe%2Fcss-tools)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
Parse CSS into an Abstract Syntax Tree (AST) and convert it back to CSS with configurable formatting. Built with TypeScript for type safety and modern JavaScript features.
## Install
```bash
npm install @adobe/css-tools
```
## Usage
```js
import { parse, stringify } from '@adobe/css-tools'
// Parse CSS to AST
const ast = parse('body { font-size: 12px; }')
// Stringify AST back to CSS
const css = stringify(ast)
// => "body { font-size: 12px; }"
// Pretty print with custom indentation
const formatted = stringify(ast, { indent: ' ' })
// => "body {\n font-size: 12px;\n}"
// Minify output
const minified = stringify(ast, { compress: true })
// => "body{font-size:12px}"
```
## API
### `parse(code, options?)`
Parses CSS code and returns an Abstract Syntax Tree (AST).
**Parameters:**
- `code` (string) - The CSS code to parse
- `options` (object, optional) - Parsing options
- `silent` (boolean) - Silently fail on parse errors instead of throwing
- `source` (string) - File path for better error reporting
**Returns:** `CssStylesheetAST` - The parsed CSS as an AST
### `stringify(ast, options?)`
Converts a CSS AST back to CSS string with configurable formatting.
**Parameters:**
- `ast` (CssStylesheetAST) - The CSS AST to stringify
- `options` (object, optional) - Stringification options
- `indent` (string) - Indentation string (default: `' '`)
- `compress` (boolean) - Whether to compress/minify the output (default: `false`)
**Returns:** `string` - The formatted CSS string
## Features
- **Complete CSS Support**: All standard CSS features including selectors, properties, values, at-rules, and comments
- **TypeScript Support**: Full type definitions for all AST nodes and functions
- **Error Handling**: Configurable error handling with detailed position information
- **Formatting Options**: Pretty print, minify, or custom formatting
- **Performance Optimized**: Efficient parsing and stringification for large CSS files
- **Source Maps**: Track original source positions for debugging and tooling
### Supported CSS Features
- **Selectors**: Element, class, ID, attribute, pseudo-class, pseudo-element selectors
- **Properties**: All standard CSS properties and custom properties
- **Values**: Colors, lengths, percentages, functions, calc(), etc.
- **At-rules**: @media, @keyframes, @import, @charset, @namespace, @font-face, @page, @document, @supports, @container, @layer, @starting-style, @host, @custom-media
- **Comments**: Both /* */ and // comments
- **Whitespace**: Preserves formatting information
- **Vendor prefixes**: Supports vendor-prefixed at-rules and properties
- **Nested rules**: Media queries, supports, containers, etc.
- **Complex selectors**: Combinators, pseudo-selectors, attribute selectors
## Examples
### Error Handling
```js
import { parse } from '@adobe/css-tools'
const malformedCss = `
body { color: red; }
{ color: blue; } /* Missing selector */
.valid { background: green; }
`
// Parse with silent error handling
const result = parse(malformedCss, { silent: true })
// Check for parsing errors
if (result.stylesheet.parsingErrors) {
console.log('Parsing errors:', result.stylesheet.parsingErrors.length)
result.stylesheet.parsingErrors.forEach(error => {
console.log(`Error at line ${error.line}: ${error.message}`)
})
}
// Valid rules are still parsed
console.log('Valid rules:', result.stylesheet.rules.length)
```
### Source Tracking
```js
import { parse } from '@adobe/css-tools'
const css = 'body { color: red; }'
const ast = parse(css, { source: 'styles.css' })
// Position information is available
const rule = ast.stylesheet.rules[0]
console.log(rule.position?.source) // "styles.css"
console.log(rule.position?.start) // { line: 1, column: 1 }
console.log(rule.position?.end) // { line: 1, column: 20 }
```
For more examples, see the [Examples documentation](docs/EXAMPLES.md).
## Performance
The library is optimized for performance and can handle large CSS files efficiently. For benchmarking information, see the `benchmark/` directory in the source code.
## Documentation
- [API Reference](docs/API.md) - Complete API documentation
- [AST Structure](docs/AST.md) - Detailed AST node types and structure
- [Examples](docs/EXAMPLES.md) - Comprehensive usage examples
- [Changelog](docs/CHANGELOG.md) - Version history and changes
## Background
This is a fork of the npm `css` package, maintained by Adobe with modern improvements including TypeScript support, enhanced performance, and security updates. It provides a robust foundation for CSS tooling, preprocessing, and analysis.
## License
[MIT](LICENSE)
-2
View File
@@ -1,2 +0,0 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});class t extends Error{reason;filename;line;column;source;constructor(t,s,e,i,n){super(`${t}:${e}:${i}: ${s}`),this.reason=s,this.filename=t,this.line=e,this.column=i,this.source=n}}class s{start;end;source;constructor(t,s,e){this.start=t,this.end=s,this.source=e}}var e;exports.CssTypes=void 0,(e=exports.CssTypes||(exports.CssTypes={})).stylesheet="stylesheet",e.rule="rule",e.declaration="declaration",e.comment="comment",e.atRule="at-rule",e.container="container",e.charset="charset",e.counterStyle="counter-style",e.document="document",e.customMedia="custom-media",e.fontFace="font-face",e.fontFeatureValues="font-feature-values",e.host="host",e.import="import",e.keyframes="keyframes",e.keyframe="keyframe",e.layer="layer",e.media="media",e.namespace="namespace",e.page="page",e.pageMarginBox="page-margin-box",e.positionTry="position-try",e.property="property",e.scope="scope",e.startingStyle="starting-style",e.supports="supports",e.viewTransition="view-transition";const i=(t,s,e)=>{let i=e,n=1e4;do{const e=s.map(s=>t.indexOf(s,i));e.push(t.indexOf("\\",i));const r=e.filter(t=>-1!==t);if(0===r.length)return-1;const o=Math.min(...r);if("\\"!==t[o])return o;i=o+2,n--}while(n>0);throw new Error("Too many escaping")},n=(t,s,e)=>{let r=e,o=1e4;do{const e=s.map(s=>t.indexOf(s,r));e.push(t.indexOf("(",r)),e.push(t.indexOf('"',r)),e.push(t.indexOf("'",r)),e.push(t.indexOf("\\",r));const c=e.filter(t=>-1!==t);if(0===c.length)return-1;const p=Math.min(...c);switch(t[p]){case"\\":r=p+2;break;case"(":{const s=n(t,[")"],p+1);if(-1===s)return-1;r=s+1}break;case'"':{const s=i(t,['"'],p+1);if(-1===s)return-1;r=s+1}break;case"'":{const s=i(t,["'"],p+1);if(-1===s)return-1;r=s+1}break;default:return p}o--}while(o>0);throw new Error("Too many escaping")},r=/\/\*[^]*?(?:\*\/|$)/g;function o(t){return t?t.trim():""}function c(t,s){const e=t&&"string"==typeof t.type,i=e?t:s;for(const s in t){const e=t[s];Array.isArray(e)?e.forEach(t=>{c(t,i)}):e&&"object"==typeof e&&c(e,i)}return e&&Object.defineProperty(t,"parent",{configurable:!0,writable:!0,enumerable:!1,value:s||null}),t}class p{level=0;indentation=" ";compress=!1;constructor(t){"string"==typeof t?.indent&&(this.indentation=t?.indent),t?.compress&&(this.compress=!0)}emit(t,s){return t}indent(t){return this.level=this.level||1,t?(this.level+=t,""):Array(this.level).join(this.indentation)}visit(t){switch(t.type){case exports.CssTypes.stylesheet:return this.stylesheet(t);case exports.CssTypes.rule:return this.rule(t);case exports.CssTypes.declaration:return this.declaration(t);case exports.CssTypes.comment:return this.comment(t);case exports.CssTypes.container:return this.container(t);case exports.CssTypes.charset:return this.charset(t);case exports.CssTypes.counterStyle:return this.counterStyle(t);case exports.CssTypes.document:return this.document(t);case exports.CssTypes.customMedia:return this.customMedia(t);case exports.CssTypes.fontFace:return this.fontFace(t);case exports.CssTypes.fontFeatureValues:return this.fontFeatureValues(t);case exports.CssTypes.host:return this.host(t);case exports.CssTypes.import:return this.import(t);case exports.CssTypes.keyframes:return this.keyframes(t);case exports.CssTypes.keyframe:return this.keyframe(t);case exports.CssTypes.layer:return this.layer(t);case exports.CssTypes.media:return this.media(t);case exports.CssTypes.namespace:return this.namespace(t);case exports.CssTypes.page:return this.page(t);case exports.CssTypes.pageMarginBox:return this.pageMarginBox(t);case exports.CssTypes.positionTry:return this.positionTry(t);case exports.CssTypes.property:return this.property(t);case exports.CssTypes.scope:return this.scope(t);case exports.CssTypes.startingStyle:return this.startingStyle(t);case exports.CssTypes.supports:return this.supports(t);case exports.CssTypes.viewTransition:return this.viewTransition(t);case exports.CssTypes.atRule:return this.genericAtRule(t)}}mapVisit(t,s){let e="";s=s||"";for(let i=0,n=t.length;i<n;i++)e+=this.visit(t[i]),s&&i<n-1&&(e+=this.emit(s));return e}compile(t){return this.compress?t.stylesheet.rules.map(this.visit,this).join(""):this.stylesheet(t)}stylesheet(t){return this.mapVisit(t.stylesheet.rules,"\n\n")}comment(t){return this.compress?this.emit("",t.position):this.emit(`${this.indent()}/*${t.comment}*/`,t.position)}container(t){return this.compress?this.emit(`@container ${t.container}`,t.position)+this.emit("{")+this.mapVisit(t.rules)+this.emit("}"):this.emit(`${this.indent()}@container ${t.container}`,t.position)+this.emit(` {\n${this.indent(1)}`)+this.mapVisit(t.rules,"\n\n")+this.emit(`\n${this.indent(-1)}${this.indent()}}`)}layer(t){return this.compress?this.emit(`@layer ${t.layer}`,t.position)+(t.rules?this.emit("{")+this.mapVisit(t.rules)+this.emit("}"):";"):this.emit(`${this.indent()}@layer ${t.layer}`,t.position)+(t.rules?this.emit(` {\n${this.indent(1)}`)+this.mapVisit(t.rules,"\n\n")+this.emit(`\n${this.indent(-1)}${this.indenLine truncated
//# sourceMappingURL=adobe-css-tools.cjs.map
@@ -1 +0,0 @@
{"version":3,"file":"adobe-css-tools.cjs","sources":["../../src/CssParseError.ts","../../src/CssPosition.ts","../../src/type.ts","../../src/utils/stringSearch.ts","../../src/parse/index.ts","../../src/stringify/compiler.ts","../../src/index.ts","../../src/stringify/index.ts"],"sourcesContent":[null,null,null,null,null,null,null,null],"names":["CssParseError","Error","reason","filename","line","column","source","constructor","msg","lineno","css","super","this","Position","start","end","CssTypes","indexOfArrayNonEscaped","string","search","position","currentPosition","maxLoop","all","map","v","indexOf","push","foundAll","filter","length","found","Math","min","indexOfArrayWithBracketAndQuoteSupport","currentSearchPosition","firstMatchPos","endPosition","endQuotePosition","commentRegex","trim","str","addParent","obj","parent","isNode","type","childParent","k","value","Array","isArray","forEach","Object","defineProperty","configurable","writable","enumerable","Compiler","level","indentation","compress","options","indent","emit","_position","join","visit","node","stylesheet","rule","declaration","comment","container","charset","counterStyle","document","customMedia","fontFace","fontFeatureValues","host","import","keyframes","keyframe","layer","media","namespace","page","pageMarginBox","positionTry","property","scope","startingStyle","supports","viewTransition","atRule","genericAtRule","mapVisit","nodes","delim","buf","i","compile","rules","doc","vendor","name","decls","declarations","values","sel","selectors","fontFamily","prelude","hasNestedRules","some","r","s","split","padEnd","parse","whitespace","errorsList","error","err","silent","open","openMatch","exec","processMatch","close","closeMatch","m","lines","match","lastIndexOf","updatePosition","slice","comments","c","pos","charAt","selector","bracePos","selectorStr","substring","result","index","splitWithBracketAndQuoteSupport","replace","propMatch","propValue","separatorMatch","endValuePosition","ret","endMatch","decl","semiPos","looksLikeNestedRule","closePos","ruleBody","items","ar","nestedR","rulesOrDeclarations","vals","res","spacesMatch","pageMarginBoxRegex","RegExp","atImport","_compileAtRule","atCharset","atNamespace","re","m1","m2","frames","frame","concat","atKeyframes","style","atMedia","atCustomMedia","atSupports","atDocument","atPage","atHost","atFontFace","atFontFeatureValues","atContainer","atStartingStyle","atLayer","atProperty","atCounterStyle","atScope","atViewTransition","atPositionTry","atPageMarginBox","preludeEnd","atGeneric","rulesList","parsingErrors","stringify"],"mappings":"oEAAc,MAAOA,UAAsBC,MAChCC,OACAC,SACAC,KACAC,OACAC,OAET,WAAAC,CACEJ,EACAK,EACAC,EACAJ,EACAK,GAEAC,MAAM,GAAGR,KAAYM,KAAUJ,MAAWG,KAC1CI,KAAKV,OAASM,EACdI,KAAKT,SAAWA,EAChBS,KAAKR,KAAOK,EACZG,KAAKP,OAASA,EACdO,KAAKN,OAASI,CAChB,ECjBY,MAAOG,EACnBC,MACAC,IACAT,OAEA,WAAAC,CACEO,EACAC,EACAT,GAEAM,KAAKE,MAAQA,EACbF,KAAKG,IAAMA,EACXH,KAAKN,OAASA,CAChB,ECbF,IAAYU,EAAAA,QAAAA,cAAAA,GAAAA,EAAAA,QAAAA,WAAAA,iBAAQ,CAAA,IAClB,WAAA,aACAA,EAAA,KAAA,OACAA,EAAA,YAAA,cACAA,EAAA,QAAA,UACAA,EAAA,OAAA,UACAA,EAAA,UAAA,YACAA,EAAA,QAAA,UACAA,EAAA,aAAA,gBACAA,EAAA,SAAA,WACAA,EAAA,YAAA,eACAA,EAAA,SAAA,YACAA,EAAA,kBAAA,sBACAA,EAAA,KAAA,OACAA,EAAA,OAAA,SACAA,EAAA,UAAA,YACAA,EAAA,SAAA,WACAA,EAAA,MAAA,QACAA,EAAA,MAAA,QACAA,EAAA,UAAA,YACAA,EAAA,KAAA,OACAA,EAAA,cAAA,kBACAA,EAAA,YAAA,eACAA,EAAA,SAAA,WACAA,EAAA,MAAA,QACAA,EAAA,cAAA,iBACAA,EAAA,SAAA,WACAA,EAAA,eAAA,kBC9BK,MAkBMC,EAAyB,CACpCC,EACAC,EACAC,KAEA,IAAIC,EAAkBD,EAClBE,EAxBkB,IAyBtB,EAAG,CACD,MAAMC,EAAMJ,EAAOK,IAAKC,GAAMP,EAAOQ,QAAQD,EAAGJ,IAChDE,EAAII,KAAKT,EAAOQ,QAAQ,KAAML,IAC9B,MAAMO,EAAWL,EAAIM,OAAQJ,QAAMA,GACnC,GAAwB,IAApBG,EAASE,OACX,OAAO,EAGT,MAAMC,EAAQC,KAAKC,OAAOL,GAC1B,GAAsB,OAAlBV,EAAOa,GAIT,OAAOA,EAHPV,EAAkBU,EAAQ,EAC1BT,GAIJ,OAASA,EAAU,GAEnB,MAAM,IAAIrB,MAAM,sBA0BLiC,EAAyC,CACpDhB,EACAC,EACAC,KAEA,IAAIe,EAAwBf,EACxBE,EA1EkB,IA4EtB,EAAG,CACD,MAAMC,EAAMJ,EAAOK,IAAKC,GAAMP,EAAOQ,QAAQD,EAAGU,IAEhDZ,EAAII,KAAKT,EAAOQ,QAAQ,IAAKS,IAC7BZ,EAAII,KAAKT,EAAOQ,QAAQ,IAAKS,IAC7BZ,EAAII,KAAKT,EAAOQ,QAAQ,IAAKS,IAC7BZ,EAAII,KAAKT,EAAOQ,QAAQ,KAAMS,IAE9B,MAAMP,EAAWL,EAAIM,OAAQJ,QAAMA,GACnC,GAAwB,IAApBG,EAASE,OACX,OAAO,EAGT,MAAMM,EAAgBJ,KAAKC,OAAOL,GAElC,OADaV,EAAOkB,IAElB,IAAK,KACHD,EAAwBC,EAAgB,EACxC,MACF,IAAK,IACH,CACE,MAAMC,EAAcH,EAClBhB,EACA,CAAC,KACDkB,EAAgB,GAElB,IAAoB,IAAhBC,EACF,OAAO,EAETF,EAAwBE,EAAc,CACxC,CACA,MACF,IAAK,IACH,CACE,MAAMC,EAAmBrB,EACvBC,EACA,CAAC,KACDkB,EAAgB,GAElB,IAAyB,IAArBE,EACF,OAAO,EAETH,EAAwBG,EAAmB,CAC7C,CACA,MACF,IAAK,IACH,CACE,MAAMA,EAAmBrB,EACvBC,EACA,CAAC,KACDkB,EAAgB,GAElB,IAAyB,IAArBE,EACF,OAAO,EAETH,EAAwBG,EAAmB,CAC7C,CACA,MACF,QACE,OAAOF,EAEXd,GACF,OAASA,EAAU,GAEnB,MAAM,IAAIrB,MAAM,sBCjGZsC,EAAe,uBAotCrB,SAASC,EAAKC,GACZ,OAAOA,EAAMA,EAAID,OAAS,EAC5B,CAKA,SAASE,EACPC,EACAC,GAEA,MAAMC,EAASF,GAA2B,iBAAbA,EAAIG,KAC3BC,EAAcF,EAASF,EAAMC,EAEnC,IAAK,MAAMI,KAAKL,EAAK,CACnB,MAAMM,EAAQN,EAAIK,GACdE,MAAMC,QAAQF,GAChBA,EAAMG,QAAS3B,IACbiB,EAAUjB,EAAGsB,KAENE,GAA0B,iBAAVA,GACzBP,EAAUO,EAAOF,EAErB,CAWA,OATIF,GACFQ,OAAOC,eAALine truncated
-224
View File
@@ -1,224 +0,0 @@
declare class CssParseError extends Error {
readonly reason: string;
readonly filename?: string;
readonly line: number;
readonly column: number;
readonly source: string;
constructor(filename: string, msg: string, lineno: number, column: number, css: string);
}
/**
* Store position information for a node
*/
declare class Position {
start: {
line: number;
column: number;
};
end: {
line: number;
column: number;
};
source?: string;
constructor(start: {
line: number;
column: number;
}, end: {
line: number;
column: number;
}, source: string);
}
declare enum CssTypes {
stylesheet = "stylesheet",
rule = "rule",
declaration = "declaration",
comment = "comment",
atRule = "at-rule",
container = "container",
charset = "charset",
counterStyle = "counter-style",
document = "document",
customMedia = "custom-media",
fontFace = "font-face",
fontFeatureValues = "font-feature-values",
host = "host",
import = "import",
keyframes = "keyframes",
keyframe = "keyframe",
layer = "layer",
media = "media",
namespace = "namespace",
page = "page",
pageMarginBox = "page-margin-box",
positionTry = "position-try",
property = "property",
scope = "scope",
startingStyle = "starting-style",
supports = "supports",
viewTransition = "view-transition"
}
type CssCommonAST = {
type: CssTypes;
};
type CssCommonPositionAST = CssCommonAST & {
position?: Position;
parent?: unknown;
};
type CssStylesheetAST = CssCommonAST & {
type: CssTypes.stylesheet;
stylesheet: {
source?: string;
rules: Array<CssAtRuleAST>;
parsingErrors?: Array<CssParseError>;
};
};
type CssRuleAST = CssCommonPositionAST & {
type: CssTypes.rule;
selectors: Array<string>;
declarations: Array<CssDeclarationAST | CssCommentAST | CssAtRuleAST>;
};
type CssDeclarationAST = CssCommonPositionAST & {
type: CssTypes.declaration;
property: string;
value: string;
};
type CssCommentAST = CssCommonPositionAST & {
type: CssTypes.comment;
comment: string;
};
type CssContainerAST = CssCommonPositionAST & {
type: CssTypes.container;
container: string;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssCharsetAST = CssCommonPositionAST & {
type: CssTypes.charset;
charset: string;
};
type CssCustomMediaAST = CssCommonPositionAST & {
type: CssTypes.customMedia;
name: string;
media: string;
};
type CssDocumentAST = CssCommonPositionAST & {
type: CssTypes.document;
document: string;
vendor?: string;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssFontFaceAST = CssCommonPositionAST & {
type: CssTypes.fontFace;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssHostAST = CssCommonPositionAST & {
type: CssTypes.host;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssImportAST = CssCommonPositionAST & {
type: CssTypes.import;
import: string;
};
type CssKeyframesAST = CssCommonPositionAST & {
type: CssTypes.keyframes;
name: string;
vendor?: string;
keyframes: Array<CssKeyframeAST | CssCommentAST>;
};
type CssKeyframeAST = CssCommonPositionAST & {
type: CssTypes.keyframe;
values: Array<string>;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssLayerAST = CssCommonPositionAST & {
type: CssTypes.layer;
layer: string;
rules?: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssMediaAST = CssCommonPositionAST & {
type: CssTypes.media;
media: string;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssNamespaceAST = CssCommonPositionAST & {
type: CssTypes.namespace;
namespace: string;
};
type CssPageAST = CssCommonPositionAST & {
type: CssTypes.page;
selectors: Array<string>;
declarations: Array<CssDeclarationAST | CssCommentAST | CssAtRuleAST>;
};
type CssSupportsAST = CssCommonPositionAST & {
type: CssTypes.supports;
supports: string;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssStartingStyleAST = CssCommonPositionAST & {
type: CssTypes.startingStyle;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssCounterStyleAST = CssCommonPositionAST & {
type: CssTypes.counterStyle;
name: string;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssFontFeatureValuesAST = CssCommonPositionAST & {
type: CssTypes.fontFeatureValues;
fontFamily: string;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssPositionTryAST = CssCommonPositionAST & {
type: CssTypes.positionTry;
name: string;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssPropertyAST = CssCommonPositionAST & {
type: CssTypes.property;
name: string;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssScopeAST = CssCommonPositionAST & {
type: CssTypes.scope;
scope: string;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssViewTransitionAST = CssCommonPositionAST & {
type: CssTypes.viewTransition;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssPageMarginBoxAST = CssCommonPositionAST & {
type: CssTypes.pageMarginBox;
name: string;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssGenericAtRuleAST = CssCommonPositionAST & {
type: CssTypes.atRule;
name: string;
prelude: string;
rules?: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssAtRuleAST = CssRuleAST | CssCommentAST | CssContainerAST | CssCharsetAST | CssCounterStyleAST | CssCustomMediaAST | CssDocumentAST | CssFontFaceAST | CssFontFeatureValuesAST | CssHostAST | CssImportAST | CssKeyframesAST | CssLayerAST | CssMediaAST | CssNamespaceAST | CssPageAST | CssPageMarginBoxAST | CssPositionTryAST | CssPropertyAST | CssScopeAST | CssSupportsAST | CssStartingStyleAST | CssViewTransitionAST | CssGenericAtRuleAST;
type CssAllNodesAST = CssAtRuleAST | CssStylesheetAST | CssDeclarationAST | CssKeyframeAST;
type CompilerOptions = {
indent?: string;
compress?: boolean;
};
declare const parse: (css: string, options?: {
source?: string;
silent?: boolean;
}) => CssStylesheetAST;
declare const stringify: (node: CssStylesheetAST, options?: CompilerOptions) => string;
declare const _default: {
parse: (css: string, options?: {
source?: string;
silent?: boolean;
}) => CssStylesheetAST;
stringify: (node: CssStylesheetAST, options?: CompilerOptions) => string;
};
export { CssTypes, _default as default, parse, stringify };
export type { CssAllNodesAST, CssAtRuleAST, CssCharsetAST, CssCommentAST, CssCommonAST, CssCommonPositionAST, CssContainerAST, CssCounterStyleAST, CssCustomMediaAST, CssDeclarationAST, CssDocumentAST, CssFontFaceAST, CssFontFeatureValuesAST, CssGenericAtRuleAST, CssHostAST, CssImportAST, CssKeyframeAST, CssKeyframesAST, CssLayerAST, CssMediaAST, CssNamespaceAST, CssPageAST, CssPageMarginBoxAST, CssPositionTryAST, CssPropertyAST, CssRuleAST, CssScopeAST, CssStartingStyleAST, CssStylesheetAST, CssSupportsAST, CssViewTransitionAST };
-224
View File
@@ -1,224 +0,0 @@
declare class CssParseError extends Error {
readonly reason: string;
readonly filename?: string;
readonly line: number;
readonly column: number;
readonly source: string;
constructor(filename: string, msg: string, lineno: number, column: number, css: string);
}
/**
* Store position information for a node
*/
declare class Position {
start: {
line: number;
column: number;
};
end: {
line: number;
column: number;
};
source?: string;
constructor(start: {
line: number;
column: number;
}, end: {
line: number;
column: number;
}, source: string);
}
declare enum CssTypes {
stylesheet = "stylesheet",
rule = "rule",
declaration = "declaration",
comment = "comment",
atRule = "at-rule",
container = "container",
charset = "charset",
counterStyle = "counter-style",
document = "document",
customMedia = "custom-media",
fontFace = "font-face",
fontFeatureValues = "font-feature-values",
host = "host",
import = "import",
keyframes = "keyframes",
keyframe = "keyframe",
layer = "layer",
media = "media",
namespace = "namespace",
page = "page",
pageMarginBox = "page-margin-box",
positionTry = "position-try",
property = "property",
scope = "scope",
startingStyle = "starting-style",
supports = "supports",
viewTransition = "view-transition"
}
type CssCommonAST = {
type: CssTypes;
};
type CssCommonPositionAST = CssCommonAST & {
position?: Position;
parent?: unknown;
};
type CssStylesheetAST = CssCommonAST & {
type: CssTypes.stylesheet;
stylesheet: {
source?: string;
rules: Array<CssAtRuleAST>;
parsingErrors?: Array<CssParseError>;
};
};
type CssRuleAST = CssCommonPositionAST & {
type: CssTypes.rule;
selectors: Array<string>;
declarations: Array<CssDeclarationAST | CssCommentAST | CssAtRuleAST>;
};
type CssDeclarationAST = CssCommonPositionAST & {
type: CssTypes.declaration;
property: string;
value: string;
};
type CssCommentAST = CssCommonPositionAST & {
type: CssTypes.comment;
comment: string;
};
type CssContainerAST = CssCommonPositionAST & {
type: CssTypes.container;
container: string;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssCharsetAST = CssCommonPositionAST & {
type: CssTypes.charset;
charset: string;
};
type CssCustomMediaAST = CssCommonPositionAST & {
type: CssTypes.customMedia;
name: string;
media: string;
};
type CssDocumentAST = CssCommonPositionAST & {
type: CssTypes.document;
document: string;
vendor?: string;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssFontFaceAST = CssCommonPositionAST & {
type: CssTypes.fontFace;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssHostAST = CssCommonPositionAST & {
type: CssTypes.host;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssImportAST = CssCommonPositionAST & {
type: CssTypes.import;
import: string;
};
type CssKeyframesAST = CssCommonPositionAST & {
type: CssTypes.keyframes;
name: string;
vendor?: string;
keyframes: Array<CssKeyframeAST | CssCommentAST>;
};
type CssKeyframeAST = CssCommonPositionAST & {
type: CssTypes.keyframe;
values: Array<string>;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssLayerAST = CssCommonPositionAST & {
type: CssTypes.layer;
layer: string;
rules?: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssMediaAST = CssCommonPositionAST & {
type: CssTypes.media;
media: string;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssNamespaceAST = CssCommonPositionAST & {
type: CssTypes.namespace;
namespace: string;
};
type CssPageAST = CssCommonPositionAST & {
type: CssTypes.page;
selectors: Array<string>;
declarations: Array<CssDeclarationAST | CssCommentAST | CssAtRuleAST>;
};
type CssSupportsAST = CssCommonPositionAST & {
type: CssTypes.supports;
supports: string;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssStartingStyleAST = CssCommonPositionAST & {
type: CssTypes.startingStyle;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssCounterStyleAST = CssCommonPositionAST & {
type: CssTypes.counterStyle;
name: string;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssFontFeatureValuesAST = CssCommonPositionAST & {
type: CssTypes.fontFeatureValues;
fontFamily: string;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssPositionTryAST = CssCommonPositionAST & {
type: CssTypes.positionTry;
name: string;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssPropertyAST = CssCommonPositionAST & {
type: CssTypes.property;
name: string;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssScopeAST = CssCommonPositionAST & {
type: CssTypes.scope;
scope: string;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssViewTransitionAST = CssCommonPositionAST & {
type: CssTypes.viewTransition;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssPageMarginBoxAST = CssCommonPositionAST & {
type: CssTypes.pageMarginBox;
name: string;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssGenericAtRuleAST = CssCommonPositionAST & {
type: CssTypes.atRule;
name: string;
prelude: string;
rules?: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssAtRuleAST = CssRuleAST | CssCommentAST | CssContainerAST | CssCharsetAST | CssCounterStyleAST | CssCustomMediaAST | CssDocumentAST | CssFontFaceAST | CssFontFeatureValuesAST | CssHostAST | CssImportAST | CssKeyframesAST | CssLayerAST | CssMediaAST | CssNamespaceAST | CssPageAST | CssPageMarginBoxAST | CssPositionTryAST | CssPropertyAST | CssScopeAST | CssSupportsAST | CssStartingStyleAST | CssViewTransitionAST | CssGenericAtRuleAST;
type CssAllNodesAST = CssAtRuleAST | CssStylesheetAST | CssDeclarationAST | CssKeyframeAST;
type CompilerOptions = {
indent?: string;
compress?: boolean;
};
declare const parse: (css: string, options?: {
source?: string;
silent?: boolean;
}) => CssStylesheetAST;
declare const stringify: (node: CssStylesheetAST, options?: CompilerOptions) => string;
declare const _default: {
parse: (css: string, options?: {
source?: string;
silent?: boolean;
}) => CssStylesheetAST;
stringify: (node: CssStylesheetAST, options?: CompilerOptions) => string;
};
export { CssTypes, _default as default, parse, stringify };
export type { CssAllNodesAST, CssAtRuleAST, CssCharsetAST, CssCommentAST, CssCommonAST, CssCommonPositionAST, CssContainerAST, CssCounterStyleAST, CssCustomMediaAST, CssDeclarationAST, CssDocumentAST, CssFontFaceAST, CssFontFeatureValuesAST, CssGenericAtRuleAST, CssHostAST, CssImportAST, CssKeyframeAST, CssKeyframesAST, CssLayerAST, CssMediaAST, CssNamespaceAST, CssPageAST, CssPageMarginBoxAST, CssPositionTryAST, CssPropertyAST, CssRuleAST, CssScopeAST, CssStartingStyleAST, CssStylesheetAST, CssSupportsAST, CssViewTransitionAST };
-2
View File
@@ -1,2 +0,0 @@
class t extends Error{reason;filename;line;column;source;constructor(t,i,e,s,n){super(`${t}:${e}:${s}: ${i}`),this.reason=i,this.filename=t,this.line=e,this.column=s,this.source=n}}class i{start;end;source;constructor(t,i,e){this.start=t,this.end=i,this.source=e}}var e;!function(t){t.stylesheet="stylesheet",t.rule="rule",t.declaration="declaration",t.comment="comment",t.atRule="at-rule",t.container="container",t.charset="charset",t.counterStyle="counter-style",t.document="document",t.customMedia="custom-media",t.fontFace="font-face",t.fontFeatureValues="font-feature-values",t.host="host",t.import="import",t.keyframes="keyframes",t.keyframe="keyframe",t.layer="layer",t.media="media",t.namespace="namespace",t.page="page",t.pageMarginBox="page-margin-box",t.positionTry="position-try",t.property="property",t.scope="scope",t.startingStyle="starting-style",t.supports="supports",t.viewTransition="view-transition"}(e||(e={}));const s=(t,i,e)=>{let s=e,n=1e4;do{const e=i.map(i=>t.indexOf(i,s));e.push(t.indexOf("\\",s));const r=e.filter(t=>-1!==t);if(0===r.length)return-1;const o=Math.min(...r);if("\\"!==t[o])return o;s=o+2,n--}while(n>0);throw new Error("Too many escaping")},n=(t,i,e)=>{let r=e,o=1e4;do{const e=i.map(i=>t.indexOf(i,r));e.push(t.indexOf("(",r)),e.push(t.indexOf('"',r)),e.push(t.indexOf("'",r)),e.push(t.indexOf("\\",r));const c=e.filter(t=>-1!==t);if(0===c.length)return-1;const h=Math.min(...c);switch(t[h]){case"\\":r=h+2;break;case"(":{const i=n(t,[")"],h+1);if(-1===i)return-1;r=i+1}break;case'"':{const i=s(t,['"'],h+1);if(-1===i)return-1;r=i+1}break;case"'":{const i=s(t,["'"],h+1);if(-1===i)return-1;r=i+1}break;default:return h}o--}while(o>0);throw new Error("Too many escaping")},r=/\/\*[^]*?(?:\*\/|$)/g;function o(t){return t?t.trim():""}function c(t,i){const e=t&&"string"==typeof t.type,s=e?t:i;for(const i in t){const e=t[i];Array.isArray(e)?e.forEach(t=>{c(t,s)}):e&&"object"==typeof e&&c(e,s)}return e&&Object.defineProperty(t,"parent",{configurable:!0,writable:!0,enumerable:!1,value:i||null}),t}class h{level=0;indentation=" ";compress=!1;constructor(t){"string"==typeof t?.indent&&(this.indentation=t?.indent),t?.compress&&(this.compress=!0)}emit(t,i){return t}indent(t){return this.level=this.level||1,t?(this.level+=t,""):Array(this.level).join(this.indentation)}visit(t){switch(t.type){case e.stylesheet:return this.stylesheet(t);case e.rule:return this.rule(t);case e.declaration:return this.declaration(t);case e.comment:return this.comment(t);case e.container:return this.container(t);case e.charset:return this.charset(t);case e.counterStyle:return this.counterStyle(t);case e.document:return this.document(t);case e.customMedia:return this.customMedia(t);case e.fontFace:return this.fontFace(t);case e.fontFeatureValues:return this.fontFeatureValues(t);case e.host:return this.host(t);case e.import:return this.import(t);case e.keyframes:return this.keyframes(t);case e.keyframe:return this.keyframe(t);case e.layer:return this.layer(t);case e.media:return this.media(t);case e.namespace:return this.namespace(t);case e.page:return this.page(t);case e.pageMarginBox:return this.pageMarginBox(t);case e.positionTry:return this.positionTry(t);case e.property:return this.property(t);case e.scope:return this.scope(t);case e.startingStyle:return this.startingStyle(t);case e.supports:return this.supports(t);case e.viewTransition:return this.viewTransition(t);case e.atRule:return this.genericAtRule(t)}}mapVisit(t,i){let e="";i=i||"";for(let s=0,n=t.length;s<n;s++)e+=this.visit(t[s]),i&&s<n-1&&(e+=this.emit(i));return e}compile(t){return this.compress?t.stylesheet.rules.map(this.visit,this).join(""):this.stylesheet(t)}stylesheet(t){return this.mapVisit(t.stylesheet.rules,"\n\n")}comment(t){return this.compress?this.emit("",t.position):this.emit(`${this.indent()}/*${t.comment}*/`,t.position)}container(t){return this.compress?this.emit(`@container ${t.container}`,t.position)+this.emit("{")+this.mapVisit(t.rules)+this.emit("}"):this.emit(`${this.indent()}@container ${t.container}`,t.position)+this.emit(` {\n${this.indent(1)}`)+this.mapVisit(t.rules,"\n\n")+this.emit(`\n${this.indent(-1)}${this.indent()}}`)}layer(t){return this.compress?this.emit(`@layer ${t.layer}`,t.position)+(t.rules?this.emit("{")+this.mapVisit(t.rules)+this.emit("}"):";"):this.emit(`${this.indent()}@layer ${t.layer}`,t.position)+(t.rules?this.emit(` {\n${this.indent(1)}`)+this.mapVisit(t.rules,"\n\n")+this.emit(`\n${this.indent(-1)}${this.indent()}}`):";")}import(t){return this.emit(`@import ${t.import};`,t.position)}media(t){return this.compress?this.emit(`@media ${t.media}`,t.position)+this.emit("{")+this.mapVisit(t.rules)+this.emit("}"):this.emit(`${this.indent()}@media ${t.media}`,t.position)+this.emit(` {\n${this.indent(1)}`)+this.mapVisit(t.rules,"\n\n")+this.emit(`\n${this.indent(-1)}${this.indent()}}`)}document(t){const i=`@${t.vendor||""}document ${t.document}`;return this.compress?this.emit(i,t.position)+this.emit("{")+this.mapVisit(t.rulLine truncated
//# sourceMappingURL=adobe-css-tools.mjs.map
@@ -1 +0,0 @@
{"version":3,"file":"adobe-css-tools.mjs","sources":["../../src/CssParseError.ts","../../src/CssPosition.ts","../../src/type.ts","../../src/utils/stringSearch.ts","../../src/parse/index.ts","../../src/stringify/compiler.ts","../../src/index.ts","../../src/stringify/index.ts"],"sourcesContent":[null,null,null,null,null,null,null,null],"names":["CssParseError","Error","reason","filename","line","column","source","constructor","msg","lineno","css","super","this","Position","start","end","CssTypes","indexOfArrayNonEscaped","string","search","position","currentPosition","maxLoop","all","map","v","indexOf","push","foundAll","filter","length","found","Math","min","indexOfArrayWithBracketAndQuoteSupport","currentSearchPosition","firstMatchPos","endPosition","endQuotePosition","commentRegex","trim","str","addParent","obj","parent","isNode","type","childParent","k","value","Array","isArray","forEach","Object","defineProperty","configurable","writable","enumerable","Compiler","level","indentation","compress","options","indent","emit","_position","join","visit","node","stylesheet","rule","declaration","comment","container","charset","counterStyle","document","customMedia","fontFace","fontFeatureValues","host","import","keyframes","keyframe","layer","media","namespace","page","pageMarginBox","positionTry","property","scope","startingStyle","supports","viewTransition","atRule","genericAtRule","mapVisit","nodes","delim","buf","i","compile","rules","doc","vendor","name","decls","declarations","values","sel","selectors","fontFamily","prelude","hasNestedRules","some","r","s","split","padEnd","parse","whitespace","errorsList","error","err","silent","open","openMatch","exec","processMatch","close","closeMatch","m","lines","match","lastIndexOf","updatePosition","slice","comments","c","pos","charAt","selector","bracePos","selectorStr","substring","result","index","splitWithBracketAndQuoteSupport","replace","propMatch","propValue","separatorMatch","endValuePosition","ret","endMatch","decl","semiPos","looksLikeNestedRule","closePos","ruleBody","items","ar","nestedR","rulesOrDeclarations","vals","res","spacesMatch","pageMarginBoxRegex","RegExp","atImport","_compileAtRule","atCharset","atNamespace","re","m1","m2","frames","frame","concat","atKeyframes","style","atMedia","atCustomMedia","atSupports","atDocument","atPage","atHost","atFontFace","atFontFeatureValues","atContainer","atStartingStyle","atLayer","atProperty","atCounterStyle","atScope","atViewTransition","atPositionTry","atPageMarginBox","preludeEnd","atGeneric","rulesList","parsingErrors","stringify"],"mappings":"AAAc,MAAOA,UAAsBC,MAChCC,OACAC,SACAC,KACAC,OACAC,OAET,WAAAC,CACEJ,EACAK,EACAC,EACAJ,EACAK,GAEAC,MAAM,GAAGR,KAAYM,KAAUJ,MAAWG,KAC1CI,KAAKV,OAASM,EACdI,KAAKT,SAAWA,EAChBS,KAAKR,KAAOK,EACZG,KAAKP,OAASA,EACdO,KAAKN,OAASI,CAChB,ECjBY,MAAOG,EACnBC,MACAC,IACAT,OAEA,WAAAC,CACEO,EACAC,EACAT,GAEAM,KAAKE,MAAQA,EACbF,KAAKG,IAAMA,EACXH,KAAKN,OAASA,CAChB,MCbUU,GAAZ,SAAYA,GACVA,EAAA,WAAA,aACAA,EAAA,KAAA,OACAA,EAAA,YAAA,cACAA,EAAA,QAAA,UACAA,EAAA,OAAA,UACAA,EAAA,UAAA,YACAA,EAAA,QAAA,UACAA,EAAA,aAAA,gBACAA,EAAA,SAAA,WACAA,EAAA,YAAA,eACAA,EAAA,SAAA,YACAA,EAAA,kBAAA,sBACAA,EAAA,KAAA,OACAA,EAAA,OAAA,SACAA,EAAA,UAAA,YACAA,EAAA,SAAA,WACAA,EAAA,MAAA,QACAA,EAAA,MAAA,QACAA,EAAA,UAAA,YACAA,EAAA,KAAA,OACAA,EAAA,cAAA,kBACAA,EAAA,YAAA,eACAA,EAAA,SAAA,WACAA,EAAA,MAAA,QACAA,EAAA,cAAA,iBACAA,EAAA,SAAA,WACAA,EAAA,eAAA,iBACD,CA5BD,CAAYA,IAAAA,EAAQ,CAAA,ICHb,MAkBMC,EAAyB,CACpCC,EACAC,EACAC,KAEA,IAAIC,EAAkBD,EAClBE,EAxBkB,IAyBtB,EAAG,CACD,MAAMC,EAAMJ,EAAOK,IAAKC,GAAMP,EAAOQ,QAAQD,EAAGJ,IAChDE,EAAII,KAAKT,EAAOQ,QAAQ,KAAML,IAC9B,MAAMO,EAAWL,EAAIM,OAAQJ,QAAMA,GACnC,GAAwB,IAApBG,EAASE,OACX,OAAO,EAGT,MAAMC,EAAQC,KAAKC,OAAOL,GAC1B,GAAsB,OAAlBV,EAAOa,GAIT,OAAOA,EAHPV,EAAkBU,EAAQ,EAC1BT,GAIJ,OAASA,EAAU,GAEnB,MAAM,IAAIrB,MAAM,sBA0BLiC,EAAyC,CACpDhB,EACAC,EACAC,KAEA,IAAIe,EAAwBf,EACxBE,EA1EkB,IA4EtB,EAAG,CACD,MAAMC,EAAMJ,EAAOK,IAAKC,GAAMP,EAAOQ,QAAQD,EAAGU,IAEhDZ,EAAII,KAAKT,EAAOQ,QAAQ,IAAKS,IAC7BZ,EAAII,KAAKT,EAAOQ,QAAQ,IAAKS,IAC7BZ,EAAII,KAAKT,EAAOQ,QAAQ,IAAKS,IAC7BZ,EAAII,KAAKT,EAAOQ,QAAQ,KAAMS,IAE9B,MAAMP,EAAWL,EAAIM,OAAQJ,QAAMA,GACnC,GAAwB,IAApBG,EAASE,OACX,OAAO,EAGT,MAAMM,EAAgBJ,KAAKC,OAAOL,GAElC,OADaV,EAAOkB,IAElB,IAAK,KACHD,EAAwBC,EAAgB,EACxC,MACF,IAAK,IACH,CACE,MAAMC,EAAcH,EAClBhB,EACA,CAAC,KACDkB,EAAgB,GAElB,IAAoB,IAAhBC,EACF,OAAO,EAETF,EAAwBE,EAAc,CACxC,CACA,MACF,IAAK,IACH,CACE,MAAMC,EAAmBrB,EACvBC,EACA,CAAC,KACDkB,EAAgB,GAElB,IAAyB,IAArBE,EACF,OAAO,EAETH,EAAwBG,EAAmB,CAC7C,CACA,MACF,IAAK,IACH,CACE,MAAMA,EAAmBrB,EACvBC,EACA,CAAC,KACDkB,EAAgB,GAElB,IAAyB,IAArBE,EACF,OAAO,EAETH,EAAwBG,EAAmB,CAC7C,CACA,MACF,QACE,OAAOF,EAEXd,GACF,OAASA,EAAU,GAEnB,MAAM,IAAIrB,MAAM,sBCjGZsC,EAAe,uBAotCrB,SAASC,EAAKC,GACZ,OAAOA,EAAMA,EAAID,OAAS,EAC5B,CAKA,SAASE,EACPC,EACAC,GAEA,MAAMC,EAASF,GAA2B,iBAAbA,EAAIG,KAC3BC,EAAcF,EAASF,EAAMC,EAEnC,IAAK,MAAMI,KAAKL,EAAK,CACnB,MAAMM,EAAQN,EAAIK,GACdE,MAAMC,QAAQF,GAChBA,EAAMG,QAAS3B,IACbiB,EAAUjB,EAAGsB,KAENE,GAA0B,iBAAVA,GACzBP,EAAUO,EAAOF,EAErB,CAWA,OATIF,GACFQ,OAAOC,eAAeX,EAAK,SAALine truncated
-224
View File
@@ -1,224 +0,0 @@
declare class CssParseError extends Error {
readonly reason: string;
readonly filename?: string;
readonly line: number;
readonly column: number;
readonly source: string;
constructor(filename: string, msg: string, lineno: number, column: number, css: string);
}
/**
* Store position information for a node
*/
declare class Position {
start: {
line: number;
column: number;
};
end: {
line: number;
column: number;
};
source?: string;
constructor(start: {
line: number;
column: number;
}, end: {
line: number;
column: number;
}, source: string);
}
declare enum CssTypes {
stylesheet = "stylesheet",
rule = "rule",
declaration = "declaration",
comment = "comment",
atRule = "at-rule",
container = "container",
charset = "charset",
counterStyle = "counter-style",
document = "document",
customMedia = "custom-media",
fontFace = "font-face",
fontFeatureValues = "font-feature-values",
host = "host",
import = "import",
keyframes = "keyframes",
keyframe = "keyframe",
layer = "layer",
media = "media",
namespace = "namespace",
page = "page",
pageMarginBox = "page-margin-box",
positionTry = "position-try",
property = "property",
scope = "scope",
startingStyle = "starting-style",
supports = "supports",
viewTransition = "view-transition"
}
type CssCommonAST = {
type: CssTypes;
};
type CssCommonPositionAST = CssCommonAST & {
position?: Position;
parent?: unknown;
};
type CssStylesheetAST = CssCommonAST & {
type: CssTypes.stylesheet;
stylesheet: {
source?: string;
rules: Array<CssAtRuleAST>;
parsingErrors?: Array<CssParseError>;
};
};
type CssRuleAST = CssCommonPositionAST & {
type: CssTypes.rule;
selectors: Array<string>;
declarations: Array<CssDeclarationAST | CssCommentAST | CssAtRuleAST>;
};
type CssDeclarationAST = CssCommonPositionAST & {
type: CssTypes.declaration;
property: string;
value: string;
};
type CssCommentAST = CssCommonPositionAST & {
type: CssTypes.comment;
comment: string;
};
type CssContainerAST = CssCommonPositionAST & {
type: CssTypes.container;
container: string;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssCharsetAST = CssCommonPositionAST & {
type: CssTypes.charset;
charset: string;
};
type CssCustomMediaAST = CssCommonPositionAST & {
type: CssTypes.customMedia;
name: string;
media: string;
};
type CssDocumentAST = CssCommonPositionAST & {
type: CssTypes.document;
document: string;
vendor?: string;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssFontFaceAST = CssCommonPositionAST & {
type: CssTypes.fontFace;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssHostAST = CssCommonPositionAST & {
type: CssTypes.host;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssImportAST = CssCommonPositionAST & {
type: CssTypes.import;
import: string;
};
type CssKeyframesAST = CssCommonPositionAST & {
type: CssTypes.keyframes;
name: string;
vendor?: string;
keyframes: Array<CssKeyframeAST | CssCommentAST>;
};
type CssKeyframeAST = CssCommonPositionAST & {
type: CssTypes.keyframe;
values: Array<string>;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssLayerAST = CssCommonPositionAST & {
type: CssTypes.layer;
layer: string;
rules?: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssMediaAST = CssCommonPositionAST & {
type: CssTypes.media;
media: string;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssNamespaceAST = CssCommonPositionAST & {
type: CssTypes.namespace;
namespace: string;
};
type CssPageAST = CssCommonPositionAST & {
type: CssTypes.page;
selectors: Array<string>;
declarations: Array<CssDeclarationAST | CssCommentAST | CssAtRuleAST>;
};
type CssSupportsAST = CssCommonPositionAST & {
type: CssTypes.supports;
supports: string;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssStartingStyleAST = CssCommonPositionAST & {
type: CssTypes.startingStyle;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssCounterStyleAST = CssCommonPositionAST & {
type: CssTypes.counterStyle;
name: string;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssFontFeatureValuesAST = CssCommonPositionAST & {
type: CssTypes.fontFeatureValues;
fontFamily: string;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssPositionTryAST = CssCommonPositionAST & {
type: CssTypes.positionTry;
name: string;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssPropertyAST = CssCommonPositionAST & {
type: CssTypes.property;
name: string;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssScopeAST = CssCommonPositionAST & {
type: CssTypes.scope;
scope: string;
rules: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssViewTransitionAST = CssCommonPositionAST & {
type: CssTypes.viewTransition;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssPageMarginBoxAST = CssCommonPositionAST & {
type: CssTypes.pageMarginBox;
name: string;
declarations: Array<CssDeclarationAST | CssCommentAST>;
};
type CssGenericAtRuleAST = CssCommonPositionAST & {
type: CssTypes.atRule;
name: string;
prelude: string;
rules?: Array<CssAtRuleAST | CssDeclarationAST>;
};
type CssAtRuleAST = CssRuleAST | CssCommentAST | CssContainerAST | CssCharsetAST | CssCounterStyleAST | CssCustomMediaAST | CssDocumentAST | CssFontFaceAST | CssFontFeatureValuesAST | CssHostAST | CssImportAST | CssKeyframesAST | CssLayerAST | CssMediaAST | CssNamespaceAST | CssPageAST | CssPageMarginBoxAST | CssPositionTryAST | CssPropertyAST | CssScopeAST | CssSupportsAST | CssStartingStyleAST | CssViewTransitionAST | CssGenericAtRuleAST;
type CssAllNodesAST = CssAtRuleAST | CssStylesheetAST | CssDeclarationAST | CssKeyframeAST;
type CompilerOptions = {
indent?: string;
compress?: boolean;
};
declare const parse: (css: string, options?: {
source?: string;
silent?: boolean;
}) => CssStylesheetAST;
declare const stringify: (node: CssStylesheetAST, options?: CompilerOptions) => string;
declare const _default: {
parse: (css: string, options?: {
source?: string;
silent?: boolean;
}) => CssStylesheetAST;
stringify: (node: CssStylesheetAST, options?: CompilerOptions) => string;
};
export { CssTypes, _default as default, parse, stringify };
export type { CssAllNodesAST, CssAtRuleAST, CssCharsetAST, CssCommentAST, CssCommonAST, CssCommonPositionAST, CssContainerAST, CssCounterStyleAST, CssCustomMediaAST, CssDeclarationAST, CssDocumentAST, CssFontFaceAST, CssFontFeatureValuesAST, CssGenericAtRuleAST, CssHostAST, CssImportAST, CssKeyframeAST, CssKeyframesAST, CssLayerAST, CssMediaAST, CssNamespaceAST, CssPageAST, CssPageMarginBoxAST, CssPositionTryAST, CssPropertyAST, CssRuleAST, CssScopeAST, CssStartingStyleAST, CssStylesheetAST, CssSupportsAST, CssViewTransitionAST };
-2
View File
@@ -1,2 +0,0 @@
!function(t,s){"object"==typeof exports&&"undefined"!=typeof module?s(exports):"function"==typeof define&&define.amd?define(["exports"],s):s((t="undefined"!=typeof globalThis?globalThis:t||self).cssTools={})}(this,function(t){"use strict";class s extends Error{reason;filename;line;column;source;constructor(t,s,e,i,n){super(`${t}:${e}:${i}: ${s}`),this.reason=s,this.filename=t,this.line=e,this.column=i,this.source=n}}class e{start;end;source;constructor(t,s,e){this.start=t,this.end=s,this.source=e}}var i;t.CssTypes=void 0,(i=t.CssTypes||(t.CssTypes={})).stylesheet="stylesheet",i.rule="rule",i.declaration="declaration",i.comment="comment",i.atRule="at-rule",i.container="container",i.charset="charset",i.counterStyle="counter-style",i.document="document",i.customMedia="custom-media",i.fontFace="font-face",i.fontFeatureValues="font-feature-values",i.host="host",i.import="import",i.keyframes="keyframes",i.keyframe="keyframe",i.layer="layer",i.media="media",i.namespace="namespace",i.page="page",i.pageMarginBox="page-margin-box",i.positionTry="position-try",i.property="property",i.scope="scope",i.startingStyle="starting-style",i.supports="supports",i.viewTransition="view-transition";const n=(t,s,e)=>{let i=e,n=1e4;do{const e=s.map(s=>t.indexOf(s,i));e.push(t.indexOf("\\",i));const r=e.filter(t=>-1!==t);if(0===r.length)return-1;const o=Math.min(...r);if("\\"!==t[o])return o;i=o+2,n--}while(n>0);throw new Error("Too many escaping")},r=(t,s,e)=>{let i=e,o=1e4;do{const e=s.map(s=>t.indexOf(s,i));e.push(t.indexOf("(",i)),e.push(t.indexOf('"',i)),e.push(t.indexOf("'",i)),e.push(t.indexOf("\\",i));const c=e.filter(t=>-1!==t);if(0===c.length)return-1;const h=Math.min(...c);switch(t[h]){case"\\":i=h+2;break;case"(":{const s=r(t,[")"],h+1);if(-1===s)return-1;i=s+1}break;case'"':{const s=n(t,['"'],h+1);if(-1===s)return-1;i=s+1}break;case"'":{const s=n(t,["'"],h+1);if(-1===s)return-1;i=s+1}break;default:return h}o--}while(o>0);throw new Error("Too many escaping")},o=/\/\*[^]*?(?:\*\/|$)/g;function c(t){return t?t.trim():""}function h(t,s){const e=t&&"string"==typeof t.type,i=e?t:s;for(const s in t){const e=t[s];Array.isArray(e)?e.forEach(t=>{h(t,i)}):e&&"object"==typeof e&&h(e,i)}return e&&Object.defineProperty(t,"parent",{configurable:!0,writable:!0,enumerable:!1,value:s||null}),t}class a{level=0;indentation=" ";compress=!1;constructor(t){"string"==typeof t?.indent&&(this.indentation=t?.indent),t?.compress&&(this.compress=!0)}emit(t,s){return t}indent(t){return this.level=this.level||1,t?(this.level+=t,""):Array(this.level).join(this.indentation)}visit(s){switch(s.type){case t.CssTypes.stylesheet:return this.stylesheet(s);case t.CssTypes.rule:return this.rule(s);case t.CssTypes.declaration:return this.declaration(s);case t.CssTypes.comment:return this.comment(s);case t.CssTypes.container:return this.container(s);case t.CssTypes.charset:return this.charset(s);case t.CssTypes.counterStyle:return this.counterStyle(s);case t.CssTypes.document:return this.document(s);case t.CssTypes.customMedia:return this.customMedia(s);case t.CssTypes.fontFace:return this.fontFace(s);case t.CssTypes.fontFeatureValues:return this.fontFeatureValues(s);case t.CssTypes.host:return this.host(s);case t.CssTypes.import:return this.import(s);case t.CssTypes.keyframes:return this.keyframes(s);case t.CssTypes.keyframe:return this.keyframe(s);case t.CssTypes.layer:return this.layer(s);case t.CssTypes.media:return this.media(s);case t.CssTypes.namespace:return this.namespace(s);case t.CssTypes.page:return this.page(s);case t.CssTypes.pageMarginBox:return this.pageMarginBox(s);case t.CssTypes.positionTry:return this.positionTry(s);case t.CssTypes.property:return this.property(s);case t.CssTypes.scope:return this.scope(s);case t.CssTypes.startingStyle:return this.startingStyle(s);case t.CssTypes.supports:return this.supports(s);case t.CssTypes.viewTransition:return this.viewTransition(s);case t.CssTypes.atRule:return this.genericAtRule(s)}}mapVisit(t,s){let e="";s=s||"";for(let i=0,n=t.length;i<n;i++)e+=this.visit(t[i]),s&&i<n-1&&(e+=this.emit(s));return e}compile(t){return this.compress?t.stylesheet.rules.map(this.visit,this).join(""):this.stylesheet(t)}stylesheet(t){return this.mapVisit(t.stylesheet.rules,"\n\n")}comment(t){return this.compress?this.emit("",t.position):this.emit(`${this.indent()}/*${t.comment}*/`,t.position)}container(t){return this.compress?this.emit(`@container ${t.container}`,t.position)+this.emit("{")+this.mapVisit(t.rules)+this.emit("}"):this.emit(`${this.indent()}@container ${t.container}`,t.position)+this.emit(` {\n${this.indent(1)}`)+this.mapVisit(t.rules,"\n\n")+this.emit(`\n${this.indent(-1)}${this.indent()}}`)}layer(t){return this.compress?this.emit(`@layer ${t.layer}`,t.position)+(t.rules?this.emit("{")+this.mapVisit(t.rules)+this.emit("}"):";"):this.emit(`${this.indent()}@layer ${t.layer}`,t.position)+(t.rules?this.emit(` {\n${this.indent(1)}`)+this.mapVisit(t.rules,"\n\n")+this.emit(`\n${this.indent(-1)}${this.indent()}}`):"Line truncated
//# sourceMappingURL=adobe-css-tools.js.map
@@ -1 +0,0 @@
{"version":3,"file":"adobe-css-tools.js","sources":["../../src/CssParseError.ts","../../src/CssPosition.ts","../../src/type.ts","../../src/utils/stringSearch.ts","../../src/parse/index.ts","../../src/stringify/compiler.ts","../../src/index.ts","../../src/stringify/index.ts"],"sourcesContent":[null,null,null,null,null,null,null,null],"names":["CssParseError","Error","reason","filename","line","column","source","constructor","msg","lineno","css","super","this","Position","start","end","CssTypes","indexOfArrayNonEscaped","string","search","position","currentPosition","maxLoop","all","map","v","indexOf","push","foundAll","filter","length","found","Math","min","indexOfArrayWithBracketAndQuoteSupport","currentSearchPosition","firstMatchPos","endPosition","endQuotePosition","commentRegex","trim","str","addParent","obj","parent","isNode","type","childParent","k","value","Array","isArray","forEach","Object","defineProperty","configurable","writable","enumerable","Compiler","level","indentation","compress","options","indent","emit","_position","join","visit","node","stylesheet","rule","declaration","comment","container","charset","counterStyle","document","customMedia","fontFace","fontFeatureValues","host","import","keyframes","keyframe","layer","media","namespace","page","pageMarginBox","positionTry","property","scope","startingStyle","supports","viewTransition","atRule","genericAtRule","mapVisit","nodes","delim","buf","i","compile","rules","doc","vendor","name","decls","declarations","values","sel","selectors","fontFamily","prelude","hasNestedRules","some","r","s","split","padEnd","parse","whitespace","errorsList","error","err","silent","open","openMatch","exec","processMatch","close","closeMatch","m","lines","match","lastIndexOf","updatePosition","slice","comments","c","pos","charAt","selector","bracePos","selectorStr","substring","result","index","splitWithBracketAndQuoteSupport","replace","propMatch","propValue","separatorMatch","endValuePosition","ret","endMatch","decl","semiPos","looksLikeNestedRule","closePos","ruleBody","items","ar","nestedR","rulesOrDeclarations","vals","res","spacesMatch","pageMarginBoxRegex","RegExp","atImport","_compileAtRule","atCharset","atNamespace","re","m1","m2","frames","frame","concat","atKeyframes","style","atMedia","atCustomMedia","atSupports","atDocument","atPage","atHost","atFontFace","atFontFeatureValues","atContainer","atStartingStyle","atLayer","atProperty","atCounterStyle","atScope","atViewTransition","atPositionTry","atPageMarginBox","preludeEnd","atGeneric","rulesList","parsingErrors","stringify"],"mappings":"+OAAc,MAAOA,UAAsBC,MAChCC,OACAC,SACAC,KACAC,OACAC,OAET,WAAAC,CACEJ,EACAK,EACAC,EACAJ,EACAK,GAEAC,MAAM,GAAGR,KAAYM,KAAUJ,MAAWG,KAC1CI,KAAKV,OAASM,EACdI,KAAKT,SAAWA,EAChBS,KAAKR,KAAOK,EACZG,KAAKP,OAASA,EACdO,KAAKN,OAASI,CAChB,ECjBY,MAAOG,EACnBC,MACAC,IACAT,OAEA,WAAAC,CACEO,EACAC,EACAT,GAEAM,KAAKE,MAAQA,EACbF,KAAKG,IAAMA,EACXH,KAAKN,OAASA,CAChB,ECbF,IAAYU,EAAAA,EAAAA,cAAAA,GAAAA,EAAAA,EAAAA,WAAAA,WAAQ,CAAA,IAClB,WAAA,aACAA,EAAA,KAAA,OACAA,EAAA,YAAA,cACAA,EAAA,QAAA,UACAA,EAAA,OAAA,UACAA,EAAA,UAAA,YACAA,EAAA,QAAA,UACAA,EAAA,aAAA,gBACAA,EAAA,SAAA,WACAA,EAAA,YAAA,eACAA,EAAA,SAAA,YACAA,EAAA,kBAAA,sBACAA,EAAA,KAAA,OACAA,EAAA,OAAA,SACAA,EAAA,UAAA,YACAA,EAAA,SAAA,WACAA,EAAA,MAAA,QACAA,EAAA,MAAA,QACAA,EAAA,UAAA,YACAA,EAAA,KAAA,OACAA,EAAA,cAAA,kBACAA,EAAA,YAAA,eACAA,EAAA,SAAA,WACAA,EAAA,MAAA,QACAA,EAAA,cAAA,iBACAA,EAAA,SAAA,WACAA,EAAA,eAAA,kBC9BK,MAkBMC,EAAyB,CACpCC,EACAC,EACAC,KAEA,IAAIC,EAAkBD,EAClBE,EAxBkB,IAyBtB,EAAG,CACD,MAAMC,EAAMJ,EAAOK,IAAKC,GAAMP,EAAOQ,QAAQD,EAAGJ,IAChDE,EAAII,KAAKT,EAAOQ,QAAQ,KAAML,IAC9B,MAAMO,EAAWL,EAAIM,OAAQJ,QAAMA,GACnC,GAAwB,IAApBG,EAASE,OACX,OAAO,EAGT,MAAMC,EAAQC,KAAKC,OAAOL,GAC1B,GAAsB,OAAlBV,EAAOa,GAIT,OAAOA,EAHPV,EAAkBU,EAAQ,EAC1BT,GAIJ,OAASA,EAAU,GAEnB,MAAM,IAAIrB,MAAM,sBA0BLiC,EAAyC,CACpDhB,EACAC,EACAC,KAEA,IAAIe,EAAwBf,EACxBE,EA1EkB,IA4EtB,EAAG,CACD,MAAMC,EAAMJ,EAAOK,IAAKC,GAAMP,EAAOQ,QAAQD,EAAGU,IAEhDZ,EAAII,KAAKT,EAAOQ,QAAQ,IAAKS,IAC7BZ,EAAII,KAAKT,EAAOQ,QAAQ,IAAKS,IAC7BZ,EAAII,KAAKT,EAAOQ,QAAQ,IAAKS,IAC7BZ,EAAII,KAAKT,EAAOQ,QAAQ,KAAMS,IAE9B,MAAMP,EAAWL,EAAIM,OAAQJ,QAAMA,GACnC,GAAwB,IAApBG,EAASE,OACX,OAAO,EAGT,MAAMM,EAAgBJ,KAAKC,OAAOL,GAElC,OADaV,EAAOkB,IAElB,IAAK,KACHD,EAAwBC,EAAgB,EACxC,MACF,IAAK,IACH,CACE,MAAMC,EAAcH,EAClBhB,EACA,CAAC,KACDkB,EAAgB,GAElB,IAAoB,IAAhBC,EACF,OAAO,EAETF,EAAwBE,EAAc,CACxC,CACA,MACF,IAAK,IACH,CACE,MAAMC,EAAmBrB,EACvBC,EACA,CAAC,KACDkB,EAAgB,GAElB,IAAyB,IAArBE,EACF,OAAO,EAETH,EAAwBG,EAAmB,CAC7C,CACA,MACF,IAAK,IACH,CACE,MAAMA,EAAmBrB,EACvBC,EACA,CAAC,KACDkB,EAAgB,GAElB,IAAyB,IAArBE,EACF,OAAO,EAETH,EAAwBG,EAAmB,CAC7C,CACA,MACF,QACE,OAAOF,EAEXd,GACF,OAASA,EAAU,GAEnB,MAAM,IAAIrB,MAAM,sBCjGZsC,EAAe,uBAotCrB,SAASC,EAAKC,GACZ,OAAOA,EAAMA,EAAID,OAAS,EAC5B,CAKA,SAASE,EACPC,EACAC,GAEA,MAAMC,EAASF,GAA2B,iBAAbA,EAAIG,KAC3BC,EAAcF,EAASF,EAAMC,EAEnC,IAAK,MAAMI,KAAKL,EAAK,CACnB,MAAMM,EAAQN,EAAIK,GACdE,MAAMC,QAAQF,GAChBA,EAAMG,QAAS3B,IACbiB,EAAUjB,EAAGsB,KAENE,GAA0B,iBAAVA,GACzBP,EAAUO,EAAOF,EAErB,CAWA,OATIF,GACFQ,OAAOC,eAAeXLine truncated
-314
View File
@@ -1,314 +0,0 @@
# API Reference
## Overview
`@adobe/css-tools` provides a modern CSS parser and stringifier with comprehensive TypeScript support. It can parse CSS into an Abstract Syntax Tree (AST) and convert the AST back to CSS with various formatting options.
## Installation
```bash
npm install @adobe/css-tools
```
## Core Functions
### `parse(code, options?)`
Parses CSS code and returns an Abstract Syntax Tree (AST).
#### Parameters
- `code` (string) - The CSS code to parse
- `options` (object, optional) - Parsing options
- `silent` (boolean) - Silently fail on parse errors instead of throwing. When `true`, errors are collected in `ast.stylesheet.parsingErrors`
- `source` (string) - File path for better error reporting
#### Returns
- `CssStylesheetAST` - The parsed CSS as an AST
#### Example
```javascript
import { parse } from '@adobe/css-tools';
const css = `
.example {
color: red;
font-size: 16px;
}
`;
const ast = parse(css);
console.log(ast.stylesheet.rules);
```
### `stringify(ast, options?)`
Converts a CSS AST back to CSS string with configurable formatting.
#### Parameters
- `ast` (CssStylesheetAST) - The CSS AST to stringify
- `options` (CompilerOptions, optional) - Stringification options
- `indent` (string) - Indentation string (default: `' '`)
- `compress` (boolean) - Whether to compress/minify the output (default: `false`)
#### Returns
- `string` - The formatted CSS string
#### Example
```javascript
import { parse, stringify } from '@adobe/css-tools';
const css = '.example { color: red; }';
const ast = parse(css);
// Pretty print
const formatted = stringify(ast, { indent: ' ' });
console.log(formatted);
// Output:
// .example {
// color: red;
// }
// Compressed
const minified = stringify(ast, { compress: true });
console.log(minified);
// Output: .example{color:red}
```
## Type Definitions
### Core Types
#### `CssStylesheetAST`
The root AST node representing a complete CSS stylesheet.
```typescript
type CssStylesheetAST = {
type: CssTypes.stylesheet;
stylesheet: {
source?: string;
rules: CssRuleAST[];
parsingErrors?: CssParseError[];
};
};
```
#### `CssRuleAST`
Represents a CSS rule (selector + declarations).
```typescript
type CssRuleAST = {
type: CssTypes.rule;
selectors: string[];
declarations: CssDeclarationAST[];
position?: CssPosition;
parent?: CssStylesheetAST;
};
```
#### `CssDeclarationAST`
Represents a CSS property declaration.
```typescript
type CssDeclarationAST = {
type: CssTypes.declaration;
property: string;
value: string;
position?: CssPosition;
parent?: CssRuleAST;
};
```
#### `CssMediaAST`
Represents a CSS @media rule.
```typescript
type CssMediaAST = {
type: CssTypes.media;
media: string;
rules: CssRuleAST[];
position?: CssPosition;
parent?: CssStylesheetAST;
};
```
#### `CssKeyframesAST`
Represents a CSS @keyframes rule.
```typescript
type CssKeyframesAST = {
type: CssTypes.keyframes;
name: string;
keyframes: CssKeyframeAST[];
position?: CssPosition;
parent?: CssStylesheetAST;
};
```
#### `CssPosition`
Represents source position information.
```typescript
type CssPosition = {
source?: string;
start: {
line: number;
column: number;
};
end: {
line: number;
column: number;
};
};
```
#### `CssParseError`
Represents a parsing error.
```typescript
type CssParseError = {
message: string;
reason: string;
filename?: string;
line: number;
column: number;
source?: string;
};
```
### Compiler Options
#### `CompilerOptions`
Options for the stringifier.
```typescript
type CompilerOptions = {
indent?: string; // Default: ' '
compress?: boolean; // Default: false
};
```
## Error Handling
### Silent Parsing
When parsing malformed CSS, you can use the `silent` option to collect errors instead of throwing:
```javascript
import { parse } from '@adobe/css-tools';
const malformedCss = `
body { color: red; }
{ color: blue; } /* Missing selector */
.valid { background: green; }
`;
const result = parse(malformedCss, { silent: true });
if (result.stylesheet.parsingErrors) {
result.stylesheet.parsingErrors.forEach(error => {
console.log(`Error at line ${error.line}: ${error.message}`);
});
}
// Valid rules are still parsed
console.log('Valid rules:', result.stylesheet.rules.length);
```
### Source Tracking
Enable source tracking for better error reporting:
```javascript
import { parse } from '@adobe/css-tools';
const css = 'body { color: red; }';
const ast = parse(css, { source: 'styles.css' });
const rule = ast.stylesheet.rules[0];
console.log(rule.position?.source); // "styles.css"
console.log(rule.position?.start); // { line: 1, column: 1 }
console.log(rule.position?.end); // { line: 1, column: 20 }
```
## Advanced Usage
### Working with At-Rules
```javascript
import { parse, stringify } from '@adobe/css-tools';
const css = `
@media (max-width: 768px) {
.container {
padding: 10px;
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
`;
const ast = parse(css);
// Access media rules
const mediaRule = ast.stylesheet.rules.find(rule => rule.type === 'media');
console.log(mediaRule.media); // "(max-width: 768px)"
// Access keyframes
const keyframesRule = ast.stylesheet.rules.find(rule => rule.type === 'keyframes');
console.log(keyframesRule.name); // "fadeIn"
```
### Custom Formatting
```javascript
import { parse, stringify } from '@adobe/css-tools';
const css = '.example{color:red;font-size:16px}';
const ast = parse(css);
// Custom indentation
const formatted = stringify(ast, { indent: ' ' });
console.log(formatted);
// Output:
// .example {
// color: red;
// font-size: 16px;
// }
// Compressed with no spaces
const compressed = stringify(ast, { compress: true });
console.log(compressed);
// Output: .example{color:red;font-size:16px}
```
## TypeScript Integration
The library provides comprehensive TypeScript support with full type definitions for all AST nodes and functions:
```typescript
import { parse, stringify, type CssStylesheetAST } from '@adobe/css-tools';
const css: string = '.example { color: red; }';
const ast: CssStylesheetAST = parse(css);
const output: string = stringify(ast);
```
## Performance Considerations
- The parser is optimized for large CSS files
- AST nodes are lightweight and memory-efficient
- Stringification is fast and supports streaming for large outputs
- Consider using `compress: true` for production builds to reduce file size
## Browser Support
The library works in all modern browsers and Node.js environments. For older environments, you may need to use a bundler with appropriate polyfills.
-366
View File
@@ -1,366 +0,0 @@
# Abstract Syntax Tree (AST)
## Overview
The AST represents CSS as a tree structure where each node has a specific type and properties. All nodes share common properties and have type-specific properties.
## Common Properties
All AST nodes have these properties:
### `type`
The node type as a string. See [Node Types](#node-types) for all possible values.
### `position` (optional)
Position information for the node in the source code:
```typescript
{
start: { line: number; column: number };
end: { line: number; column: number };
source?: string;
}
```
### `parent` (optional)
Reference to the parent node in the AST.
## Node Types
### `stylesheet`
The root node representing an entire CSS document.
**Properties:**
- `stylesheet.source` (optional): Source file path
- `stylesheet.rules`: Array of top-level rules
- `stylesheet.parsingErrors` (optional): Array of parse errors when `silent` option is used
**Example:**
```json
{
"type": "stylesheet",
"stylesheet": {
"rules": [
// ... other nodes
]
}
}
```
### `rule`
A CSS rule with selectors and declarations.
**Properties:**
- `selectors`: Array of CSS selectors as strings
- `declarations`: Array of declarations and comments
**Example:**
```json
{
"type": "rule",
"selectors": ["body", "html"],
"declarations": [
{
"type": "declaration",
"property": "color",
"value": "red"
}
]
}
```
### `declaration`
A CSS property declaration.
**Properties:**
- `property`: The CSS property name
- `value`: The CSS property value as a string
**Example:**
```json
{
"type": "declaration",
"property": "background-color",
"value": "#ffffff"
}
```
### `comment`
A CSS comment.
**Properties:**
- `comment`: The comment text (without `/*` and `*/`)
**Example:**
```json
{
"type": "comment",
"comment": " This is a comment "
}
```
### `media`
A `@media` rule.
**Properties:**
- `media`: The media query string
- `rules`: Array of rules within the media block
**Example:**
```json
{
"type": "media",
"media": "screen and (max-width: 768px)",
"rules": [
// ... nested rules
]
}
```
### `keyframes`
A `@keyframes` rule.
**Properties:**
- `name`: The keyframes name
- `vendor` (optional): Vendor prefix (e.g., "-webkit-")
- `keyframes`: Array of keyframe rules and comments
**Example:**
```json
{
"type": "keyframes",
"name": "fade",
"keyframes": [
{
"type": "keyframe",
"values": ["from"],
"declarations": [
{
"type": "declaration",
"property": "opacity",
"value": "0"
}
]
}
]
}
```
### `keyframe`
A keyframe within a `@keyframes` rule.
**Properties:**
- `values`: Array of keyframe selectors (e.g., `["from"]`, `["to"]`, `["50%"]`)
- `declarations`: Array of declarations and comments
### `import`
An `@import` rule.
**Properties:**
- `import`: The import string (URL or media query)
**Example:**
```json
{
"type": "import",
"import": "url('styles.css')"
}
```
### `charset`
A `@charset` rule.
**Properties:**
- `charset`: The character encoding
**Example:**
```json
{
"type": "charset",
"charset": "utf-8"
}
```
### `namespace`
A `@namespace` rule.
**Properties:**
- `namespace`: The namespace declaration
**Example:**
```json
{
"type": "namespace",
"namespace": "url(http://www.w3.org/1999/xhtml)"
}
```
### `supports`
A `@supports` rule.
**Properties:**
- `supports`: The supports condition
- `rules`: Array of rules within the supports block
**Example:**
```json
{
"type": "supports",
"supports": "(display: grid)",
"rules": [
// ... nested rules
]
}
```
### `document`
A `@document` rule.
**Properties:**
- `document`: The document condition
- `vendor` (optional): Vendor prefix
- `rules`: Array of rules within the document block
### `page`
A `@page` rule.
**Properties:**
- `selectors`: Array of page selectors
- `declarations`: Array of declarations and comments
### `font-face`
A `@font-face` rule.
**Properties:**
- `declarations`: Array of font declarations and comments
### `host`
A `:host` rule.
**Properties:**
- `rules`: Array of rules within the host block
### `container`
A `@container` rule.
**Properties:**
- `container`: The container query
- `rules`: Array of rules within the container block
### `layer`
A `@layer` rule.
**Properties:**
- `layer`: The layer name
- `rules` (optional): Array of rules within the layer block
### `custom-media`
A `@custom-media` rule.
**Properties:**
- `name`: The custom media query name
- `media`: The media query definition
### `starting-style`
A `@starting-style` rule.
**Properties:**
- `rules`: Array of rules within the starting-style block
## Type Hierarchy
The AST nodes are organized in the following hierarchy:
- `CssStylesheetAST` - Root node
- `CssAtRuleAST` - Union of all at-rule and rule nodes
- `CssAllNodesAST` - Union of all possible node types
## Working with the AST
### Traversing Nodes
```typescript
import { parse, CssStylesheetAST } from '@adobe/css-tools';
const ast: CssStylesheetAST = parse('body { color: red; }');
// Access top-level rules
ast.stylesheet.rules.forEach(rule => {
if (rule.type === 'rule') {
console.log('Selectors:', rule.selectors);
rule.declarations.forEach(decl => {
if (decl.type === 'declaration') {
console.log(`${decl.property}: ${decl.value}`);
}
});
}
});
```
### Modifying Nodes
```typescript
// Add a new declaration
const newDecl = {
type: 'declaration' as const,
property: 'font-size',
value: '16px'
};
// Find a rule and add the declaration
ast.stylesheet.rules.forEach(rule => {
if (rule.type === 'rule' && rule.selectors.includes('body')) {
rule.declarations.push(newDecl);
}
});
```
### Error Handling
When parsing with the `silent` option, errors are collected in the AST:
```typescript
const ast = parse('invalid css {', { silent: true });
if (ast.stylesheet.parsingErrors) {
ast.stylesheet.parsingErrors.forEach(error => {
console.error('Parse error:', error.message);
});
}
```
## Position Information
Position information is available on most nodes and includes:
- `start.line` and `start.column`: Beginning of the node
- `end.line` and `end.column`: End of the node
- `source`: Source file path (if provided during parsing)
This is useful for:
- Error reporting
- Source mapping
- Code analysis tools
- IDE integration
-216
View File
@@ -1,216 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [4.5.0] - 2026-05-20
### Added
- Add support for CSS Nesting — rules nested inside other rules (#621 by @jogibear9988)
- Add support for `@container` container queries with nested rule body
- Add support for Page Margin Boxes (`@top-left`, `@bottom-right`, `@left-middle`, etc.) inside `@page`
- Add generic at-rule fallback — unknown `@foo { ... }` and `@foo ...;` rules are preserved as `CssGenericAtRuleAST` instead of being dropped
- Add support for `@counter-style`, `@font-feature-values`, `@scope`, `@view-transition`, `@position-try`
- Export new TypeScript types: `CssGenericAtRuleAST`, `CssPageMarginBoxAST`, `CssPositionTryAST`, `CssScopeAST`, `CssViewTransitionAST`, `CssCounterStyleAST`, `CssFontFeatureValuesAST`
### Fixed
- Fix selector parsing for complex nested selectors including `:is()`, `:has()`, `:not()`
- Fix `@page` to correctly accept margin-box at-rule children
- Fix bracket- and quote-aware position tracking for pseudo-functions
### Changed
- Upgrade to TypeScript 6.0.2
- Upgrade to `@rollup/plugin-terser` 1.0.0
- Drop Node 18 from CI (EOL April 2025) — minimum supported runtime is now Node 20
### Security
- Update lodash to 4.18.1
- Update minimatch to 3.1.5
- Update picomatch to 4.0.4
## [4.4.4] - 2025-07-22
### Changed
- Switch from yarn to npm for package management
- Switch from eslint to biome for code formatting and linting
- Reformat codebase to comply with biome recommendations
- Switch from webpack to rollup for bundling
### Fixed
- Fix module exports to ensure proper compatibility with bundlers
- Add validation check to prevent future export issues
## [4.4.3] - 2025-05-15
### Security
- Fix polynomial regular expression vulnerability on uncontrolled data
- Refactor code to enable GitHub security static analysis
### Performance
- Improve parsing performance with minor optimizations
- Replace regex patterns with string search (indexOf-based) for better performance
### Added
- Add new utility functions with comprehensive unit tests
- Add improved formatting for CSS Grid template areas (#283 by @jogibear9988)
### Fixed
- Fix TypeScript error with ConstructorParameters in Parcel bundler (#444)
## [4.4.2] - 2025-02-12
### Fixed
- Fix regular expression for parsing quoted values in parentheses
## [4.4.0] - 2024-06-05
### Added
- Add support for CSS `@starting-style` at-rule (#319)
## [4.3.3] - 2024-01-24
### Changed
- Update package export configuration (#271)
## [4.3.2] - 2023-11-28
### Security
- Fix ReDoS vulnerability with crafted CSS strings - CVE-2023-48631
### Fixed
- Fix parsing issues with `:is()` and nested `:nth-child()` selectors (#211)
## [4.3.1] - 2023-03-14
### Security
- Fix ReDoS vulnerability with crafted CSS strings - CVE-2023-26364
## [4.3.0] - 2023-03-07
### Changed
- Update build toolchain and dependencies
- Update package exports configuration and file structure
## [4.2.0] - 2023-02-21
### Added
- Add support for CSS `@container` at-rule
- Add support for CSS `@layer` at-rule
## [4.1.0] - 2023-01-25
### Added
- Add support for ES Modules (ESM)
## [4.0.2] - 2023-01-12
### Fixed
- Fix `@import` parsing when URL contains semicolons (#71)
- Fix regression in selector parsing for attribute selectors (#77)
## [4.0.1] - 2022-08-03
### Fixed
- Fix `globalThis` configuration for webpack to enable UMD module usage in Node.js environments
## [4.0.0] - 2022-06-09
### Changed
- Fork from original css library to Adobe's `@adobe/css-tools` package
- Convert codebase from JavaScript to TypeScript
- Improve parsing performance by approximately 25%
- Update all dependencies to latest versions
- Remove source map support
---
## Legacy Versions (Pre-Adobe Fork)
## [3.0.0] - 2020-07-01
### Changed
- Bump major version due to major dependency updates and Node.js version requirement changes
## [2.2.1] - 2015-06-17
### Fixed
- Fix parsing of escaped quotes in quoted strings
## [2.2.0] - 2015-02-18
### Added
- Add `parsingErrors` property to list errors when parsing with `silent: true`
- Accept EOL characters and all whitespace characters in at-rules such as `@media`
## [2.1.0] - 2014-08-05
### Added
- Add `inputSourcemaps` option to disable input source map processing
- Add `sourcemap: 'generator'` option to return the `SourceMapGenerator` object
- Use `inherits` package for inheritance (fixes browser compatibility issues)
### Changed
- Change error message format and add `.reason` property to error objects
## [2.0.0] - 2014-06-18
### Added
- Add non-enumerable parent reference to each AST node
- Add support for `@custom-media`, `@host`, and `@font-face` at-rules
- Allow commas inside selector functions
- Allow empty property values
- Add `node.position.content` property
- Integrate css-parse and css-stringify libraries
- Apply original source maps from source files
### Changed
- Change default `options.position` value to `true`
- Remove comments from properties and values
### Removed
- Drop Component(1) support
### Fixed
- Fix assertion errors when selectors are missing
## [1.6.1] - 2014-01-02
### Fixed
- Fix component.json configuration
## [1.6.0] - 2013-12-21
### Changed
- Update dependencies
## [1.5.0] - 2013-12-03
### Changed
- Update dependencies
## [1.1.0] - 2013-04-04
### Changed
- Update dependencies
## [1.0.7] - 2012-11-21
### Fixed
- Fix component.json configuration
## [1.0.4] - 2012-11-15
### Changed
- Update css-stringify dependency
## [1.0.3] - 2012-09-01
### Added
- Add Component support
## [0.0.1] - 2010-01-03
### Added
- Initial release
-450
View File
@@ -1,450 +0,0 @@
# Usage Examples
## Basic Usage
### Parsing CSS
```javascript
import { parse } from '@adobe/css-tools';
// Basic CSS parsing
const css = `
body {
font-size: 12px;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
`;
const ast = parse(css);
console.log(ast);
```
### Stringifying AST
```javascript
import { parse, stringify } from '@adobe/css-tools';
const css = 'body { font-size: 12px; color: #333; }';
const ast = parse(css);
// Convert back to CSS
const output = stringify(ast);
console.log(output); // "body { font-size: 12px; color: #333; }"
```
## Advanced Parsing Options
### Source Tracking
```javascript
import { parse } from '@adobe/css-tools';
const css = 'body { color: red; }';
const ast = parse(css, { source: 'styles.css' });
// Position information is available
const rule = ast.stylesheet.rules[0];
console.log(rule.position?.source); // "styles.css"
console.log(rule.position?.start); // { line: 1, column: 1 }
console.log(rule.position?.end); // { line: 1, column: 20 }
```
### Silent Error Handling
```javascript
import { parse } from '@adobe/css-tools';
const malformedCss = `
body { color: red; }
{ color: blue; } /* Missing selector */
.valid { background: green; }
`;
// Parse with silent error handling
const result = parse(malformedCss, { silent: true });
// Check for parsing errors
if (result.stylesheet.parsingErrors) {
console.log('Parsing errors:', result.stylesheet.parsingErrors.length);
result.stylesheet.parsingErrors.forEach(error => {
console.log(`Error at line ${error.line}: ${error.message}`);
});
}
// Valid rules are still parsed
console.log('Valid rules:', result.stylesheet.rules.length);
```
## AST Structure Examples
### Basic Rule
```javascript
import { parse } from '@adobe/css-tools';
const css = `
.header {
background: #f0f0f0;
padding: 20px;
border-bottom: 1px solid #ccc;
}
`;
const ast = parse(css);
const rule = ast.stylesheet.rules[0];
console.log(rule.type); // "rule"
console.log(rule.selectors); // [".header"]
console.log(rule.declarations.length); // 3
rule.declarations.forEach(decl => {
console.log(`${decl.property}: ${decl.value}`);
});
// Output:
// background: #f0f0f0
// padding: 20px
// border-bottom: 1px solid #ccc
```
### Media Queries
```javascript
import { parse } from '@adobe/css-tools';
const css = `
@media screen and (max-width: 768px) {
.container {
padding: 10px;
}
.sidebar {
display: none;
}
}
`;
const ast = parse(css);
const mediaRule = ast.stylesheet.rules[0];
console.log(mediaRule.type); // "media"
console.log(mediaRule.media); // "screen and (max-width: 768px)"
console.log(mediaRule.rules.length); // 2
```
### Keyframes
```javascript
import { parse } from '@adobe/css-tools';
const css = `
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
`;
const ast = parse(css);
const keyframesRule = ast.stylesheet.rules[0];
console.log(keyframesRule.type); // "keyframes"
console.log(keyframesRule.name); // "fadeIn"
console.log(keyframesRule.keyframes.length); // 2
keyframesRule.keyframes.forEach(keyframe => {
console.log(`Keyframe: ${keyframe.values.join(', ')}`);
keyframe.declarations.forEach(decl => {
console.log(` ${decl.property}: ${decl.value}`);
});
});
```
### Comments
```javascript
import { parse } from '@adobe/css-tools';
const css = `
/* Header styles */
.header {
background: red; /* Fallback color */
}
/* Footer styles */
.footer {
background: blue;
}
`;
const ast = parse(css);
ast.stylesheet.rules.forEach(rule => {
if (rule.type === 'comment') {
console.log(`Comment: ${rule.comment}`);
} else if (rule.type === 'rule') {
console.log(`Rule: ${rule.selectors.join(', ')}`);
rule.declarations.forEach(decl => {
if (decl.type === 'comment') {
console.log(` Comment: ${decl.comment}`);
} else {
console.log(` ${decl.property}: ${decl.value}`);
}
});
}
});
```
## Stringifying Options
### Compressed Output
```javascript
import { parse, stringify } from '@adobe/css-tools';
const css = `
body {
font-size: 12px;
color: #333;
margin: 0;
padding: 0;
}
`;
const ast = parse(css);
// Compressed output
const compressed = stringify(ast, { compress: true });
console.log(compressed);
// Output: "body{font-size:12px;color:#333;margin:0;padding:0}"
```
### Custom Indentation
```javascript
import { parse, stringify } from '@adobe/css-tools';
const css = 'body { font-size: 12px; color: #333; }';
const ast = parse(css);
// Custom indentation
const formatted = stringify(ast, { indent: ' ' });
console.log(formatted);
// Output:
// body {
// font-size: 12px;
// color: #333;
// }
```
## Working with Complex CSS
### Nested Rules and At-Rules
```javascript
import { parse, stringify } from '@adobe/css-tools';
const complexCss = `
@import url('https://fonts.googleapis.com/css2?family=Roboto');
@charset "UTF-8";
@media print {
body {
font-size: 12pt;
}
}
@supports (display: grid) {
.grid {
display: grid;
}
}
@keyframes slideIn {
0% { transform: translateX(-100%); }
100% { transform: translateX(0); }
}
@font-face {
font-family: 'CustomFont';
src: url('custom-font.woff2') format('woff2');
}
`;
const ast = parse(complexCss);
ast.stylesheet.rules.forEach(rule => {
switch (rule.type) {
case 'import':
console.log(`Import: ${rule.import}`);
break;
case 'charset':
console.log(`Charset: ${rule.charset}`);
break;
case 'media':
console.log(`Media query: ${rule.media}`);
break;
case 'supports':
console.log(`Supports: ${rule.supports}`);
break;
case 'keyframes':
console.log(`Keyframes: ${rule.name}`);
break;
case 'font-face':
console.log('Font-face rule');
break;
}
});
```
### Manipulating the AST
```javascript
import { parse, stringify } from '@adobe/css-tools';
const css = `
.button {
background: blue;
color: white;
padding: 10px;
}
`;
const ast = parse(css);
const rule = ast.stylesheet.rules[0];
// Add a new declaration
rule.declarations.push({
type: 'declaration',
property: 'border-radius',
value: '5px'
});
// Modify existing declaration
const backgroundDecl = rule.declarations.find(d => d.property === 'background');
if (backgroundDecl) {
backgroundDecl.value = 'red';
}
// Add a new selector
rule.selectors.push('.btn');
const modifiedCss = stringify(ast);
console.log(modifiedCss);
```
## Error Handling
### Catching Parse Errors
```javascript
import { parse, CssParseError } from '@adobe/css-tools';
try {
const ast = parse('body { color: red; } { invalid }');
} catch (error) {
if (error instanceof CssParseError) {
console.log(`Parse error at line ${error.line}, column ${error.column}:`);
console.log(error.message);
console.log(`Source: ${error.filename}`);
}
}
```
### Working with Silent Errors
```javascript
import { parse } from '@adobe/css-tools';
const problematicCss = `
body { color: red; }
{ color: blue; } /* Missing selector */
.valid { background: green; }
.another { border: 1px solid; } /* Missing closing brace */
`;
const result = parse(problematicCss, {
silent: true,
source: 'problematic.css'
});
// Process valid rules
const validRules = result.stylesheet.rules.filter(rule => rule.type === 'rule');
console.log(`Found ${validRules.length} valid rules`);
// Log errors for debugging
if (result.stylesheet.parsingErrors) {
result.stylesheet.parsingErrors.forEach(error => {
console.log(`Error: ${error.message} at line ${error.line}`);
});
}
```
### CSS Minification
```javascript
import { parse, stringify } from '@adobe/css-tools';
function minifyCSS(css) {
const ast = parse(css);
return stringify(ast, { compress: true });
}
const css = `
body {
font-size: 12px;
color: #333;
margin: 0;
padding: 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
`;
const minified = minifyCSS(css);
console.log(minified);
// Output: "body{font-size:12px;color:#333;margin:0;padding:0}.container{max-width:1200px;margin:0 auto}"
```
### CSS Validation
```javascript
import { parse } from '@adobe/css-tools';
function validateCSS(css, filename = 'unknown') {
try {
const ast = parse(css, { source: filename });
return {
valid: true,
rules: ast.stylesheet.rules.length,
errors: []
};
} catch (error) {
return {
valid: false,
rules: 0,
errors: [{
message: error.message,
line: error.line,
column: error.column,
source: error.filename
}]
};
}
}
const result = validateCSS('body { color: red; } { invalid }', 'test.css');
console.log(result);
```
-83
View File
@@ -1,83 +0,0 @@
{
"name": "@adobe/css-tools",
"version": "4.5.0",
"description": "A modern CSS parser and stringifier with TypeScript support",
"source": "src/index.ts",
"main": "./dist/cjs/adobe-css-tools.cjs",
"module": "./dist/esm/adobe-css-tools.mjs",
"browser": "./dist/umd/adobe-css-tools.js",
"sideEffects": false,
"exports": {
".": {
"import": "./dist/esm/adobe-css-tools.mjs",
"require": "./dist/cjs/adobe-css-tools.cjs"
},
"./package.json": "./package.json"
},
"types": "./dist/esm/adobe-css-tools.d.mts",
"type": "module",
"files": [
"dist/cjs",
"dist/esm",
"dist/umd",
"src",
"README.md",
"docs/"
],
"devDependencies": {
"@arethetypeswrong/cli": "^0.18.2",
"@biomejs/biome": "^2.1.2",
"@rollup/plugin-commonjs": "^29.0.0",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-terser": "^1.0.0",
"@rollup/plugin-typescript": "^12.1.4",
"@types/benchmark": "^2.1.1",
"@types/bytes": "^3.1.5",
"@types/jest": "^30.0.0",
"@types/node": "^25.0.3",
"benchmark": "^2.1.4",
"bytes": "^3.1.0",
"jest": "^30.0.5",
"rollup": "^4.45.1",
"rollup-plugin-dts": "^6.2.1",
"ts-jest": "^29.4.10",
"tslib": "^2.8.1",
"typescript": "^6.0.2"
},
"scripts": {
"benchmark": "npm run build && node benchmark/index.mjs",
"test": "jest",
"clean": "rm -rf ./dist",
"build": "rollup -c",
"fix": "biome check --write",
"check-package-bundle": "attw --pack .",
"lint": "biome check",
"prebuild": "npm run clean",
"prepack": "rollup -c",
"posttest": "npm run lint",
"postbuild": "tar -czf dist/archive.tgz LICENSE README.md dist/cjs dist/esm dist/umd docs package.json && npx attw --format ascii dist/archive.tgz && rm dist/archive.tgz"
},
"author": "TJ Holowaychuk <tj@vision-media.ca>",
"contributors": [
"Jean-Philippe Zolesio <holblin@gmail.com>"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/adobe/css-tools.git"
},
"keywords": [
"css",
"parser",
"stringifier",
"stylesheet",
"ast",
"typescript",
"css-parser",
"css-ast",
"css-tools",
"minify",
"format",
"preprocessor"
]
}
-22
View File
@@ -1,22 +0,0 @@
export default class CssParseError extends Error {
readonly reason: string;
readonly filename?: string;
readonly line: number;
readonly column: number;
readonly source: string;
constructor(
filename: string,
msg: string,
lineno: number,
column: number,
css: string,
) {
super(`${filename}:${lineno}:${column}: ${msg}`);
this.reason = msg;
this.filename = filename;
this.line = lineno;
this.column = column;
this.source = css;
}
}
-18
View File
@@ -1,18 +0,0 @@
/**
* Store position information for a node
*/
export default class Position {
start: { line: number; column: number };
end: { line: number; column: number };
source?: string;
constructor(
start: { line: number; column: number },
end: { line: number; column: number },
source: string,
) {
this.start = start;
this.end = end;
this.source = source;
}
}
-8
View File
@@ -1,8 +0,0 @@
import { default as parseFn } from './parse';
import { default as stringifyFn } from './stringify';
export const parse = parseFn;
export const stringify = stringifyFn;
export * from './CssParseError';
export * from './CssPosition';
export * from './type';
export default { parse, stringify };
File diff suppressed because it is too large. Load diff
Loaded 100 of 8170 files, more files were not shown because too many files have changed in this diff. Show more