mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Refactor local emulator setup and enhance GitHub Actions workflow
- Updated the local emulator scripts to streamline command usage, replacing the previous docker-compose setup with a QEMU-based approach for improved performance and simplicity. - Removed the outdated `docker-compose.yaml` file and adjusted related scripts to utilize QEMU for running the emulator, enhancing clarity and maintainability. - Enhanced the GitHub Actions workflow to include a smoke test for the emulator, ensuring that services are healthy after startup. - Updated environment variable handling in the emulator scripts to reflect new port configurations, improving service accessibility. - Adjusted documentation to reflect the new emulator commands and usage patterns, providing clearer guidance for developers. These changes improve the usability and reliability of the local emulator setup, facilitating a smoother development experience.
This commit is contained in:
parent
9d0f7c1acd
commit
77d87fd2dd
84
.github/workflows/qemu-emulator-build.yaml
vendored
84
.github/workflows/qemu-emulator-build.yaml
vendored
@ -5,6 +5,7 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
publish:
|
||||
@ -51,6 +52,9 @@ jobs:
|
||||
EMULATOR_PROVISION_TIMEOUT=6000 \
|
||||
docker/local-emulator/qemu/build-image.sh ${{ matrix.arch }}
|
||||
|
||||
- name: Generate emulator env
|
||||
run: node docker/local-emulator/generate-env-development.mjs
|
||||
|
||||
- name: Start emulator and verify
|
||||
run: |
|
||||
chmod +x docker/local-emulator/qemu/run-emulator.sh
|
||||
@ -64,7 +68,7 @@ jobs:
|
||||
docker/local-emulator/qemu/run-emulator.sh status
|
||||
|
||||
- name: Stop emulator
|
||||
if: ${{ always() }}
|
||||
if: always()
|
||||
run: |
|
||||
EMULATOR_ARCH=${{ matrix.arch }} \
|
||||
docker/local-emulator/qemu/run-emulator.sh stop
|
||||
@ -82,9 +86,71 @@ jobs:
|
||||
retention-days: 30
|
||||
compression-level: 0
|
||||
|
||||
test:
|
||||
name: Smoke Test (${{ matrix.arch }})
|
||||
needs: build
|
||||
runs-on: ubicloud-standard-8
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install QEMU dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y qemu-system-x86 qemu-utils genisoimage socat
|
||||
|
||||
- name: Download built image
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: qemu-emulator-${{ matrix.arch }}
|
||||
path: docker/local-emulator/qemu/images/
|
||||
|
||||
- name: Generate emulator env
|
||||
run: node docker/local-emulator/generate-env-development.mjs
|
||||
|
||||
- name: Start emulator from artifact
|
||||
run: |
|
||||
chmod +x docker/local-emulator/qemu/run-emulator.sh docker/local-emulator/qemu/common.sh
|
||||
EMULATOR_ARCH=${{ matrix.arch }} \
|
||||
EMULATOR_READY_TIMEOUT=600 \
|
||||
docker/local-emulator/qemu/run-emulator.sh start
|
||||
|
||||
- name: Verify services are healthy
|
||||
run: |
|
||||
EMULATOR_ARCH=${{ matrix.arch }} \
|
||||
docker/local-emulator/qemu/run-emulator.sh status
|
||||
|
||||
- name: Smoke test — backend health
|
||||
run: curl -sf http://localhost:26701/health?db=1
|
||||
|
||||
- name: Smoke test — dashboard reachable
|
||||
run: curl -sf -o /dev/null -w "HTTP %{http_code}\n" http://localhost:26700/handler/sign-in
|
||||
|
||||
- name: Smoke test — MinIO health
|
||||
run: curl -sf http://localhost:26702/minio/health/live
|
||||
|
||||
- name: Smoke test — Inbucket reachable
|
||||
run: curl -sf -o /dev/null -w "HTTP %{http_code}\n" http://localhost:26703/
|
||||
|
||||
- name: Stop emulator
|
||||
if: always()
|
||||
run: |
|
||||
EMULATOR_ARCH=${{ matrix.arch }} \
|
||||
docker/local-emulator/qemu/run-emulator.sh stop
|
||||
|
||||
- name: Print serial log on failure
|
||||
if: failure()
|
||||
run: tail -100 docker/local-emulator/qemu/run/vm/serial.log 2>/dev/null || true
|
||||
|
||||
publish:
|
||||
name: Publish to GitHub Releases
|
||||
needs: build
|
||||
needs: [build, test]
|
||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || (github.event_name == 'workflow_dispatch' && inputs.publish)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@ -112,22 +178,22 @@ jobs:
|
||||
cp "$f" release/
|
||||
done
|
||||
|
||||
cat > release-notes.md <<'EOF'
|
||||
cat > release-notes.md <<EOF
|
||||
## QEMU Emulator Images
|
||||
|
||||
Built from `${{ github.ref_name }}` @ `${{ github.sha }}`
|
||||
Built from \`${BRANCH}\` @ \`${GITHUB_SHA}\`
|
||||
|
||||
### Images
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `stack-emulator-arm64.qcow2` | ARM64 emulator image |
|
||||
| `stack-emulator-amd64.qcow2` | AMD64 emulator image |
|
||||
| \`stack-emulator-arm64.qcow2\` | ARM64 emulator image |
|
||||
| \`stack-emulator-amd64.qcow2\` | AMD64 emulator image |
|
||||
|
||||
### Usage
|
||||
```bash
|
||||
\`\`\`bash
|
||||
stack emulator pull
|
||||
stack emulator run
|
||||
```
|
||||
stack emulator start
|
||||
\`\`\`
|
||||
EOF
|
||||
|
||||
ls -lh release/
|
||||
|
||||
@ -1,54 +0,0 @@
|
||||
# Stack Auth Local Emulator — all-in-one image via docker compose.
|
||||
# Ports follow the ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}XX convention.
|
||||
|
||||
services:
|
||||
stack-local:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: docker/local-emulator/Dockerfile
|
||||
image: stack-local-emulator
|
||||
ports:
|
||||
- "${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}28:5432" # PostgreSQL
|
||||
- "${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}29:2500" # Inbucket SMTP
|
||||
- "${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}05:9001" # Inbucket HTTP
|
||||
- "${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}30:1100" # Inbucket POP3
|
||||
- "${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}13:8071" # Svix
|
||||
- "${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}21:9090" # MinIO (S3-compatible)
|
||||
- "${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}25:8080" # QStash
|
||||
- "${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}36:8123" # ClickHouse HTTP
|
||||
- "${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}37:9009" # ClickHouse Native
|
||||
- "${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}01:8101" # Dashboard
|
||||
- "${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02:8102" # Backend
|
||||
volumes:
|
||||
- postgres-data:/data/postgres
|
||||
- redis-data:/data/redis
|
||||
- clickhouse-data:/data/clickhouse
|
||||
- minio-data:/data/minio
|
||||
- inbucket-data:/data/inbucket
|
||||
- "${HOME}:${HOME}"
|
||||
- "/tmp:/tmp"
|
||||
env_file: ./.env.development
|
||||
environment:
|
||||
# Port-prefixed URLs — need shell interpolation, can't go in env_file
|
||||
NEXT_PUBLIC_STACK_PORT_PREFIX: "${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}"
|
||||
NEXT_PUBLIC_STACK_API_URL: "http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02"
|
||||
NEXT_PUBLIC_STACK_DASHBOARD_URL: "http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}01"
|
||||
NEXT_PUBLIC_STACK_SVIX_SERVER_URL: "http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}13"
|
||||
STACK_S3_PUBLIC_ENDPOINT: "http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}21/stack-storage"
|
||||
STACK_EMAIL_MONITOR_VERIFICATION_CALLBACK_URL: "http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}01/handler/email-verification"
|
||||
STACK_OAUTH_MOCK_URL: "http://host.docker.internal:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}14"
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U postgres && curl -sf http://127.0.0.1:8123/ping && curl -fsS 'http://127.0.0.1:8102/health?db=1' >/dev/null && curl -fsS http://127.0.0.1:8101/handler/sign-in >/dev/null"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
start_period: 120s
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
redis-data:
|
||||
clickhouse-data:
|
||||
minio-data:
|
||||
inbucket-data:
|
||||
@ -13,6 +13,13 @@ VM_CPUS="${EMULATOR_CPUS:-4}"
|
||||
PORT_PREFIX="${PORT_PREFIX:-${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}}"
|
||||
READY_TIMEOUT="${EMULATOR_READY_TIMEOUT:-240}"
|
||||
|
||||
# Fixed host-side ports for the QEMU emulator (267xx range).
|
||||
# Only user-facing services are exposed; internal deps stay inside the VM.
|
||||
EMULATOR_DASHBOARD_PORT="${EMULATOR_DASHBOARD_PORT:-26700}"
|
||||
EMULATOR_BACKEND_PORT="${EMULATOR_BACKEND_PORT:-26701}"
|
||||
EMULATOR_MINIO_PORT="${EMULATOR_MINIO_PORT:-26702}"
|
||||
EMULATOR_INBUCKET_PORT="${EMULATOR_INBUCKET_PORT:-26703}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
@ -106,18 +113,13 @@ service_is_up() {
|
||||
}
|
||||
|
||||
deps_ready() {
|
||||
service_is_up "${PORT_PREFIX}28" tcp &&
|
||||
service_is_up "${PORT_PREFIX}05" http / &&
|
||||
service_is_up "${PORT_PREFIX}29" tcp &&
|
||||
service_is_up "${PORT_PREFIX}13" http /api/v1/health/ &&
|
||||
service_is_up "${PORT_PREFIX}36" http /ping &&
|
||||
service_is_up "${PORT_PREFIX}21" http /minio/health/live &&
|
||||
service_is_up "${PORT_PREFIX}25" http / 401
|
||||
service_is_up "$EMULATOR_MINIO_PORT" http /minio/health/live &&
|
||||
service_is_up "$EMULATOR_INBUCKET_PORT" http /
|
||||
}
|
||||
|
||||
app_ready() {
|
||||
service_is_up "${PORT_PREFIX}02" http "/health?db=1" &&
|
||||
service_is_up "${PORT_PREFIX}01" http /handler/sign-in
|
||||
service_is_up "$EMULATOR_BACKEND_PORT" http "/health?db=1" &&
|
||||
service_is_up "$EMULATOR_DASHBOARD_PORT" http /handler/sign-in
|
||||
}
|
||||
|
||||
all_ready() {
|
||||
@ -194,19 +196,11 @@ build_qemu_cmd() {
|
||||
esac
|
||||
|
||||
local netdev="user,id=net0"
|
||||
# Deps services
|
||||
netdev+=",hostfwd=tcp::${PORT_PREFIX}28-:5432"
|
||||
netdev+=",hostfwd=tcp::${PORT_PREFIX}29-:2500"
|
||||
netdev+=",hostfwd=tcp::${PORT_PREFIX}05-:9001"
|
||||
netdev+=",hostfwd=tcp::${PORT_PREFIX}30-:1100"
|
||||
netdev+=",hostfwd=tcp::${PORT_PREFIX}13-:8071"
|
||||
netdev+=",hostfwd=tcp::${PORT_PREFIX}21-:9090"
|
||||
netdev+=",hostfwd=tcp::${PORT_PREFIX}25-:8080"
|
||||
netdev+=",hostfwd=tcp::${PORT_PREFIX}36-:8123"
|
||||
netdev+=",hostfwd=tcp::${PORT_PREFIX}37-:9009"
|
||||
# App services
|
||||
netdev+=",hostfwd=tcp::${PORT_PREFIX}01-:${PORT_PREFIX}01"
|
||||
netdev+=",hostfwd=tcp::${PORT_PREFIX}02-:${PORT_PREFIX}02"
|
||||
# Only expose user-facing services; internal deps stay inside the VM.
|
||||
netdev+=",hostfwd=tcp::${EMULATOR_DASHBOARD_PORT}-:${PORT_PREFIX}01"
|
||||
netdev+=",hostfwd=tcp::${EMULATOR_BACKEND_PORT}-:${PORT_PREFIX}02"
|
||||
netdev+=",hostfwd=tcp::${EMULATOR_MINIO_PORT}-:9090"
|
||||
netdev+=",hostfwd=tcp::${EMULATOR_INBUCKET_PORT}-:9001"
|
||||
|
||||
QEMU_CMD=(
|
||||
"$qemu_bin"
|
||||
@ -251,11 +245,11 @@ tail_vm_logs() {
|
||||
}
|
||||
|
||||
ensure_ports_free() {
|
||||
local ports=("${PORT_PREFIX}01" "${PORT_PREFIX}02" "${PORT_PREFIX}05" "${PORT_PREFIX}13" "${PORT_PREFIX}21" "${PORT_PREFIX}25" "${PORT_PREFIX}28" "${PORT_PREFIX}29" "${PORT_PREFIX}30" "${PORT_PREFIX}36" "${PORT_PREFIX}37")
|
||||
local ports=("$EMULATOR_DASHBOARD_PORT" "$EMULATOR_BACKEND_PORT" "$EMULATOR_MINIO_PORT" "$EMULATOR_INBUCKET_PORT")
|
||||
local port
|
||||
for port in "${ports[@]}"; do
|
||||
if lsof -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1; then
|
||||
err "Port $port is already in use. Stop the Docker emulator or other services first."
|
||||
err "Port $port is already in use. Stop any conflicting services first."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
@ -297,7 +291,8 @@ cmd_start() {
|
||||
mkdir -p "$RUN_DIR"
|
||||
|
||||
info "Starting QEMU local emulator"
|
||||
info "Arch: $ARCH | Accel: $ACCEL | Prefix: $PORT_PREFIX"
|
||||
info "Arch: $ARCH | Accel: $ACCEL"
|
||||
info "Ports: Dashboard=$EMULATOR_DASHBOARD_PORT Backend=$EMULATOR_BACKEND_PORT MinIO=$EMULATOR_MINIO_PORT Inbucket=$EMULATOR_INBUCKET_PORT"
|
||||
|
||||
start_vm
|
||||
|
||||
@ -313,7 +308,9 @@ cmd_start() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "All services are green. The qcow2 overlay preserves emulator state across restarts, while /host stays a live host share outside the VM disk."
|
||||
log "All services are green."
|
||||
info "Dashboard: http://localhost:${EMULATOR_DASHBOARD_PORT}"
|
||||
info "Backend: http://localhost:${EMULATOR_BACKEND_PORT}"
|
||||
}
|
||||
|
||||
cmd_stop() {
|
||||
@ -354,14 +351,10 @@ cmd_status() {
|
||||
fi
|
||||
echo ""
|
||||
echo "Services:"
|
||||
print_service_status "Dashboard" "${PORT_PREFIX}01" http /handler/sign-in
|
||||
print_service_status "Backend" "${PORT_PREFIX}02" http "/health?db=1"
|
||||
print_service_status "PostgreSQL" "${PORT_PREFIX}28" tcp
|
||||
print_service_status "Inbucket HTTP" "${PORT_PREFIX}05" http /
|
||||
print_service_status "Svix" "${PORT_PREFIX}13" http /api/v1/health/
|
||||
print_service_status "MinIO" "${PORT_PREFIX}21" http /minio/health/live
|
||||
print_service_status "QStash" "${PORT_PREFIX}25" http / 401
|
||||
print_service_status "ClickHouse" "${PORT_PREFIX}36" http /ping
|
||||
print_service_status "Dashboard" "$EMULATOR_DASHBOARD_PORT" http /handler/sign-in
|
||||
print_service_status "Backend" "$EMULATOR_BACKEND_PORT" http "/health?db=1"
|
||||
print_service_status "MinIO" "$EMULATOR_MINIO_PORT" http /minio/health/live
|
||||
print_service_status "Inbucket HTTP" "$EMULATOR_INBUCKET_PORT" http /
|
||||
exit "$STATUS_FAILED"
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import net from 'net';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const PORT_PREFIX = process.env.NEXT_PUBLIC_STACK_PORT_PREFIX ?? '81';
|
||||
|
||||
type ServiceCheck = {
|
||||
name: string;
|
||||
description: string;
|
||||
@ -15,63 +12,30 @@ type ServiceCheck = {
|
||||
|
||||
const SERVICES: ServiceCheck[] = [
|
||||
{
|
||||
name: 'PostgreSQL',
|
||||
description: 'Primary database',
|
||||
port: Number(`${PORT_PREFIX}28`),
|
||||
protocol: 'tcp',
|
||||
},
|
||||
{
|
||||
name: 'Inbucket (HTTP)',
|
||||
description: 'Email capture UI',
|
||||
port: Number(`${PORT_PREFIX}05`),
|
||||
name: 'Stack Dashboard',
|
||||
description: 'Dashboard UI',
|
||||
port: 26700,
|
||||
protocol: 'http',
|
||||
httpPath: '/',
|
||||
},
|
||||
{
|
||||
name: 'Inbucket (SMTP)',
|
||||
description: 'Email SMTP server',
|
||||
port: Number(`${PORT_PREFIX}29`),
|
||||
protocol: 'tcp',
|
||||
},
|
||||
{
|
||||
name: 'Svix',
|
||||
description: 'Webhook delivery',
|
||||
port: Number(`${PORT_PREFIX}13`),
|
||||
protocol: 'http',
|
||||
httpPath: '/api/v1/health/',
|
||||
},
|
||||
{
|
||||
name: 'ClickHouse',
|
||||
description: 'Analytics database',
|
||||
port: Number(`${PORT_PREFIX}36`),
|
||||
protocol: 'http',
|
||||
httpPath: '/ping',
|
||||
},
|
||||
{
|
||||
name: 'MinIO (S3)',
|
||||
description: 'Object storage',
|
||||
port: Number(`${PORT_PREFIX}21`),
|
||||
protocol: 'http',
|
||||
httpPath: '/minio/health/live',
|
||||
},
|
||||
{
|
||||
name: 'QStash',
|
||||
description: 'Job queue',
|
||||
port: Number(`${PORT_PREFIX}25`),
|
||||
protocol: 'http',
|
||||
httpPath: '/',
|
||||
httpPath: '/handler/sign-in',
|
||||
},
|
||||
{
|
||||
name: 'Stack Backend',
|
||||
description: 'API server',
|
||||
port: Number(`${PORT_PREFIX}02`),
|
||||
port: 26701,
|
||||
protocol: 'http',
|
||||
httpPath: '/',
|
||||
httpPath: '/health?db=1',
|
||||
},
|
||||
{
|
||||
name: 'Stack Dashboard',
|
||||
description: 'Dashboard UI',
|
||||
port: Number(`${PORT_PREFIX}01`),
|
||||
name: 'MinIO (S3)',
|
||||
description: 'Object storage',
|
||||
port: 26702,
|
||||
protocol: 'http',
|
||||
httpPath: '/minio/health/live',
|
||||
},
|
||||
{
|
||||
name: 'Inbucket (HTTP)',
|
||||
description: 'Email capture UI',
|
||||
port: 26703,
|
||||
protocol: 'http',
|
||||
httpPath: '/',
|
||||
},
|
||||
@ -90,27 +54,10 @@ async function checkHttp(port: number, path: string, timeoutMs = 3000): Promise<
|
||||
}
|
||||
}
|
||||
|
||||
async function checkTcp(port: number, timeoutMs = 3000): Promise<{ up: boolean; latencyMs: number }> {
|
||||
const start = performance.now();
|
||||
return await new Promise((resolve) => {
|
||||
const socket = net.createConnection({ host: '127.0.0.1', port }, () => {
|
||||
socket.destroy();
|
||||
resolve({ up: true, latencyMs: Math.round(performance.now() - start) });
|
||||
});
|
||||
socket.on('error', () => resolve({ up: false, latencyMs: Math.round(performance.now() - start) }));
|
||||
socket.setTimeout(timeoutMs, () => {
|
||||
socket.destroy();
|
||||
resolve({ up: false, latencyMs: Math.round(performance.now() - start) });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const results = await Promise.all(
|
||||
SERVICES.map(async (svc) => {
|
||||
const check = svc.protocol === 'http'
|
||||
? await checkHttp(svc.port, svc.httpPath ?? '/')
|
||||
: await checkTcp(svc.port);
|
||||
const check = await checkHttp(svc.port, svc.httpPath ?? '/');
|
||||
return {
|
||||
name: svc.name,
|
||||
description: svc.description,
|
||||
|
||||
@ -163,16 +163,22 @@ export default function EmulatorStatusPage() {
|
||||
<Typography type="h4">Quick Start</Typography>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Typography className="text-sm">Start the all-in-one local emulator dependencies:</Typography>
|
||||
<Typography className="text-sm">Start the QEMU local emulator:</Typography>
|
||||
<pre className="bg-gray-100 dark:bg-gray-900 rounded p-3 text-xs font-mono overflow-x-auto">
|
||||
{`# Start (single container with all services)
|
||||
pnpm run emulator:compose up --detach --build
|
||||
{`# Pull the latest image and start the emulator
|
||||
pnpm run emulator:start
|
||||
|
||||
# Stop and remove volumes
|
||||
pnpm run emulator:compose down -v`}
|
||||
# Check service health
|
||||
pnpm run emulator:status
|
||||
|
||||
# Stop (data is preserved)
|
||||
pnpm run emulator:stop
|
||||
|
||||
# Reset for a fresh boot
|
||||
pnpm run emulator:reset`}
|
||||
</pre>
|
||||
<Typography className="text-sm text-gray-500">
|
||||
This single container replaces the 17+ containers from the full docker-compose setup.
|
||||
Dashboard: localhost:26700 | Backend: localhost:26701
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
18
package.json
18
package.json
@ -26,18 +26,12 @@
|
||||
"deps-compose": "docker compose -p stack-dependencies-${NEXT_PUBLIC_STACK_PORT_PREFIX:-81} -f docker/dependencies/docker.compose.yaml",
|
||||
"emulator:generate-env": "node ./docker/local-emulator/generate-env-development.mjs",
|
||||
"emulator:check-env": "node ./docker/local-emulator/generate-env-development.mjs --check",
|
||||
"emulator:compose": "docker compose -p stack-local-emulator-${NEXT_PUBLIC_STACK_PORT_PREFIX:-81} -f docker/local-emulator/docker-compose.yaml",
|
||||
"emulator:start": "pnpm pre && pnpm run emulator:generate-env && pnpm run emulator:compose up --detach --build && pnpm run emulator:wait-ready && echo \"\\nLocal emulator started. Dashboard: http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}01 Backend: http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02. 'pnpm run emulator:stop' to stop.\"",
|
||||
"emulator:stop": "pnpm run emulator:compose kill && pnpm run emulator:compose down -v",
|
||||
"emulator:restart": "pnpm pre && pnpm run emulator:stop && pnpm run emulator:start",
|
||||
"emulator:wait-ready": "pnpx wait-on http-get://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02/health?db=1 http-get://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}01/handler/sign-in",
|
||||
"emulator:wait-postgres": "until pg_isready -h localhost -p ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}28 2>/dev/null; do sleep 1; done",
|
||||
"emulator:qemu:build": "docker/local-emulator/qemu/build-image.sh",
|
||||
"emulator:qemu:start": "pnpm run emulator:generate-env && docker/local-emulator/qemu/run-emulator.sh start",
|
||||
"emulator:qemu:stop": "docker/local-emulator/qemu/run-emulator.sh stop",
|
||||
"emulator:qemu:reset": "docker/local-emulator/qemu/run-emulator.sh reset",
|
||||
"emulator:qemu:status": "docker/local-emulator/qemu/run-emulator.sh status",
|
||||
"emulator:qemu:bench": "docker/local-emulator/qemu/run-emulator.sh bench",
|
||||
"emulator:start": "pnpm run emulator:generate-env && docker/local-emulator/qemu/run-emulator.sh start",
|
||||
"emulator:stop": "docker/local-emulator/qemu/run-emulator.sh stop",
|
||||
"emulator:reset": "docker/local-emulator/qemu/run-emulator.sh reset",
|
||||
"emulator:status": "docker/local-emulator/qemu/run-emulator.sh status",
|
||||
"emulator:build": "docker/local-emulator/qemu/build-image.sh",
|
||||
"emulator:bench": "docker/local-emulator/qemu/run-emulator.sh bench",
|
||||
"stop-deps": "POSTGRES_DELAY_MS=0 pnpm run deps-compose kill && POSTGRES_DELAY_MS=0 pnpm run deps-compose down -v",
|
||||
"wait-until-postgres-is-ready:pg_isready": "until pg_isready -h localhost -p ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}28 && pg_isready -h localhost -p ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}34; do sleep 1; done",
|
||||
"wait-until-postgres-is-ready": "command -v pg_isready >/dev/null 2>&1 && pnpm run wait-until-postgres-is-ready:pg_isready || sleep 10 # not everyone has pg_isready installed, so we fallback to sleeping",
|
||||
|
||||
@ -4,70 +4,7 @@ import { existsSync, mkdirSync, renameSync, unlinkSync } from "fs";
|
||||
import { join, resolve } from "path";
|
||||
import { CliError } from "../lib/errors.js";
|
||||
|
||||
const DEFAULT_REPO = "stack-auth/stack-auth";
|
||||
const DEFAULT_BRANCH = "dev";
|
||||
const EMULATOR_ARCHES = ["arm64", "amd64"] as const;
|
||||
|
||||
type EmulatorArch = typeof EMULATOR_ARCHES[number];
|
||||
|
||||
function detectArch(): EmulatorArch {
|
||||
switch (process.arch) {
|
||||
case "arm64": {
|
||||
return "arm64";
|
||||
}
|
||||
case "x64": {
|
||||
return "amd64";
|
||||
}
|
||||
default: {
|
||||
throw new CliError(`Unsupported architecture: ${process.arch}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findQemuDir(): string {
|
||||
const candidates = [
|
||||
resolve(process.cwd(), "docker/local-emulator/qemu"),
|
||||
resolve(process.cwd(), "../docker/local-emulator/qemu"),
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (existsSync(join(candidate, "run-emulator.sh"))) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
throw new CliError(
|
||||
"Could not find QEMU emulator directory. Run this from the stack-auth repo root."
|
||||
);
|
||||
}
|
||||
|
||||
function runCommand(cwd: string, command: string, args: string[], env?: Record<string, string>): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
stdio: "inherit",
|
||||
env: { ...process.env, ...env },
|
||||
cwd,
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(new CliError(`${command} exited with code ${code}`));
|
||||
});
|
||||
child.on("error", (err) => {
|
||||
reject(new CliError(`Failed to run ${command}: ${err.message}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function runScript(qemuDir: string, script: string, args: string[], env?: Record<string, string>): Promise<void> {
|
||||
return runCommand(qemuDir, join(qemuDir, script), args, env);
|
||||
}
|
||||
|
||||
function runEmulatorAction(action: string, env?: Record<string, string>): Promise<void> {
|
||||
return runScript(findQemuDir(), "run-emulator.sh", [action], env);
|
||||
}
|
||||
|
||||
function ghRelease(args: string[]): string {
|
||||
function gh(args: string[]): string {
|
||||
try {
|
||||
return execFileSync("gh", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
||||
} catch (err: unknown) {
|
||||
@ -78,128 +15,124 @@ function ghRelease(args: string[]): string {
|
||||
}
|
||||
}
|
||||
|
||||
function isValidEmulatorArch(arch: string): arch is EmulatorArch {
|
||||
return arch === "arm64" || arch === "amd64";
|
||||
}
|
||||
|
||||
function parseEmulatorArch(arch: string | undefined): EmulatorArch {
|
||||
const resolvedArch = arch ?? detectArch();
|
||||
if (isValidEmulatorArch(resolvedArch)) {
|
||||
return resolvedArch;
|
||||
function findQemuDir(): string {
|
||||
for (const rel of ["docker/local-emulator/qemu", "../docker/local-emulator/qemu"]) {
|
||||
const dir = resolve(process.cwd(), rel);
|
||||
if (existsSync(join(dir, "run-emulator.sh"))) return dir;
|
||||
}
|
||||
throw new CliError(`Invalid --arch: ${resolvedArch}; expected one of: ${EMULATOR_ARCHES.join(", ")}.`);
|
||||
throw new CliError("Could not find QEMU emulator directory. Run this from the stack-auth repo root.");
|
||||
}
|
||||
|
||||
async function pullImage(arch: EmulatorArch, opts: { repo?: string; branch?: string; tag?: string } = {}) {
|
||||
const repo = opts.repo ?? DEFAULT_REPO;
|
||||
const branch = opts.branch ?? DEFAULT_BRANCH;
|
||||
function runEmulator(action: string, env?: Record<string, string>): Promise<void> {
|
||||
const qemuDir = findQemuDir();
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(join(qemuDir, "run-emulator.sh"), [action], {
|
||||
stdio: "inherit",
|
||||
env: { ...process.env, ...env },
|
||||
cwd: qemuDir,
|
||||
});
|
||||
child.on("close", (code) => code === 0 ? resolve() : reject(new CliError(`run-emulator.sh ${action} exited with code ${code}`)));
|
||||
child.on("error", (err) => reject(new CliError(`Failed to run run-emulator.sh: ${err.message}`)));
|
||||
});
|
||||
}
|
||||
|
||||
function resolveArch(raw?: string): "arm64" | "amd64" {
|
||||
const arch = raw ?? (process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "amd64" : null);
|
||||
if (arch === "arm64" || arch === "amd64") return arch;
|
||||
throw new CliError(`Invalid architecture: ${raw ?? process.arch}. Expected arm64 or amd64.`);
|
||||
}
|
||||
|
||||
function pullRelease(arch: "arm64" | "amd64", opts: { repo?: string; branch?: string; tag?: string } = {}) {
|
||||
const repo = opts.repo ?? "stack-auth/stack-auth";
|
||||
const branch = opts.branch ?? "dev";
|
||||
const tag = opts.tag ?? `emulator-${branch}-latest`;
|
||||
const asset = `stack-emulator-${arch}.qcow2`;
|
||||
|
||||
const qemuDir = findQemuDir();
|
||||
const imageDir = join(qemuDir, "images");
|
||||
const imageDir = join(findQemuDir(), "images");
|
||||
mkdirSync(imageDir, { recursive: true });
|
||||
|
||||
const dest = join(imageDir, asset);
|
||||
const tmpDest = `${dest}.download`;
|
||||
|
||||
console.log(`Pulling image for ${arch} from release ${tag}...`);
|
||||
|
||||
console.log(`Pulling ${asset} from release ${tag}...`);
|
||||
try {
|
||||
execFileSync("gh", [
|
||||
"release",
|
||||
"download",
|
||||
tag,
|
||||
"--repo",
|
||||
repo,
|
||||
"--pattern",
|
||||
asset,
|
||||
"--output",
|
||||
tmpDest,
|
||||
"--clobber",
|
||||
], { stdio: "inherit" });
|
||||
execFileSync("gh", ["release", "download", tag, "--repo", repo, "--pattern", asset, "--output", tmpDest, "--clobber"], { stdio: "inherit" });
|
||||
} catch (err) {
|
||||
if (existsSync(tmpDest)) unlinkSync(tmpDest);
|
||||
const reason = err instanceof Error
|
||||
? (err.stack ?? err.message)
|
||||
: String(err);
|
||||
throw new CliError(
|
||||
`Failed to download ${asset} from release ${tag}: ${reason}\nRun 'stack emulator list-releases' to see available releases.`
|
||||
);
|
||||
throw new CliError(`Failed to download ${asset} from release ${tag}: ${err instanceof Error ? err.message : err}\nRun 'stack emulator list-releases' to see available releases.`);
|
||||
}
|
||||
|
||||
renameSync(tmpDest, dest);
|
||||
console.log(`Downloaded: ${dest}`);
|
||||
}
|
||||
|
||||
export function registerEmulatorCommand(program: Command) {
|
||||
const emulator = program
|
||||
.command("emulator")
|
||||
.description("Manage the QEMU local emulator");
|
||||
const emulator = program.command("emulator").description("Manage the QEMU local emulator");
|
||||
|
||||
emulator
|
||||
.command("pull")
|
||||
.description("Download the latest emulator image from GitHub Releases")
|
||||
.option("--arch <arch>", `Target architecture (arm64 or amd64, default: current system arch)`)
|
||||
.option("--branch <branch>", `Release branch (default: ${DEFAULT_BRANCH})`)
|
||||
.description("Download an emulator image from GitHub Releases or a PR build")
|
||||
.option("--arch <arch>", "Target architecture (default: current system arch)")
|
||||
.option("--branch <branch>", "Release branch (default: dev)")
|
||||
.option("--tag <tag>", "Specific release tag (default: latest)")
|
||||
.option("--repo <repo>", `GitHub repository (default: ${DEFAULT_REPO})`)
|
||||
.option("--repo <repo>", "GitHub repository (default: stack-auth/stack-auth)")
|
||||
.option("--pr <number>", "Pull from a PR's CI artifacts")
|
||||
.option("--run <id>", "Pull from a specific workflow run's artifacts")
|
||||
.action(async (opts) => {
|
||||
const arch = parseEmulatorArch(opts.arch);
|
||||
await pullImage(arch, {
|
||||
repo: opts.repo,
|
||||
branch: opts.branch,
|
||||
tag: opts.tag,
|
||||
});
|
||||
const arch = resolveArch(opts.arch);
|
||||
const repo = opts.repo ?? "stack-auth/stack-auth";
|
||||
|
||||
if (opts.run || opts.pr) {
|
||||
let runId = opts.run as string | undefined;
|
||||
if (!runId) {
|
||||
console.log(`Finding latest successful build for PR #${opts.pr}...`);
|
||||
const { headRefName } = JSON.parse(gh(["pr", "view", opts.pr, "--repo", repo, "--json", "headRefName"]));
|
||||
const runs = JSON.parse(gh(["run", "list", "--repo", repo, "--workflow", "qemu-emulator-build.yaml", "--branch", headRefName, "--status", "success", "--limit", "1", "--json", "databaseId"]));
|
||||
if (runs.length === 0) throw new CliError(`No successful build found for PR #${opts.pr} (branch: ${headRefName}).`);
|
||||
runId = String(runs[0].databaseId);
|
||||
}
|
||||
|
||||
const imageDir = join(findQemuDir(), "images");
|
||||
mkdirSync(imageDir, { recursive: true });
|
||||
const dest = join(imageDir, `stack-emulator-${arch}.qcow2`);
|
||||
if (existsSync(dest)) unlinkSync(dest);
|
||||
console.log(`Downloading qemu-emulator-${arch} from workflow run ${runId}...`);
|
||||
try {
|
||||
execFileSync("gh", ["run", "download", runId, "--repo", repo, "--name", `qemu-emulator-${arch}`, "--dir", imageDir], { stdio: "inherit" });
|
||||
} catch (err) {
|
||||
throw new CliError(`Failed to download artifact from run ${runId}: ${err instanceof Error ? err.message : err}`);
|
||||
}
|
||||
if (!existsSync(dest)) throw new CliError(`Expected image not found at ${dest} after download.`);
|
||||
console.log(`Downloaded: ${dest}`);
|
||||
} else {
|
||||
pullRelease(arch, { repo, branch: opts.branch, tag: opts.tag });
|
||||
}
|
||||
});
|
||||
|
||||
emulator
|
||||
.command("start")
|
||||
.description("Start the emulator in the background (auto-pulls the latest image if none exists)")
|
||||
.option("--arch <arch>", "Target architecture (arm64 or amd64, default: current system arch). Using a non-native architecture will use software emulation and be significantly slower.")
|
||||
.option("--arch <arch>", "Target architecture (default: current system arch). Non-native uses software emulation and is significantly slower.")
|
||||
.action(async (opts) => {
|
||||
const arch = parseEmulatorArch(opts.arch);
|
||||
const qemuDir = findQemuDir();
|
||||
const img = join(qemuDir, "images", `stack-emulator-${arch}.qcow2`);
|
||||
|
||||
const arch = resolveArch(opts.arch);
|
||||
const img = join(findQemuDir(), "images", `stack-emulator-${arch}.qcow2`);
|
||||
if (!existsSync(img)) {
|
||||
console.log("No emulator image found. Pulling latest...");
|
||||
await pullImage(arch);
|
||||
pullRelease(arch);
|
||||
}
|
||||
|
||||
await runScript(qemuDir, "run-emulator.sh", ["start"], { EMULATOR_ARCH: arch });
|
||||
await runEmulator("start", { EMULATOR_ARCH: arch });
|
||||
});
|
||||
|
||||
emulator
|
||||
.command("stop")
|
||||
.description("Stop the emulator (data is preserved; use 'reset' to clear all state)")
|
||||
.action(() => runEmulatorAction("stop"));
|
||||
|
||||
emulator
|
||||
.command("reset")
|
||||
.description("Reset emulator state for a fresh boot")
|
||||
.action(() => runEmulatorAction("reset"));
|
||||
|
||||
emulator
|
||||
.command("status")
|
||||
.description("Show emulator and service health")
|
||||
.action(() => runEmulatorAction("status"));
|
||||
emulator.command("stop").description("Stop the emulator (data preserved; use 'reset' to clear)").action(() => runEmulator("stop"));
|
||||
emulator.command("reset").description("Reset emulator state for a fresh boot").action(() => runEmulator("reset"));
|
||||
emulator.command("status").description("Show emulator and service health").action(() => runEmulator("status"));
|
||||
|
||||
emulator
|
||||
.command("list-releases")
|
||||
.description("List available emulator releases")
|
||||
.option("--repo <repo>", `GitHub repository (default: ${DEFAULT_REPO})`)
|
||||
.action(async (opts) => {
|
||||
const repo = opts.repo || DEFAULT_REPO;
|
||||
.option("--repo <repo>", "GitHub repository (default: stack-auth/stack-auth)")
|
||||
.action((opts) => {
|
||||
const repo = opts.repo ?? "stack-auth/stack-auth";
|
||||
console.log(`Available emulator releases from ${repo}:\n`);
|
||||
const output = ghRelease(["release", "list", "--repo", repo, "--limit", "20"]);
|
||||
const lines = output.split("\n").filter((l) => l.toLowerCase().includes("emulator"));
|
||||
if (lines.length === 0) {
|
||||
console.log("No emulator releases found.");
|
||||
} else {
|
||||
for (const line of lines) {
|
||||
console.log(line);
|
||||
}
|
||||
}
|
||||
const lines = gh(["release", "list", "--repo", repo, "--limit", "20"]).split("\n").filter((l) => l.toLowerCase().includes("emulator"));
|
||||
if (lines.length === 0) console.log("No emulator releases found.");
|
||||
else for (const line of lines) console.log(line);
|
||||
});
|
||||
}
|
||||
|
||||
@ -4976,14 +4976,6 @@ packages:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@isaacs/balanced-match@4.0.1':
|
||||
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
'@isaacs/brace-expansion@5.0.0':
|
||||
resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
@ -13581,10 +13573,6 @@ packages:
|
||||
resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
minimatch@10.1.1:
|
||||
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
minimatch@10.2.4:
|
||||
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
@ -19873,12 +19861,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 20.17.6
|
||||
|
||||
'@isaacs/balanced-match@4.0.1': {}
|
||||
|
||||
'@isaacs/brace-expansion@5.0.0':
|
||||
dependencies:
|
||||
'@isaacs/balanced-match': 4.0.1
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
@ -25529,7 +25511,7 @@ snapshots:
|
||||
'@ts-morph/common@0.27.0':
|
||||
dependencies:
|
||||
fast-glob: 3.3.3
|
||||
minimatch: 10.1.1
|
||||
minimatch: 10.2.4
|
||||
path-browserify: 1.0.1
|
||||
|
||||
'@turf/boolean-point-in-polygon@7.1.0':
|
||||
@ -28542,7 +28524,7 @@ snapshots:
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
||||
eslint-plugin-react: 7.37.2(eslint@8.57.1)
|
||||
eslint-plugin-react-hooks: 5.1.0(eslint@8.57.1)
|
||||
@ -28566,7 +28548,7 @@ snapshots:
|
||||
debug: 4.4.3
|
||||
enhanced-resolve: 5.17.0
|
||||
eslint: 8.57.1
|
||||
eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1)
|
||||
eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1)
|
||||
fast-glob: 3.3.2
|
||||
get-tsconfig: 4.8.1
|
||||
@ -28616,7 +28598,7 @@ snapshots:
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1):
|
||||
eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
@ -28676,7 +28658,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1)
|
||||
eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.15.1
|
||||
is-glob: 4.0.3
|
||||
@ -29788,7 +29770,7 @@ snapshots:
|
||||
|
||||
glob@13.0.0:
|
||||
dependencies:
|
||||
minimatch: 10.1.1
|
||||
minimatch: 10.2.4
|
||||
minipass: 7.1.2
|
||||
path-scurry: 2.0.0
|
||||
|
||||
@ -31481,10 +31463,6 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.1
|
||||
|
||||
minimatch@10.1.1:
|
||||
dependencies:
|
||||
'@isaacs/brace-expansion': 5.0.0
|
||||
|
||||
minimatch@10.2.4:
|
||||
dependencies:
|
||||
brace-expansion: 5.0.4
|
||||
|
||||
Loading…
Reference in New Issue
Block a user