diff --git a/.github/workflows/eval.yml b/.github/workflows/eval.yml new file mode 100644 index 0000000..b3ec2fc --- /dev/null +++ b/.github/workflows/eval.yml @@ -0,0 +1,36 @@ +name: eval + +on: + pull_request: + paths: + - 'skills/java-to-typescript/**' + - '.github/workflows/eval.yml' + workflow_dispatch: + +jobs: + unit-and-e1: + runs-on: ubuntu-latest + defaults: + run: + working-directory: skills/java-to-typescript + steps: + - uses: actions/checkout@v4 + + - name: Setup Node 20 + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: skills/java-to-typescript/package-lock.json + + - name: Install dev deps + run: npm ci + + - name: Typecheck + run: npm run typecheck + + - name: Unit tests (scripts/lib + scripts) + run: npx vitest run scripts/ + + - name: E1 (analyze accuracy) eval + run: npx vitest run evals/__tests__/runner.test.ts diff --git a/.gitignore b/.gitignore index 0b719a4..fe270c4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ target/ .idea/ *.iml .vscode/ +.project +.settings/ +.classpath .DS_Store *.log /tmp/ @@ -11,3 +14,9 @@ target/ # skill-creator iteration artifacts (sibling to each skill, named -workspace/) *-workspace/ + +# java-to-typescript skill: TS scripts / evals build artifacts +skills/java-to-typescript/node_modules/ +skills/java-to-typescript/dist/ +skills/java-to-typescript/.vitest-cache/ +skills/java-to-typescript/coverage/ diff --git a/README.md b/README.md index 4ca992e..9ee066e 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ A monorepo of [Claude Code](https://docs.claude.com/claude-code) / Agent SDK ski | Skill | Maven coordinates | What it does | |---|---|---| | [`gitlab-helper`](skills/gitlab-helper) | `io.github.randomcodespace.ai:gitlab-helper` | GitLab CI/CD, pipelines, runners, and API automation. Version-aware doc grounding + `glab`/`python-gitlab` automation. | +| [`java-to-typescript`](skills/java-to-typescript) | `io.github.randomcodespace.ai:java-to-typescript` | Migrate a Java service (Spring Boot, Quarkus, Micronaut, Spring MVC) to TypeScript with four-phase orchestration, deterministic plumbing scripts, and contract-parity verification. Air-gap-friendly. | ## Using a skill diff --git a/assembly/skill-bundle.xml b/assembly/skill-bundle.xml index 2814e76..62f8434 100644 --- a/assembly/skill-bundle.xml +++ b/assembly/skill-bundle.xml @@ -20,7 +20,13 @@ pom.xml - target/** + **/target/** + **/__tests__/** + **/*.test.ts + **/node_modules/** + **/.vitest-cache/** + **/coverage/** + **/dist/** diff --git a/pom.xml b/pom.xml index b2ffd35..d465b5d 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,7 @@ skills/gitlab-helper + skills/java-to-typescript diff --git a/skills/java-to-typescript/SKILL.md b/skills/java-to-typescript/SKILL.md new file mode 100644 index 0000000..84fb646 --- /dev/null +++ b/skills/java-to-typescript/SKILL.md @@ -0,0 +1,120 @@ +--- +name: java-to-typescript +description: Use when migrating a Java service (Spring Boot, Quarkus, Micronaut, Spring MVC) to TypeScript. Supports full rewrite, strangler-fig, and module-by-module modes; targets Node (default), Bun, or Deno on Express, Koa, Hono, or Restify, with contract-parity verification against the original Java service. +--- + +# java-to-typescript + +Migrate a Java HTTP service to TypeScript across four phases with explicit user gates. The LLM owns every translation decision; the bundled scripts only do deterministic plumbing (XML parsing, workspace scaffolding, HTTP capture, JSON diff). No runtime internet access is required. + +## When to use + +Invoke when the user asks to port, migrate, or rewrite a Java service in TypeScript. Detect Spring Boot / Quarkus / Micronaut / Spring MVC by scanning `pom.xml` or `build.gradle[.kts]` for the canonical dependency coords. + +Do not use for: GraphQL Java migrations, Akka/Pekko, Android, JNI, Java agents, or pure-library JARs without an HTTP surface. + +## Hard constraints + +1. The LLM does every translation. Scripts never translate, never decide a library choice, never write source code. They produce JSON, JSONL, or templated config files only. +2. No runtime internet. The registry at `references/library-map.yaml` is the only source of library-mapping truth. Unmapped libraries escalate to the user via `AskUserQuestion` — never silently auto-fill, never call `context7` at runtime. +3. Never skip a phase. Phase 2 is a hard user gate; Phase 4 is the acceptance gate. + +## The four phases + +### Phase 1 — Analyze (read-only) + +Run: + +``` +tsx scripts/pom-to-workspace.ts analyze --repo +``` + +This writes `/migration/analysis.json` with detected build system, modules, framework per module, dependency inventory, and any `unmappedDependencies`. Cross-reference dependencies against `references/library-map.yaml`. + +Consult: `references/library-map.yaml`, `references/frameworks/*.md`. + +### Phase 2 — Plan (hard user gate, diff preview) + +Ask the user (via your platform's interactive-prompt mechanism — `AskUserQuestion` in Claude Code; see `references/platform-adaptation.md` for Copilot CLI / Codex equivalents) for: + +- Migration mode (`full-rewrite | strangler-fig | module-by-module`) — consult `references/migration-modes.md`. +- Target TS framework per Java module (Express default; Koa, Hono, Restify selectable) — consult `references/targets/*.md`. +- Runtime (Node default; Bun, Deno opt-in) — consult `references/runtimes/*.md`. +- Package manager (npm default; pnpm, yarn, bun opt-in). +- DI library (tsyringe default). +- Validation library (zod default). + +For each entry in `analysis.json.unmappedDependencies`, ask the user (interactive prompt) for a target. Do not auto-resolve. Do not call `context7`. + +Write `/migration/plan.md` containing per-module decisions, dependency order, and a one-shot diff preview (per module: what gets created, what the projected `package.json` looks like, which Java files map to which TS files). + +**Gate:** wait for explicit user approval of `plan.md` before Phase 3. + +Consult: `references/migration-modes.md`, `references/targets/*.md`, `references/runtimes/*.md`, `references/build-layout.md`, `references/type-fidelity.md`. + +### Phase 3 — Port + +Order: + +1. Write `/migration/scaffold.json` (derived from `plan.md`). Run `tsx scripts/pom-to-workspace.ts scaffold --plan /migration/scaffold.json --out `. This emits root `package.json`, per-module `package.json`, `tsconfig.json`, `vitest.config.ts`, and `.gitignore`. No source code. + +2. Port deterministic surfaces first, per module: config (zod-validated env), DTOs (zod schemas), entities (Drizzle schema), validation rules. Consult `references/categories/{config,validation,persistence}.md` and `references/type-fidelity.md`. + +3. Port handlers one at a time, smallest module first. For each handler: read Java source → consult `references/frameworks/.md` + `references/targets/.md` → emit TS → run `tsc --noEmit` on the touched module → append a one-line entry to `/migration/port-log.md` recording any non-trivial decision. + +4. Port tests alongside each handler. Map JUnit→Vitest, Mockito→`vi.mock`, AssertJ→`expect`, Testcontainers→testcontainers-node. Consult `references/categories/testing.md`. + +5. Apply type-fidelity policy per `references/type-fidelity.md`. Deviations require a one-line `port-log.md` entry. + +6. **Parallelize independent modules** when the dependency graph permits — use whatever subagent/worker dispatch your platform offers. Modules with no cross-edges to in-flight work can be ported concurrently; modules that depend on an unfinished sibling MUST wait. The skill itself does not bundle a subagent runner; orchestration is the platform's job. (See `references/platform-adaptation.md` for the per-platform mechanism: Claude Code subagents, Copilot CLI workers, Codex CLI parallel tasks. If your platform has no parallel-dispatch primitive, port sequentially — correctness is unaffected.) + +**Non-negotiable patterns** (from `references/type-fidelity.md` §7.3): +- One handler per file. +- DTO ≡ zod schema. No interfaces on HTTP boundaries without a schema. +- Repository ≡ Drizzle query module. No faked JPA repository interfaces. +- Config ≡ single zod-parsed env object per module. No scattered `process.env.FOO`. +- Logger ≡ module-scoped pino child. No `console.log`. +- Errors ≡ subclasses of a per-module base class, mapped to HTTP status via single middleware. + +### Phase 4 — Verify (acceptance gate) + +The user starts both the Java service (`localhost:`) and the TS service (`localhost:`). The skill prints the expected commands and waits — it never boots services itself. + +1. Generate `/migration/corpus.jsonl` from Java controller signatures + sample DB state. Ask the user to review or extend before recording. + +2. Run: + + ``` + tsx scripts/record-fixtures.ts --java-base http://localhost: --corpus migration/corpus.jsonl --out migration/fixtures.jsonl + ``` + +3. Write `/migration/allowlist.json` declaring paths expected to differ (timestamps, generated IDs, trace headers). Schema: + + ```json + { "headers": ["x-request-id", "date", "traceparent"], + "bodyPaths": ["$.createdAt", "$.updatedAt"], + "arrayKeys": { "$.items": "id" } } + ``` + +4. Run: + + ``` + tsx scripts/replay-fixtures.ts --ts-base http://localhost: --fixtures migration/fixtures.jsonl --allowlist migration/allowlist.json --report migration/verify-report.md + ``` + +5. Run the ported Vitest suite from the TS workspace root. + +**Acceptance gate (both required):** zero unexpected diffs in `verify-report.md` AND all ported Vitest suites green. + +Consult: `references/categories/testing.md`. + +## Migration artifact location + +All artifacts (`analysis.json`, `plan.md`, `scaffold.json`, `corpus.jsonl`, `fixtures.jsonl`, `allowlist.json`, `port-log.md`, `verify-report.md`, `verify-report.json`) live in `/migration/`. On first run, the skill adds `migration/` to the target repo's `.gitignore`. + +## When stuck + +- Library not in registry: ask the user via `AskUserQuestion`. Do not invent a mapping. +- Build-system parse warnings (Gradle Kotlin DSL with dynamic deps): show the warning verbatim to the user; ask them to confirm the dependency list. +- Phase 4 unexpected diffs: report each diff with the Java side and TS side excerpted; return to Phase 3 for the responsible handler. +- Tests fail after porting: do not weaken assertions to make them pass. Diagnose root cause; if the Java behavior cannot be replicated, escalate. diff --git a/skills/java-to-typescript/evals/README.md b/skills/java-to-typescript/evals/README.md new file mode 100644 index 0000000..c8e674f --- /dev/null +++ b/skills/java-to-typescript/evals/README.md @@ -0,0 +1,98 @@ +# Evals + +How to run the eval suite for the `java-to-typescript` skill. + +## Eval types + +| ID | What it measures | Determinism | When it runs | +|---|---|---|---| +| E1 | Analyze accuracy (Phase 1) | deterministic | every PR | +| E2 | Plan reasonability (Phase 2) | LLM-judged | tag releases / on demand | +| E3 | Port quality (Phase 3) | hybrid (hard `tsc`+tests; idioms LLM-judged) | tag releases / on demand | +| E4 | Contract parity (Phase 4) | deterministic | manual smoke; CI optional | + +## Quick start + +```bash +# All evals against the default fixture (spring-boot-users) +npx tsx evals/runner.ts --eval all + +# Specific eval +npx tsx evals/runner.ts --eval E1 +npx tsx evals/runner.ts --eval E2 --runs 5 +npx tsx evals/runner.ts --eval E3 --runs 5 + +# Specific fixture +npx tsx evals/runner.ts --fixture spring-boot-users --eval E1 + +# Override the plan/port being judged (defaults to sample/plan.md and sample/ts-port/api) +npx tsx evals/runner.ts --eval E2 --plan /tmp/my-plan.md +npx tsx evals/runner.ts --eval E3 --ts-repo /tmp/my-ts-port +``` + +Vitest also exercises the deterministic + mock-mode paths: + +```bash +npx vitest run evals/__tests__/runner.test.ts +npx vitest run evals/__tests__/judge.test.ts +``` + +## Flags + +| Flag | Default | Notes | +|---|---|---| +| `--fixture ` | `spring-boot-users` | Picks fixture under `evals/fixtures//` | +| `--eval ` | `all` | `E1`, `E2`, `E3`, or `all` (E4 is invoked separately via `replay-fixtures.ts`) | +| `--runs ` | `1` for deterministic evals, `5` for LLM-judged | N runs aggregated into `mean ± stddev` | +| `--plan ` | `/sample/plan.md` | Plan content for E2 to judge | +| `--ts-repo ` | `/sample/ts-port/api` | TS workspace for E3 to gate + judge | + +## Judge modes (E2 + E3) + +The judge in `evals/judge.ts` runs in two modes: + +| Mode | Trigger | Behavior | +|---|---|---| +| **Mock** (default) | Either `MOCK_JUDGE` is unset/non-`0` OR `ANTHROPIC_API_KEY` is absent | Returns a deterministic score in 7-9 derived from `sha256(content + rubric)`. Free, fast, CI-safe, no network. | +| **Real** | `MOCK_JUDGE=0` AND `ANTHROPIC_API_KEY=sk-...` both set | Calls `claude-haiku-4-5-20251001` with the rubric + content + judge instructions, parses a `{score, rationale}` JSON response. | + +```bash +# Run E2 + E3 with the real Anthropic judge +MOCK_JUDGE=0 ANTHROPIC_API_KEY=sk-ant-... \ + npx tsx evals/runner.ts --eval all --runs 5 +``` + +The real-mode test in `evals/__tests__/judge.test.ts` is auto-skipped when the env vars aren't set, so CI never accidentally hits the API. + +## Variance handling + +LLM-judged evals (`E2`, `E3` idiom dimension) run N times (default 5). Output is `mean ± stddev`. Sample-stddev with `n-1` denominator. The rule for accepting a skill change as an improvement: **mean delta must exceed 1 stddev** of the prior baseline. + +Mock mode produces `stddev=0` because the hash is deterministic — useful for verifying the harness wiring, not for measuring real model judgment. + +## Sample inputs (reference shapes E2/E3 judge against) + +Per fixture, `sample/` holds reference inputs: + +- `sample/plan.md` — example of what a passing Phase 2 plan looks like +- `sample/ts-port//` — minimal but tsc-clean + vitest-green reference TS workspace + +These are *examples* for E2/E3 in mock mode. In real mode against a real migration, you'd pass `--plan /path/to/your/migration/plan.md` and `--ts-repo /path/to/the/produced/ts-workspace`. + +## Adding a new fixture + +1. `mkdir -p evals/fixtures//{java,expected,sample}`. +2. Populate `java/` with a minimal runnable Spring Boot / Quarkus / Micronaut / Spring MVC project. +3. Run analyze: `npx tsx scripts/pom-to-workspace.ts analyze --repo evals/fixtures//java`. +4. Copy `evals/fixtures//java/migration/analysis.json` to `evals/fixtures//expected/analysis.json`. +5. Write `expected/corpus.jsonl`, `expected/allowlist.json`, `expected/ts-shape.md`, `expected/plan-rubric.md`. +6. Hand-author `sample/plan.md` (covering every rubric item) + `sample/ts-port//` (a minimal TS port that compiles + has at least one passing test). +7. Re-run `evals/__tests__/runner.test.ts` to verify E1 + E2 + E3 against the new fixture. + +## CI integration + +`.github/workflows/eval.yml` runs typecheck + unit tests + E1 on every PR touching `skills/java-to-typescript/**`. E2/E3 in mock mode are exercised by the unit test suite (`runner.test.ts` + `judge.test.ts`). Real-mode E2/E3 against actual Claude API is opt-in only — run manually on tag releases with the env vars set, or wire a separate workflow keyed to `ANTHROPIC_API_KEY` secret if you want it on every release. + +## Air-gap note + +Evals never hit the public internet in mock mode. In real mode, only the Anthropic API is contacted (egress to `api.anthropic.com` over HTTPS). Java fixtures must compile against locally vendored Maven repositories (Artifactory / Nexus / proxied central). The eval runner itself only parses XML and connects to `localhost` for E4. diff --git a/skills/java-to-typescript/evals/__tests__/judge.test.ts b/skills/java-to-typescript/evals/__tests__/judge.test.ts new file mode 100644 index 0000000..9a0572e --- /dev/null +++ b/skills/java-to-typescript/evals/__tests__/judge.test.ts @@ -0,0 +1,102 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { judgeAgainstRubric, isMockMode } from '../judge.js'; + +const origMock = process.env.MOCK_JUDGE; +const origKey = process.env.ANTHROPIC_API_KEY; + +beforeEach(() => { + delete process.env.MOCK_JUDGE; + delete process.env.ANTHROPIC_API_KEY; +}); + +afterEach(() => { + if (origMock !== undefined) process.env.MOCK_JUDGE = origMock; + else delete process.env.MOCK_JUDGE; + if (origKey !== undefined) process.env.ANTHROPIC_API_KEY = origKey; + else delete process.env.ANTHROPIC_API_KEY; +}); + +describe('isMockMode', () => { + it('mock by default', () => { + expect(isMockMode()).toBe(true); + }); + it('mock when MOCK_JUDGE unset', () => { + process.env.ANTHROPIC_API_KEY = 'sk-test'; + expect(isMockMode()).toBe(true); + }); + it('mock when ANTHROPIC_API_KEY missing even if MOCK_JUDGE=0', () => { + process.env.MOCK_JUDGE = '0'; + expect(isMockMode()).toBe(true); + }); + it('real only when both set correctly', () => { + process.env.MOCK_JUDGE = '0'; + process.env.ANTHROPIC_API_KEY = 'sk-test'; + expect(isMockMode()).toBe(false); + }); +}); + +describe('judgeAgainstRubric (mock)', () => { + it('returns a deterministic score for the same input', async () => { + const args = { + content: 'plan body here', + rubricMd: '# rubric\n- item 1\n- item 2', + judgeInstructions: 'eval plan quality', + }; + const a = await judgeAgainstRubric(args); + const b = await judgeAgainstRubric(args); + expect(a.score).toBe(b.score); + expect(a.rationale).toBe(b.rationale); + }); + + it('returns a score in 7-9 range', async () => { + const result = await judgeAgainstRubric({ + content: 'x', + rubricMd: 'y', + judgeInstructions: 'z', + }); + expect(result.score).toBeGreaterThanOrEqual(7); + expect(result.score).toBeLessThanOrEqual(9); + }); + + it('respects maxScore', async () => { + const result = await judgeAgainstRubric({ + content: 'x', + rubricMd: 'y', + judgeInstructions: 'z', + maxScore: 5, + }); + expect(result.score).toBeLessThanOrEqual(5); + }); + + it('rationale mentions content/rubric lengths', async () => { + const result = await judgeAgainstRubric({ + content: 'abcdef', + rubricMd: '12345', + judgeInstructions: 'irrelevant', + }); + expect(result.rationale).toContain('content-length=6'); + expect(result.rationale).toContain('rubric-length=5'); + }); + + it('different content produces different (or possibly same) scores within range', async () => { + const r1 = await judgeAgainstRubric({ content: 'a', rubricMd: 'r', judgeInstructions: 'i' }); + const r2 = await judgeAgainstRubric({ content: 'totally different content', rubricMd: 'r', judgeInstructions: 'i' }); + // Both in range; not asserting inequality since hash collisions in 3-bucket space are common. + expect([7, 8, 9]).toContain(r1.score); + expect([7, 8, 9]).toContain(r2.score); + }); +}); + +describe.skipIf(!process.env.ANTHROPIC_API_KEY || process.env.MOCK_JUDGE !== '0')('judgeAgainstRubric (real)', () => { + it('calls the Anthropic API and returns a valid score', async () => { + const result = await judgeAgainstRubric({ + content: 'A short plan with one bullet.', + rubricMd: '# Rubric\n- A plan must contain at least one bullet (5 points)\n- A plan must mention "deployment" (5 points)', + judgeInstructions: 'Score the plan against the rubric. Be strict.', + }); + expect(result.score).toBeGreaterThanOrEqual(0); + expect(result.score).toBeLessThanOrEqual(10); + expect(result.rationale.length).toBeGreaterThan(0); + expect(result.raw).toBeDefined(); + }, 30_000); +}); diff --git a/skills/java-to-typescript/evals/__tests__/runner.test.ts b/skills/java-to-typescript/evals/__tests__/runner.test.ts new file mode 100644 index 0000000..d731eee --- /dev/null +++ b/skills/java-to-typescript/evals/__tests__/runner.test.ts @@ -0,0 +1,68 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { runE1, runE2, runWithVariance } from '../runner.js'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import * as fs from 'node:fs/promises'; +import * as os from 'node:os'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const fixture = path.join(here, '..', 'fixtures', 'spring-boot-users'); + +const origMock = process.env.MOCK_JUDGE; +const origKey = process.env.ANTHROPIC_API_KEY; + +beforeEach(() => { + delete process.env.MOCK_JUDGE; + delete process.env.ANTHROPIC_API_KEY; +}); + +afterEach(() => { + if (origMock !== undefined) process.env.MOCK_JUDGE = origMock; else delete process.env.MOCK_JUDGE; + if (origKey !== undefined) process.env.ANTHROPIC_API_KEY = origKey; else delete process.env.ANTHROPIC_API_KEY; +}); + +describe('runE1 (analyze accuracy)', () => { + it('matches expected analysis.json for spring-boot-users', async () => { + const expected = JSON.parse(await fs.readFile(path.join(fixture, 'expected', 'analysis.json'), 'utf8')); + const result = await runE1(path.join(fixture, 'java')); + expect(result).toEqual(expected); + }); +}); + +describe('runE2 (plan reasonability, mock judge)', () => { + it('reads the fixture rubric and judges a plan content', async () => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'e2-')); + const planPath = path.join(tmp, 'plan.md'); + await fs.writeFile(planPath, '# Plan\n- module: api (spring-boot)\n- module: core (spring-boot)\n- mode: full-rewrite\n'); + const result = await runE2(fixture, planPath); + expect(result.score).toBeGreaterThanOrEqual(7); + expect(result.score).toBeLessThanOrEqual(9); + expect(result.rationale).toContain('[mock judge]'); + }); +}); + +describe('runWithVariance', () => { + it('aggregates N runs into mean + stddev', async () => { + let counter = 0; + const agg = await runWithVariance(5, async () => { + counter += 1; + return { score: counter }; + }); + expect(agg.runs).toHaveLength(5); + expect(agg.scores).toEqual([1, 2, 3, 4, 5]); + expect(agg.mean).toBe(3); + expect(agg.stddev).toBeCloseTo(1.5811, 3); + }); + + it('single run returns stddev=0', async () => { + const agg = await runWithVariance(1, async () => ({ score: 7 })); + expect(agg.mean).toBe(7); + expect(agg.stddev).toBe(0); + }); + + it('uses overallScore when present (preferred over score)', async () => { + const agg = await runWithVariance(3, async () => ({ score: 1, overallScore: 9 })); + expect(agg.scores).toEqual([9, 9, 9]); + expect(agg.mean).toBe(9); + }); +}); diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/README.md b/skills/java-to-typescript/evals/fixtures/spring-boot-users/README.md new file mode 100644 index 0000000..9cc4ceb --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/README.md @@ -0,0 +1,22 @@ +# Fixture: spring-boot-users + +Minimal Spring Boot service used by the M0 eval suite. + +- **Modules:** `core` (entity + repository + service), `api` (controller + Spring Boot main) +- **Endpoints:** + - `GET /users/{id}` — fetch one user + - `POST /users` — create user +- **DB:** in-memory H2 (eval mode) so the fixture is self-contained +- **Java version:** 21 +- **Spring Boot version:** 3.3.x + +## How E1 (analyze accuracy) uses this fixture + +`evals/runner.ts runE1` calls `analyze(/java)` and deep-equals the result against `expected/analysis.json`. Any drift in the analyze script (or a new known library not yet added to the registry) shows up immediately. + +## How E4 (contract parity) uses this fixture + +Manual / out-of-CI: +1. `cd java && mvn spring-boot:run -pl api` +2. `record-fixtures --java-base http://localhost:8080 --corpus expected/corpus.jsonl --out /tmp/fixtures.jsonl` +3. (After porting) `replay-fixtures --ts-base http://localhost:3000 --fixtures /tmp/fixtures.jsonl --allowlist expected/allowlist.json --report /tmp/r.md` diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/expected/allowlist.json b/skills/java-to-typescript/evals/fixtures/spring-boot-users/expected/allowlist.json new file mode 100644 index 0000000..ffea048 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/expected/allowlist.json @@ -0,0 +1,5 @@ +{ + "headers": ["date", "x-request-id", "traceparent", "content-length"], + "bodyPaths": ["$.createdAt"], + "arrayKeys": {} +} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/expected/analysis.json b/skills/java-to-typescript/evals/fixtures/spring-boot-users/expected/analysis.json new file mode 100644 index 0000000..0324e28 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/expected/analysis.json @@ -0,0 +1,32 @@ +{ + "buildSystem": "maven", + "rootGroupId": "com.example", + "modules": [ + { + "path": "api", + "artifactId": "api", + "packaging": "jar", + "dependencies": [ + { "groupId": "org.springframework.boot", "artifactId": "spring-boot-starter-web", "scope": "compile" }, + { "groupId": "org.springframework.boot", "artifactId": "spring-boot-starter-validation", "scope": "compile" }, + { "groupId": "com.example", "artifactId": "core", "scope": "compile" } + ], + "detectedFramework": "spring-boot" + }, + { + "path": "core", + "artifactId": "core", + "packaging": "jar", + "dependencies": [ + { "groupId": "org.springframework.boot", "artifactId": "spring-boot-starter-data-jpa", "scope": "compile" }, + { "groupId": "org.hibernate.orm", "artifactId": "hibernate-core", "scope": "compile" }, + { "groupId": "com.h2database", "artifactId": "h2", "scope": "compile" } + ], + "detectedFramework": "spring-boot" + } + ], + "unmappedDependencies": [ + { "groupId": "com.h2database", "artifactId": "h2", "usedBy": ["core"] } + ], + "parseWarnings": [] +} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/expected/corpus.jsonl b/skills/java-to-typescript/evals/fixtures/spring-boot-users/expected/corpus.jsonl new file mode 100644 index 0000000..284b386 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/expected/corpus.jsonl @@ -0,0 +1,3 @@ +{"name":"POST-user-create","method":"POST","path":"/users","headers":{"content-type":"application/json"},"body":{"name":"Ada","balance":"100.0000"}} +{"name":"GET-user-by-id","method":"GET","path":"/users/1","headers":{"accept":"application/json"}} +{"name":"GET-user-missing","method":"GET","path":"/users/99999","headers":{"accept":"application/json"}} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/expected/plan-rubric.md b/skills/java-to-typescript/evals/fixtures/spring-boot-users/expected/plan-rubric.md new file mode 100644 index 0000000..0f3e328 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/expected/plan-rubric.md @@ -0,0 +1,13 @@ +# Plan rubric for spring-boot-users + +Score 0–10. A passing plan must mention each of the following correctly. + +| Item | Points | +|---|---| +| Identifies both modules (api, core) and their detected framework (spring-boot) | 1 | +| Picks Express + tsyringe + zod as the target (or documents an explicit override) | 1 | +| Flags h2 as an unmapped dependency and asks the user for a target DB driver | 2 | +| Orders core before api (dependency-correct port order) | 2 | +| Includes a per-module projected package.json showing drizzle-orm + decimal.js + temporal-polyfill (or notes their absence and why) | 1 | +| Picks a migration mode (full-rewrite is the right call here; strangler would be over-engineering) | 1 | +| Includes the diff preview per module | 2 | diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/expected/ts-shape.md b/skills/java-to-typescript/evals/fixtures/spring-boot-users/expected/ts-shape.md new file mode 100644 index 0000000..3914cc8 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/expected/ts-shape.md @@ -0,0 +1,42 @@ +# Canonical TS shape for spring-boot-users port + +A passing port produces this structure. Idioms checked: one handler per file, DTO ≡ zod schema, repository ≡ drizzle query module, config ≡ zod env, logger ≡ pino child, errors ≡ subclass. + +``` +api/ +├── package.json # express, tsyringe, zod, pino, decimal.js, drizzle-orm +├── tsconfig.json # extends ../tsconfig.base.json +└── src/ + ├── server.ts # imports app, listens, graceful shutdown + ├── app.ts # buildApp() — express + middleware + routes + ├── wiring.ts # tsyringe registrations + ├── config.ts # zod-parsed env + ├── logger.ts # baseLogger + module export + ├── middleware/errors.ts # ZodError + UsersError → HTTP + ├── schemas/user.ts # CreateUserSchema, UserResponseSchema + ├── routes/get-user.ts # GET /users/:id handler + └── routes/create-user.ts # POST /users handler + +core/ +├── package.json # drizzle-orm, decimal.js +├── tsconfig.json +└── src/ + ├── db/ + │ ├── schema.ts # drizzle table for users + │ └── client.ts # pool + db export + ├── repos/user.ts # makeUserRepo: { findById, insert } + ├── services/user-service.ts # @injectable() with @inject('UserRepo') + └── errors.ts # UsersError base class +``` + +## Type-fidelity checks +- `User.id` is `bigint` (per "Long used as entity ID") +- `User.balance` is `Decimal` from decimal.js (per "BigDecimal always") +- `User.createdAt` is `Date` (per "Instant → Date") +- HTTP response serializes `id` as string and `balance` via `.toString()` + +## Forbidden shapes (each costs 2 points in the port rubric) +- An `AbstractUserService` or `BaseRepository` interface that's only used once +- A `UserRepositoryImpl` that wraps a drizzle call to fake JPA +- A `ResponseEntity` shim +- A `config.ts` that calls `process.env.X` more than once diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/api/pom.xml b/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/api/pom.xml new file mode 100644 index 0000000..e439f8c --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/api/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + com.example + users-parent + 1.0.0 + + api + + + + org.springframework.boot + spring-boot-starter-web + 3.3.4 + + + org.springframework.boot + spring-boot-starter-validation + 3.3.4 + + + com.example + core + 1.0.0 + + + diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/api/src/main/java/com/example/api/UserController.java b/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/api/src/main/java/com/example/api/UserController.java new file mode 100644 index 0000000..6d2a242 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/api/src/main/java/com/example/api/UserController.java @@ -0,0 +1,27 @@ +package com.example.api; + +import com.example.core.UserService; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/users") +public class UserController { + private final UserService svc; + + public UserController(UserService svc) { this.svc = svc; } + + @GetMapping("/{id}") + public ResponseEntity getById(@PathVariable Long id) { + return svc.findById(id) + .map(u -> ResponseEntity.ok(new UserDto(u.getId(), u.getName(), u.getBalance(), u.getCreatedAt()))) + .orElse(ResponseEntity.notFound().build()); + } + + @PostMapping + public ResponseEntity create(@Valid @RequestBody UserDto.CreateRequest req) { + var u = svc.create(req.name(), req.balance()); + return ResponseEntity.status(201).body(new UserDto(u.getId(), u.getName(), u.getBalance(), u.getCreatedAt())); + } +} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/api/src/main/java/com/example/api/UserDto.java b/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/api/src/main/java/com/example/api/UserDto.java new file mode 100644 index 0000000..e0ab7db --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/api/src/main/java/com/example/api/UserDto.java @@ -0,0 +1,14 @@ +package com.example.api; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; +import java.time.Instant; + +public record UserDto(Long id, String name, BigDecimal balance, Instant createdAt) { + + public record CreateRequest( + @NotBlank String name, + @NotNull BigDecimal balance + ) {} +} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/api/src/main/java/com/example/api/UsersApplication.java b/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/api/src/main/java/com/example/api/UsersApplication.java new file mode 100644 index 0000000..c44e960 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/api/src/main/java/com/example/api/UsersApplication.java @@ -0,0 +1,17 @@ +package com.example.api; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.context.annotation.ComponentScan; + +@SpringBootApplication +@ComponentScan(basePackages = {"com.example.api", "com.example.core"}) +@EntityScan(basePackages = "com.example.core") +@EnableJpaRepositories(basePackages = "com.example.core") +public class UsersApplication { + public static void main(String[] args) { + SpringApplication.run(UsersApplication.class, args); + } +} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/api/src/main/resources/application.yml b/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/api/src/main/resources/application.yml new file mode 100644 index 0000000..ac68921 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/api/src/main/resources/application.yml @@ -0,0 +1,12 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 + driver-class-name: org.h2.Driver + username: sa + password: '' + jpa: + hibernate: + ddl-auto: create-drop + show-sql: false +server: + port: 8080 diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/core/pom.xml b/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/core/pom.xml new file mode 100644 index 0000000..0e1f871 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/core/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + com.example + users-parent + 1.0.0 + + core + + + + org.springframework.boot + spring-boot-starter-data-jpa + 3.3.4 + + + org.hibernate.orm + hibernate-core + 6.5.2.Final + + + com.h2database + h2 + 2.3.232 + + + diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/core/src/main/java/com/example/core/User.java b/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/core/src/main/java/com/example/core/User.java new file mode 100644 index 0000000..92161d3 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/core/src/main/java/com/example/core/User.java @@ -0,0 +1,34 @@ +package com.example.core; + +import jakarta.persistence.*; +import java.math.BigDecimal; +import java.time.Instant; + +@Entity +@Table(name = "users") +public class User { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Column(name = "balance", precision = 19, scale = 4, nullable = false) + private BigDecimal balance; + + @Column(name = "created_at", nullable = false) + private Instant createdAt; + + protected User() {} + + public User(String name, BigDecimal balance) { + this.name = name; + this.balance = balance; + this.createdAt = Instant.now(); + } + + public Long getId() { return id; } + public String getName() { return name; } + public BigDecimal getBalance() { return balance; } + public Instant getCreatedAt() { return createdAt; } +} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/core/src/main/java/com/example/core/UserRepository.java b/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/core/src/main/java/com/example/core/UserRepository.java new file mode 100644 index 0000000..c745574 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/core/src/main/java/com/example/core/UserRepository.java @@ -0,0 +1,6 @@ +package com.example.core; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { +} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/core/src/main/java/com/example/core/UserService.java b/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/core/src/main/java/com/example/core/UserService.java new file mode 100644 index 0000000..6ec6c7d --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/core/src/main/java/com/example/core/UserService.java @@ -0,0 +1,18 @@ +package com.example.core; + +import org.springframework.stereotype.Service; +import java.math.BigDecimal; +import java.util.Optional; + +@Service +public class UserService { + private final UserRepository repo; + + public UserService(UserRepository repo) { this.repo = repo; } + + public Optional findById(Long id) { return repo.findById(id); } + + public User create(String name, BigDecimal balance) { + return repo.save(new User(name, balance)); + } +} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/pom.xml b/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/pom.xml new file mode 100644 index 0000000..da97d72 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/java/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + com.example + users-parent + 1.0.0 + pom + + api + core + + + + 21 + 3.3.4 + + diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/plan.md b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/plan.md new file mode 100644 index 0000000..7383c0f --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/plan.md @@ -0,0 +1,126 @@ +# Migration Plan: spring-boot-users → TypeScript + +## 1. Detected source layout + +The Maven analyzer identified **two modules** under `com.example`: + +| Module | Path | Packaging | Detected framework | +|--------|-------|-----------|---------------------| +| core | core/ | jar | spring-boot | +| api | api/ | jar | spring-boot | + +Both modules are Spring Boot 3.3.x, Java 21. `api` depends on `core`. + +## 2. Target stack + +Mapping to the project default TypeScript stack: + +- **HTTP framework:** Express 5 (maps to `spring-boot-starter-web`) +- **DI container:** tsyringe (maps to Spring's `@Component` / `@Service`) +- **Validation:** zod (maps to `spring-boot-starter-validation` / `@Valid` + Jakarta Validation) +- **Logger:** pino (maps to Spring's logback default) +- **ORM:** drizzle-orm (maps to `spring-boot-starter-data-jpa` + Hibernate) +- **Decimal type:** decimal.js (maps to `java.math.BigDecimal`) +- **Date/time:** temporal-polyfill (maps to `java.time.Instant`) — used only if Instant escapes domain boundaries; here `Date` suffices for `createdAt`, so temporal-polyfill is **omitted** from the projected deps. + +No override requested; defaults apply. + +## 3. Unmapped dependencies — needs user input + +| Dependency | Module | Reason unmapped | +|----------------------|--------|------------------------------------------------------------------------------| +| `com.h2database:h2` | core | In-process H2 has no direct TS analog. Drizzle needs a real driver target. | + +**Question for the user:** which DB driver should the port target? +- (a) `postgres` (recommended for prod parity; use `pg` + `drizzle-orm/node-postgres`) +- (b) `better-sqlite3` (closest to H2's in-process feel; good for tests/dev) +- (c) other + +Pausing the plan until you confirm. The rest of the plan assumes **(a) postgres** for projection. + +## 4. Port order + +Dependency-correct order — `api` imports from `core`, so `core` must land first: + +1. **core** (entity + repository + service) +2. **api** (controller + bootstrap) + +## 5. Migration mode + +**`full-rewrite`** per module. Rationale: + +- The codebase is small (2 modules, ~5 source files each). +- No live traffic to preserve. +- Strangler-fig would require a long-lived JVM + Node side-by-side rig for marginal benefit and would over-engineer a fixture-sized service. + +## 6. Projected per-module `package.json` + +### core/package.json +```json +{ + "name": "@spring-boot-users/core", + "private": true, + "type": "module", + "dependencies": { + "drizzle-orm": "^0.36.0", + "decimal.js": "^10.4.3", + "pg": "^8.13.0", + "tsyringe": "^4.8.0", + "reflect-metadata": "^0.2.2" + }, + "devDependencies": { "typescript": "^5.6.0", "tsx": "^4.19.0", "vitest": "^2.1.0" } +} +``` + +### api/package.json +```json +{ + "name": "@spring-boot-users/api", + "private": true, + "type": "module", + "dependencies": { + "express": "^5.0.0", + "tsyringe": "^4.8.0", + "reflect-metadata": "^0.2.2", + "zod": "^3.23.0", + "pino": "^9.5.0", + "@spring-boot-users/core": "workspace:*" + }, + "devDependencies": { "typescript": "^5.6.0", "tsx": "^4.19.0", "vitest": "^2.1.0", "@types/express": "^5.0.0" } +} +``` + +`temporal-polyfill` is intentionally absent — no Instant fields escape the domain. + +## 7. Diff preview per module + +### core (Java → TS) +``` +- core/src/main/java/com/example/User.java (@Entity, Long id, BigDecimal balance, Instant createdAt) +- core/src/main/java/com/example/UserRepository.java (JpaRepository) +- core/src/main/java/com/example/UserService.java (@Service) ++ core/src/db/schema.ts (drizzle pgTable: bigint id, text name, numeric balance, timestamp createdAt) ++ core/src/db/client.ts (pg Pool + drizzle()) ++ core/src/repos/user.ts (makeUserRepo: findById, insert) ++ core/src/services/user-service.ts (@injectable UserService) ++ core/src/errors.ts (UsersError base class) +``` + +### api (Java → TS) +``` +- api/src/main/java/com/example/UserController.java (@RestController, @GetMapping, @PostMapping) +- api/src/main/java/com/example/Application.java (@SpringBootApplication) ++ api/src/server.ts (createServer + listen + graceful shutdown) ++ api/src/app.ts (buildApp(): express + middleware + routes) ++ api/src/wiring.ts (tsyringe container.register(...)) ++ api/src/config.ts (zod-parsed process.env, single call) ++ api/src/logger.ts (pino base + child) ++ api/src/middleware/errors.ts (ZodError → 400, UsersError → mapped status) ++ api/src/schemas/user.ts (CreateUserSchema, UserResponseSchema) ++ api/src/routes/get-user.ts (GET /users/:id) ++ api/src/routes/create-user.ts (POST /users) +``` + +## 8. Awaiting confirmation + +Confirm the DB driver choice in §3, then I'll proceed module-by-module per §4. diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/.gitignore b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/.gitignore new file mode 100644 index 0000000..de44ca7 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.vitest-cache/ diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/package-lock.json b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/package-lock.json new file mode 100644 index 0000000..05ad35a --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/package-lock.json @@ -0,0 +1,3251 @@ +{ + "name": "@spring-boot-users-sample/api", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@spring-boot-users-sample/api", + "version": "0.0.0", + "dependencies": { + "decimal.js": "^10.4.3", + "drizzle-orm": "^0.36.0", + "express": "^4.21.0", + "pino": "^9.5.0", + "reflect-metadata": "^0.2.2", + "tsyringe": "^4.8.0", + "zod": "^3.23.0" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.6.0", + "vitest": "^2.1.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/drizzle-orm": { + "version": "0.36.4", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.36.4.tgz", + "integrity": "sha512-1OZY3PXD7BR00Gl61UUOFihslDldfH4NFRH2MbP54Yxi0G/PKn4HfO65JYZ7c16DeP3SpM3Aw+VXVG9j6CRSXA==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=3", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/react": ">=18", + "@types/sql.js": "*", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "react": ">=18", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "react": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.1.tgz", + "integrity": "sha512-TvncJykhxAzFCk0VQZKBTClall4Pm7qXDSodb6uxi8QFa8X8mT6ABjxxsQ2opDRYxG7AzcRWXaFtruz5HJKuWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/vite-node/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/package.json b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/package.json new file mode 100644 index 0000000..e41ec12 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/package.json @@ -0,0 +1,26 @@ +{ + "name": "@spring-boot-users-sample/api", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "decimal.js": "^10.4.3", + "drizzle-orm": "^0.36.0", + "express": "^4.21.0", + "pino": "^9.5.0", + "reflect-metadata": "^0.2.2", + "tsyringe": "^4.8.0", + "zod": "^3.23.0" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.6.0", + "vitest": "^2.1.0" + } +} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/app.ts b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/app.ts new file mode 100644 index 0000000..ada665f --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/app.ts @@ -0,0 +1,16 @@ +import 'reflect-metadata'; +import express, { type Express } from 'express'; +import { wireContainer } from './wiring.js'; +import { getUserHandler } from './routes/get-user.js'; +import { createUserHandler } from './routes/create-user.js'; +import { errorMiddleware } from './middleware/errors.js'; + +export function buildApp(): Express { + wireContainer(); + const app = express(); + app.use(express.json()); + app.get('/users/:id', getUserHandler); + app.post('/users', createUserHandler); + app.use(errorMiddleware); + return app; +} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/config.ts b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/config.ts new file mode 100644 index 0000000..e4b4922 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/config.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +const EnvSchema = z.object({ + PORT: z.coerce.number().int().positive().default(3000), + NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), + DATABASE_URL: z.string().default('postgres://localhost:5432/users'), + LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'), +}); + +export type Config = z.infer; + +export const config: Config = EnvSchema.parse(process.env); diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/db/schema.ts b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/db/schema.ts new file mode 100644 index 0000000..7cdd80c --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/db/schema.ts @@ -0,0 +1,11 @@ +import { bigint, numeric, pgTable, text, timestamp } from 'drizzle-orm/pg-core'; + +export const users = pgTable('users', { + id: bigint('id', { mode: 'bigint' }).primaryKey(), + name: text('name').notNull(), + balance: numeric('balance', { precision: 19, scale: 4 }).notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), +}); + +export type UserRow = typeof users.$inferSelect; +export type NewUserRow = typeof users.$inferInsert; diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/errors.ts b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/errors.ts new file mode 100644 index 0000000..a0a9060 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/errors.ts @@ -0,0 +1,16 @@ +export class UsersError extends Error { + readonly status: number; + + constructor(message: string, status = 500) { + super(message); + this.name = 'UsersError'; + this.status = status; + } +} + +export class UserNotFoundError extends UsersError { + constructor(id: bigint) { + super(`user not found: ${id.toString()}`, 404); + this.name = 'UserNotFoundError'; + } +} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/index.test.ts b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/index.test.ts new file mode 100644 index 0000000..4d3a51d --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/index.test.ts @@ -0,0 +1,52 @@ +import 'reflect-metadata'; +import { describe, expect, it } from 'vitest'; +import Decimal from 'decimal.js'; +import { makeUserRepo } from './repos/user.js'; +import { CreateUserSchema, UserResponseSchema } from './schemas/user.js'; +import { UsersError, UserNotFoundError } from './errors.js'; + +describe('sanity', () => { + it('arithmetic still works', () => { + expect(1 + 1).toBe(2); + }); +}); + +describe('schemas', () => { + it('CreateUserSchema accepts a valid body', () => { + const parsed = CreateUserSchema.parse({ name: 'Ada', balance: '10.5' }); + expect(parsed.name).toBe('Ada'); + }); + + it('CreateUserSchema rejects non-decimal balance', () => { + expect(() => CreateUserSchema.parse({ name: 'Ada', balance: 'abc' })).toThrow(); + }); + + it('UserResponseSchema round-trips', () => { + const out = UserResponseSchema.parse({ + id: '1', + name: 'Ada', + balance: '10.5', + createdAt: new Date(0).toISOString(), + }); + expect(out.id).toBe('1'); + }); +}); + +describe('repo', () => { + it('insert + findById returns a User with Decimal balance', async () => { + const repo = makeUserRepo(); + const created = await repo.insert({ name: 'Ada', balance: new Decimal('1.25') }); + const fetched = await repo.findById(created.id); + expect(fetched).not.toBeNull(); + expect(fetched!.balance.toString()).toBe('1.25'); + expect(typeof fetched!.id).toBe('bigint'); + }); +}); + +describe('errors', () => { + it('UserNotFoundError extends UsersError with status 404', () => { + const e = new UserNotFoundError(7n); + expect(e).toBeInstanceOf(UsersError); + expect(e.status).toBe(404); + }); +}); diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/logger.ts b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/logger.ts new file mode 100644 index 0000000..34cddd8 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/logger.ts @@ -0,0 +1,8 @@ +import pino from 'pino'; +import { config } from './config.js'; + +export const baseLogger = pino({ level: config.LOG_LEVEL }); + +export function childLogger(module: string): pino.Logger { + return baseLogger.child({ module }); +} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/middleware/errors.ts b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/middleware/errors.ts new file mode 100644 index 0000000..b1a3ad3 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/middleware/errors.ts @@ -0,0 +1,24 @@ +import type { NextFunction, Request, Response } from 'express'; +import { ZodError } from 'zod'; +import { UsersError } from '../errors.js'; +import { childLogger } from '../logger.js'; + +const log = childLogger('errors'); + +export function errorMiddleware( + err: unknown, + _req: Request, + res: Response, + _next: NextFunction, +): void { + if (err instanceof ZodError) { + res.status(400).json({ error: 'validation_error', issues: err.issues }); + return; + } + if (err instanceof UsersError) { + res.status(err.status).json({ error: err.name, message: err.message }); + return; + } + log.error({ err }, 'unhandled error'); + res.status(500).json({ error: 'internal_error' }); +} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/repos/user.ts b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/repos/user.ts new file mode 100644 index 0000000..5c3d677 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/repos/user.ts @@ -0,0 +1,44 @@ +import Decimal from 'decimal.js'; +import type { UserRow } from '../db/schema.js'; + +export type User = { + id: bigint; + name: string; + balance: Decimal; + createdAt: Date; +}; + +export type UserRepo = { + findById(id: bigint): Promise; + insert(input: { name: string; balance: Decimal }): Promise; +}; + +function rowToUser(row: UserRow): User { + return { + id: row.id, + name: row.name, + balance: new Decimal(row.balance), + createdAt: row.createdAt, + }; +} + +export function makeUserRepo(store: Map = new Map()): UserRepo { + let nextId = 1n; + return { + async findById(id) { + const row = store.get(id); + return row ? rowToUser(row) : null; + }, + async insert(input) { + const id = nextId++; + const row: UserRow = { + id, + name: input.name, + balance: input.balance.toString(), + createdAt: new Date(), + }; + store.set(id, row); + return rowToUser(row); + }, + }; +} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/routes/create-user.ts b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/routes/create-user.ts new file mode 100644 index 0000000..87ce89b --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/routes/create-user.ts @@ -0,0 +1,29 @@ +import type { Request, Response, NextFunction } from 'express'; +import Decimal from 'decimal.js'; +import { container } from 'tsyringe'; +import { UserService } from '../services/user-service.js'; +import { CreateUserSchema, UserResponseSchema, type UserResponse } from '../schemas/user.js'; + +export async function createUserHandler( + req: Request, + res: Response, + next: NextFunction, +): Promise { + try { + const input = CreateUserSchema.parse(req.body); + const service = container.resolve(UserService); + const user = await service.create({ + name: input.name, + balance: new Decimal(input.balance), + }); + const body: UserResponse = UserResponseSchema.parse({ + id: user.id.toString(), + name: user.name, + balance: user.balance.toString(), + createdAt: user.createdAt.toISOString(), + }); + res.status(201).json(body); + } catch (e) { + next(e); + } +} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/routes/get-user.ts b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/routes/get-user.ts new file mode 100644 index 0000000..cbd13e3 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/routes/get-user.ts @@ -0,0 +1,29 @@ +import type { Request, Response, NextFunction } from 'express'; +import { container } from 'tsyringe'; +import { UserService } from '../services/user-service.js'; +import { UserResponseSchema, type UserResponse } from '../schemas/user.js'; + +export async function getUserHandler( + req: Request, + res: Response, + next: NextFunction, +): Promise { + try { + const idStr = req.params.id; + if (!idStr || !/^\d+$/.test(idStr)) { + res.status(400).json({ error: 'invalid_id' }); + return; + } + const service = container.resolve(UserService); + const user = await service.getById(BigInt(idStr)); + const body: UserResponse = UserResponseSchema.parse({ + id: user.id.toString(), + name: user.name, + balance: user.balance.toString(), + createdAt: user.createdAt.toISOString(), + }); + res.json(body); + } catch (e) { + next(e); + } +} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/schemas/user.ts b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/schemas/user.ts new file mode 100644 index 0000000..b786d1e --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/schemas/user.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; + +export const CreateUserSchema = z.object({ + name: z.string().min(1).max(200), + balance: z.string().regex(/^-?\d+(\.\d+)?$/, 'must be a decimal string'), +}); +export type CreateUserInput = z.infer; + +export const UserResponseSchema = z.object({ + id: z.string(), + name: z.string(), + balance: z.string(), + createdAt: z.string(), +}); +export type UserResponse = z.infer; diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/server.ts b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/server.ts new file mode 100644 index 0000000..c1418c2 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/server.ts @@ -0,0 +1,16 @@ +import { buildApp } from './app.js'; +import { config } from './config.js'; +import { baseLogger } from './logger.js'; + +const app = buildApp(); +const server = app.listen(config.PORT, () => { + baseLogger.info({ port: config.PORT }, 'server listening'); +}); + +function shutdown(signal: string): void { + baseLogger.info({ signal }, 'shutting down'); + server.close(() => process.exit(0)); +} + +process.on('SIGINT', () => shutdown('SIGINT')); +process.on('SIGTERM', () => shutdown('SIGTERM')); diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/services/user-service.ts b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/services/user-service.ts new file mode 100644 index 0000000..1fefef5 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/services/user-service.ts @@ -0,0 +1,20 @@ +import 'reflect-metadata'; +import Decimal from 'decimal.js'; +import { inject, injectable } from 'tsyringe'; +import type { User, UserRepo } from '../repos/user.js'; +import { UserNotFoundError } from '../errors.js'; + +@injectable() +export class UserService { + constructor(@inject('UserRepo') private readonly repo: UserRepo) {} + + async getById(id: bigint): Promise { + const u = await this.repo.findById(id); + if (!u) throw new UserNotFoundError(id); + return u; + } + + async create(input: { name: string; balance: Decimal }): Promise { + return this.repo.insert(input); + } +} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/wiring.ts b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/wiring.ts new file mode 100644 index 0000000..e3986c0 --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/src/wiring.ts @@ -0,0 +1,7 @@ +import 'reflect-metadata'; +import { container } from 'tsyringe'; +import { makeUserRepo } from './repos/user.js'; + +export function wireContainer(): void { + container.register('UserRepo', { useValue: makeUserRepo() }); +} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/tsconfig.json b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/tsconfig.json new file mode 100644 index 0000000..fbb17fe --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "exactOptionalPropertyTypes": false, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": false, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/vitest.config.ts b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/vitest.config.ts new file mode 100644 index 0000000..96eb6ab --- /dev/null +++ b/skills/java-to-typescript/evals/fixtures/spring-boot-users/sample/ts-port/api/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['src/**/*.test.ts'], + environment: 'node', + }, +}); diff --git a/skills/java-to-typescript/evals/judge.ts b/skills/java-to-typescript/evals/judge.ts new file mode 100644 index 0000000..562832c --- /dev/null +++ b/skills/java-to-typescript/evals/judge.ts @@ -0,0 +1,103 @@ +import Anthropic from '@anthropic-ai/sdk'; +import { createHash } from 'node:crypto'; + +export type JudgeArgs = { + content: string; + rubricMd: string; + judgeInstructions: string; + maxScore?: number; +}; + +export type JudgeResult = { + score: number; + rationale: string; + raw?: string; +}; + +const DEFAULT_MAX_SCORE = 10; +const JUDGE_MODEL = 'claude-haiku-4-5-20251001'; + +export function isMockMode(): boolean { + const mockEnv = process.env.MOCK_JUDGE; + const apiKey = process.env.ANTHROPIC_API_KEY; + if (mockEnv === '0' && apiKey) return false; + return true; +} + +export async function judgeAgainstRubric(args: JudgeArgs): Promise { + const maxScore = args.maxScore ?? DEFAULT_MAX_SCORE; + if (isMockMode()) return mockJudge(args, maxScore); + return realJudge(args, maxScore); +} + +function mockJudge(args: JudgeArgs, maxScore: number): JudgeResult { + const hash = createHash('sha256') + .update(args.content) + .update(args.rubricMd) + .digest(); + // Bucket into 7..9 range (3 buckets); first byte modulo 3. + const bucket = (hash[0] ?? 0) % 3; + const score = Math.min(7 + bucket, maxScore); + return { + score, + rationale: `[mock judge] content-length=${args.content.length}, rubric-length=${args.rubricMd.length}, hash-bucket=${bucket}`, + }; +} + +async function realJudge(args: JudgeArgs, maxScore: number): Promise { + const apiKey = process.env.ANTHROPIC_API_KEY; + if (!apiKey) throw new Error('realJudge requires ANTHROPIC_API_KEY'); + const client = new Anthropic({ apiKey }); + const prompt = buildPrompt(args, maxScore); + const response = await client.messages.create({ + model: JUDGE_MODEL, + max_tokens: 1024, + messages: [{ role: 'user', content: prompt }], + }); + const textBlock = response.content.find((b) => b.type === 'text'); + if (!textBlock || textBlock.type !== 'text') { + throw new Error('judge response had no text block'); + } + const raw = textBlock.text; + const parsed = parseJudgeResponse(raw, maxScore); + return { ...parsed, raw }; +} + +function buildPrompt(args: JudgeArgs, maxScore: number): string { + return [ + 'You are evaluating produced content against a rubric.', + '', + `Instructions: ${args.judgeInstructions}`, + '', + 'Rubric:', + '```', + args.rubricMd, + '```', + '', + 'Content to score:', + '```', + args.content, + '```', + '', + `Respond with JSON only, no prose, no fences: {"score": , "rationale": ""}`, + ].join('\n'); +} + +function parseJudgeResponse(raw: string, maxScore: number): { score: number; rationale: string } { + const jsonMatch = raw.match(/\{[\s\S]*\}/); + if (!jsonMatch) throw new Error(`judge response did not contain JSON: ${raw.slice(0, 200)}`); + let parsed: unknown; + try { + parsed = JSON.parse(jsonMatch[0]); + } catch (e) { + throw new Error(`judge JSON parse failed: ${(e as Error).message}; raw: ${raw.slice(0, 200)}`); + } + if (!parsed || typeof parsed !== 'object') throw new Error('judge response not an object'); + const obj = parsed as Record; + const score = typeof obj.score === 'number' ? obj.score : Number(obj.score); + if (!Number.isFinite(score) || score < 0 || score > maxScore) { + throw new Error(`judge score out of range [0, ${maxScore}]: ${String(obj.score)}`); + } + const rationale = typeof obj.rationale === 'string' ? obj.rationale : ''; + return { score, rationale }; +} diff --git a/skills/java-to-typescript/evals/rubrics/plan.md b/skills/java-to-typescript/evals/rubrics/plan.md new file mode 100644 index 0000000..3b289ef --- /dev/null +++ b/skills/java-to-typescript/evals/rubrics/plan.md @@ -0,0 +1,27 @@ +# Plan rubric template (E2) + +LLM-judge prompt skeleton for evaluating `migration/plan.md` against per-fixture `expected/plan-rubric.md`. + +## Scoring + +Score on the 0–10 scale defined in the per-fixture rubric. Sum the per-item points; total is the score. + +## What "good" looks like + +A passing plan: +- Identifies every Maven module in the source repo. +- Picks a TS framework / DI / validation library per module, with the registry defaults applied unless the user overrode at Phase 2. +- Surfaces every `unmappedDependencies` entry to the user as an explicit question (not silently chosen). +- Orders modules for porting by dependency DAG (leaves first). +- Includes a per-module diff preview. + +## Failure modes the judge MUST flag + +- Hallucinated dependencies not in the source pom/gradle. +- TS library picks not present in `library-map.yaml` and not flagged for user input. +- Skipped modules. +- Skipped phases. + +## Variance handling + +LLM-judged. Run 5 times. Report mean ± stddev. Compare against the previous skill version using the rule: "improvement only if mean delta > 1 stddev." diff --git a/skills/java-to-typescript/evals/rubrics/port.md b/skills/java-to-typescript/evals/rubrics/port.md new file mode 100644 index 0000000..41e3373 --- /dev/null +++ b/skills/java-to-typescript/evals/rubrics/port.md @@ -0,0 +1,29 @@ +# Port rubric template (E3) + +LLM-judge prompt skeleton for evaluating the produced TS workspace against per-fixture `expected/ts-shape.md`. + +## Hard gates (0 or 10) +- `tsc --noEmit` clean across all workspaces. +- All ported Vitest tests pass. + +A failure on either = 0 for the entire E3 score (not just for the hard-gate dimension). + +## Scored dimensions (each 0–10, averaged) + +### Framework idioms (0–10) +- One handler per file? (0–2) +- DTOs are zod schemas with inferred types? (0–2) +- Repositories are Drizzle queries, not faked JPA interfaces? (0–2) +- Config is a single zod-parsed env object per module? (0–2) +- Logger is module-scoped pino child? (0–2) + +### Type fidelity (0–10) +- BigDecimal preserved as Decimal? (0–3) +- IDs typed as bigint (or branded string) consistently? (0–3) +- Optional rendered per policy? (0–2) +- LocalDate / ZonedDateTime via Temporal polyfill? (0–2) + +### Migration discipline (0–10) +- port-log.md records every non-trivial decision? (0–4) +- No dead Java-style abstractions ported over? (0–3) +- No policy violations without a port-log entry? (0–3) diff --git a/skills/java-to-typescript/evals/runner.ts b/skills/java-to-typescript/evals/runner.ts new file mode 100644 index 0000000..e2f79ee --- /dev/null +++ b/skills/java-to-typescript/evals/runner.ts @@ -0,0 +1,232 @@ +import { analyze, type Analysis } from '../scripts/pom-to-workspace.js'; +import { replayFixtures, type ReplayResult } from '../scripts/replay-fixtures.js'; +import { judgeAgainstRubric, type JudgeResult } from './judge.js'; +import { parseArgs } from 'node:util'; +import { spawn } from 'node:child_process'; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export async function runE1(javaRepo: string): Promise { + return analyze(javaRepo); +} + +export type E2Result = JudgeResult; + +export async function runE2(fixtureDir: string, planMdPath: string): Promise { + const rubricMd = await fs.readFile(path.join(fixtureDir, 'expected', 'plan-rubric.md'), 'utf8'); + const content = await fs.readFile(planMdPath, 'utf8'); + return judgeAgainstRubric({ + content, + rubricMd, + judgeInstructions: + 'You are reviewing a migration plan produced by an AI assistant during Phase 2 of a Java→TypeScript migration. Score it against the rubric. A plan that omits a rubric item gets 0 for that item.', + }); +} + +export type E3Result = { + compileOk: boolean; + testsOk: boolean; + idiomScore: number; + rationale: string; + overallScore: number; +}; + +export async function runE3(fixtureDir: string, tsRepoDir: string): Promise { + const compileOk = await runCommand('npx', ['tsc', '--noEmit'], tsRepoDir); + const testsOk = compileOk ? await runCommand('npx', ['vitest', 'run'], tsRepoDir) : false; + + if (!compileOk || !testsOk) { + return { + compileOk, + testsOk, + idiomScore: 0, + rationale: `hard gate failed: compileOk=${compileOk}, testsOk=${testsOk}`, + overallScore: 0, + }; + } + + const shapeMd = await fs.readFile(path.join(fixtureDir, 'expected', 'ts-shape.md'), 'utf8'); + const structure = await summarizeTsRepoStructure(tsRepoDir); + const judge = await judgeAgainstRubric({ + content: structure, + rubricMd: shapeMd, + judgeInstructions: + 'You are reviewing a TypeScript port produced by an AI assistant during Phase 3 of a Java→TypeScript migration. The "Content to score" is a structural summary of the TS workspace. Score against the canonical shape and forbidden-shapes lists in the rubric. tsc clean and tests passing are already confirmed (do not re-score those).', + }); + return { + compileOk: true, + testsOk: true, + idiomScore: judge.score, + rationale: judge.rationale, + overallScore: judge.score, + }; +} + +async function runCommand(cmd: string, args: string[], cwd: string): Promise { + return new Promise((resolve) => { + const child = spawn(cmd, args, { cwd, stdio: 'ignore' }); + child.on('exit', (code) => resolve(code === 0)); + child.on('error', () => resolve(false)); + }); +} + +async function summarizeTsRepoStructure(tsRepoDir: string): Promise { + const lines: string[] = [`# TS port structural summary: ${path.basename(tsRepoDir)}`, '']; + async function walk(dir: string, prefix: string): Promise { + const entries = await fs.readdir(dir, { withFileTypes: true }); + for (const e of entries) { + if (e.name === 'node_modules' || e.name === 'dist' || e.name === '.vitest-cache') continue; + const full = path.join(dir, e.name); + const rel = `${prefix}${e.name}`; + if (e.isDirectory()) { + lines.push(`${rel}/`); + await walk(full, `${rel}/`); + } else if (e.isFile()) { + const stat = await fs.stat(full); + lines.push(`${rel} (${stat.size}B)`); + if (e.name.endsWith('.ts') || e.name.endsWith('.json')) { + const content = await fs.readFile(full, 'utf8'); + const excerpt = content.slice(0, 800); + lines.push('```'); + lines.push(excerpt); + if (content.length > 800) lines.push(`... [${content.length - 800} more bytes]`); + lines.push('```'); + } + } + } + } + await walk(tsRepoDir, ''); + return lines.join('\n'); +} + +export async function runE4( + tsBase: string, + fixturesFile: string, + allowlistFile: string, + reportMd: string, +): Promise { + const reportJson = reportMd.replace(/\.md$/, '.json'); + return replayFixtures(tsBase, fixturesFile, allowlistFile, reportMd, reportJson); +} + +export type AggregateResult = { + runs: T[]; + scores: number[]; + mean: number; + stddev: number; +}; + +export async function runWithVariance< + T extends { score?: number; overallScore?: number; idiomScore?: number }, +>(runs: number, fn: () => Promise): Promise> { + const results: T[] = []; + for (let i = 0; i < runs; i++) { + results.push(await fn()); + } + const scores = results.map((r) => r.overallScore ?? r.score ?? r.idiomScore ?? 0); + const mean = scores.reduce((a, b) => a + b, 0) / scores.length; + const variance = + scores.length > 1 + ? scores.reduce((acc, s) => acc + (s - mean) ** 2, 0) / (scores.length - 1) + : 0; + const stddev = Math.sqrt(variance); + return { runs: results, scores, mean, stddev }; +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch((e: unknown) => { + process.stderr.write(`${e instanceof Error ? e.message : String(e)}\n`); + process.exit(1); + }); +} + +async function main(): Promise { + const { values } = parseArgs({ + args: process.argv.slice(2), + options: { + fixture: { type: 'string' }, + eval: { type: 'string' }, + plan: { type: 'string' }, + 'ts-repo': { type: 'string' }, + runs: { type: 'string' }, + }, + }); + const evalChoice = values.eval ?? 'all'; + const fixtureName = values.fixture ?? 'spring-boot-users'; + const runs = values.runs ? Math.max(1, Number(values.runs)) : 1; + const here = path.dirname(fileURLToPath(import.meta.url)); + const fixtureDir = path.resolve(here, 'fixtures', fixtureName); + + let anyFail = false; + + if (evalChoice === 'E1' || evalChoice === 'all') { + const expected = JSON.parse( + await fs.readFile(path.join(fixtureDir, 'expected', 'analysis.json'), 'utf8'), + ); + const actual = await runE1(path.join(fixtureDir, 'java')); + const equal = JSON.stringify(sortKeys(expected)) === JSON.stringify(sortKeys(actual)); + process.stdout.write(`E1 ${fixtureName}: ${equal ? 'PASS' : 'FAIL'}\n`); + if (!equal) { + process.stdout.write(`expected: ${JSON.stringify(sortKeys(expected), null, 2)}\n`); + process.stdout.write(`actual: ${JSON.stringify(sortKeys(actual), null, 2)}\n`); + anyFail = true; + } + } + + if (evalChoice === 'E2' || evalChoice === 'all') { + const planPath = values.plan ?? path.join(fixtureDir, 'sample', 'plan.md'); + const planExists = await fileExists(planPath); + if (!planExists) { + process.stdout.write(`E2 ${fixtureName}: SKIP (no plan at ${planPath})\n`); + } else { + const eff = runs > 1 ? runs : 5; + const agg = await runWithVariance(eff, () => runE2(fixtureDir, planPath)); + process.stdout.write( + `E2 ${fixtureName}: mean=${agg.mean.toFixed(2)} stddev=${agg.stddev.toFixed(2)} (n=${eff})\n`, + ); + } + } + + if (evalChoice === 'E3' || evalChoice === 'all') { + const tsRepo = values['ts-repo'] ?? path.join(fixtureDir, 'sample', 'ts-port', 'api'); + const tsExists = await fileExists(tsRepo); + if (!tsExists) { + process.stdout.write(`E3 ${fixtureName}: SKIP (no ts-port at ${tsRepo})\n`); + } else { + const eff = runs > 1 ? runs : 5; + const agg = await runWithVariance(eff, () => runE3(fixtureDir, tsRepo)); + process.stdout.write( + `E3 ${fixtureName}: mean=${agg.mean.toFixed(2)} stddev=${agg.stddev.toFixed(2)} (n=${eff})\n`, + ); + const lastRun = agg.runs[agg.runs.length - 1]; + if (lastRun && (!lastRun.compileOk || !lastRun.testsOk)) { + anyFail = true; + process.stdout.write(` hard gate: compileOk=${lastRun.compileOk} testsOk=${lastRun.testsOk}\n`); + } + } + } + + if (anyFail) process.exit(1); +} + +function sortKeys(v: unknown): unknown { + if (Array.isArray(v)) return v.map(sortKeys); + if (v && typeof v === 'object') { + return Object.fromEntries( + Object.entries(v as Record) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([k, vv]) => [k, sortKeys(vv)]), + ); + } + return v; +} + +async function fileExists(p: string): Promise { + try { + await fs.access(p); + return true; + } catch { + return false; + } +} diff --git a/skills/java-to-typescript/package-lock.json b/skills/java-to-typescript/package-lock.json new file mode 100644 index 0000000..15fddf2 --- /dev/null +++ b/skills/java-to-typescript/package-lock.json @@ -0,0 +1,2086 @@ +{ + "name": "@randomcodespace/java-to-typescript-skill", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@randomcodespace/java-to-typescript-skill", + "version": "0.1.0", + "devDependencies": { + "@anthropic-ai/sdk": "^0.96.0", + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.6.0", + "vitest": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.96.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.96.0.tgz", + "integrity": "sha512-KlCsODtTyb17bLUVCSDC2HtSvAbJf60sEiPEax9dInF+aDF92vS4TZJ5XD7YCQXNb1/5icYaw8Y7wMjPlIV9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.1.tgz", + "integrity": "sha512-TvncJykhxAzFCk0VQZKBTClall4Pm7qXDSodb6uxi8QFa8X8mT6ABjxxsQ2opDRYxG7AzcRWXaFtruz5HJKuWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/skills/java-to-typescript/package.json b/skills/java-to-typescript/package.json new file mode 100644 index 0000000..6dc895a --- /dev/null +++ b/skills/java-to-typescript/package.json @@ -0,0 +1,22 @@ +{ + "name": "@randomcodespace/java-to-typescript-skill", + "private": true, + "version": "0.1.0", + "description": "Dev tooling for the java-to-typescript skill scripts and evals.", + "type": "module", + "engines": { + "node": ">=20.0.0" + }, + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@anthropic-ai/sdk": "^0.96.0", + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.6.0", + "vitest": "^2.1.0" + } +} diff --git a/skills/java-to-typescript/pom.xml b/skills/java-to-typescript/pom.xml new file mode 100644 index 0000000..6c125f7 --- /dev/null +++ b/skills/java-to-typescript/pom.xml @@ -0,0 +1,143 @@ + + + 4.0.0 + + + + io.github.randomcodespace.ai + java-to-typescript + + 0.0.0 + pom + + java-to-typescript + Migrate a Java service (Spring Boot, Quarkus, Micronaut, Spring MVC) to TypeScript with phased orchestration, deterministic plumbing scripts, and contract-parity verification. + https://github.com/RandomCodeSpace/skills/tree/main/skills/java-to-typescript + 2026 + + + + MIT License + https://opensource.org/licenses/MIT + repo + + + + + + randomcodespace + RandomCodeSpace + mail@ossrandom.com + RandomCodeSpace + https://github.com/RandomCodeSpace + + + + + scm:git:git://github.com/RandomCodeSpace/skills.git + scm:git:ssh://git@github.com/RandomCodeSpace/skills.git + https://github.com/RandomCodeSpace/skills/tree/main/skills/java-to-typescript + HEAD + + + + GitHub Issues + https://github.com/RandomCodeSpace/skills/issues + + + + UTF-8 + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.7.1 + + + bundle + package + + single + + + + ${session.executionRootDirectory}/assembly/skill-bundle.xml + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.6.0 + + + attach-bundle + package + + attach-artifact + + + + + ${project.build.directory}/${project.artifactId}-${project.version}-bin.zip + zip + bin + + + + + + + + + + + + release + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.7 + + + sign-artifacts + verify + + sign + + + + + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.6.0 + true + + central + true + published + + + + + + + diff --git a/skills/java-to-typescript/references/build-layout.md b/skills/java-to-typescript/references/build-layout.md new file mode 100644 index 0000000..66669eb --- /dev/null +++ b/skills/java-to-typescript/references/build-layout.md @@ -0,0 +1,72 @@ +# Build layout (Maven/Gradle multi-module → npm workspaces) + +## Default mapping + +| Maven / Gradle | TS workspace | +|---|---| +| Parent `pom.xml` / `settings.gradle[.kts]` | Root `package.json` with `"workspaces": [...]` | +| `foo` / `include 'foo'` | `foo/package.json` | +| `` on a sibling module | npm workspace dependency: `"@org/foo": "*"` | +| Maven `${revision}` placeholder | Per-workspace `version` field; or root version + Changesets | +| `mvn package` | `npm run build --workspaces --if-present` | +| `mvn test` | `npm run test --workspaces --if-present` | + +## Root `package.json` skeleton (npm default) + +```json +{ + "name": "@example/services", + "private": true, + "workspaces": ["api", "core"], + "scripts": { + "build": "npm run build --workspaces --if-present", + "test": "npm run test --workspaces --if-present", + "typecheck": "tsc -b" + }, + "devDependencies": { + "typescript": "^5", + "vitest": "^2", + "tsx": "^4" + } +} +``` + +## Alternate package managers (opt-in) + +### pnpm + +Root `pnpm-workspace.yaml`: + +```yaml +packages: + - 'api' + - 'core' +``` + +Remove `workspaces` from root `package.json`; pnpm uses the YAML file. + +### yarn + +Same as npm — yarn honors the `workspaces` array. `.yarnrc.yml` for v4 Berry. + +### bun + +`package.json` `workspaces` array works. Use `bun install`, `bun test` (or keep vitest). + +## Dependency-order build + +`tsc -b` (build mode) walks the workspace project references. Add to each module's `tsconfig.json`: + +```json +{ + "extends": "../tsconfig.base.json", + "references": [{ "path": "../core" }], + "compilerOptions": { "composite": true } +} +``` + +Then `tsc -b` from the root builds in topological order. + +## Don't flatten + +The temptation: collapse `api/core/users` into one big `src/`. Resist. The module boundaries from Maven/Gradle are signal — they encode intent. Preserve them as workspace boundaries. diff --git a/skills/java-to-typescript/references/categories/config.md b/skills/java-to-typescript/references/categories/config.md new file mode 100644 index 0000000..f52b8fb --- /dev/null +++ b/skills/java-to-typescript/references/categories/config.md @@ -0,0 +1,71 @@ +# Configuration (`@ConfigurationProperties` → zod-validated env) + +## The rule + +Each module has exactly ONE `config.ts` that: +1. Defines a zod schema describing every env var the module reads. +2. Parses `process.env` against the schema at module load. +3. Exports a typed `config` object. + +No scattered `process.env.FOO` reads anywhere else in the module. + +## Skeleton + +```typescript +// src/config.ts +import { z } from 'zod'; + +const ConfigSchema = z.object({ + NODE_ENV: z.enum(['development', 'test', 'production']).default('production'), + LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'), + PORT: z.coerce.number().int().positive().default(3000), + DATABASE_URL: z.string().url(), + SERVICE_NAME: z.string().default('users'), +}); + +export type Config = z.infer; + +export const config: Config = (() => { + const parsed = ConfigSchema.safeParse(process.env); + if (!parsed.success) { + console.error('config: invalid environment variables:', parsed.error.format()); + process.exit(1); + } + return parsed.data; +})(); +``` + +Fail-fast on bad config is intentional: it surfaces missing/wrong vars at startup, not at first request. + +## `@ConfigurationProperties("app")` → grouped config + +Spring's grouped properties (`app.cache.ttl-seconds`, `app.cache.max-size`) → flat env var names by convention: + +| Spring property | env var | +|---|---| +| `app.cache.ttl-seconds` | `APP_CACHE_TTL_SECONDS` | +| `db.pool.max` | `DB_POOL_MAX` | + +Group in the schema: + +```typescript +const CacheConfigSchema = z.object({ + APP_CACHE_TTL_SECONDS: z.coerce.number().int().positive().default(300), + APP_CACHE_MAX_SIZE: z.coerce.number().int().positive().default(1000), +}); + +// access: +config.APP_CACHE_TTL_SECONDS +``` + +## Profiles → env + +Spring profiles (`application-dev.yml`, `application-prod.yml`) → a single `.env.` loaded via `dotenv-flow` or by your deploy system. Don't try to load YAML at runtime. + +## Secrets + +Same convention. The schema accepts them via env vars; the deploy system (Kubernetes secrets, Vault, AWS SSM) is responsible for populating env. Never log a secret — pino's `redact` option: + +```typescript +const baseLogger = pino({ redact: ['DATABASE_URL', 'API_KEY', 'JWT_SECRET'] }); +``` diff --git a/skills/java-to-typescript/references/categories/di.md b/skills/java-to-typescript/references/categories/di.md new file mode 100644 index 0000000..f5cc5e4 --- /dev/null +++ b/skills/java-to-typescript/references/categories/di.md @@ -0,0 +1,64 @@ +# Dependency Injection (Spring DI → tsyringe) + +## Why tsyringe + +Per the default_picks: lightweight, decorator-based, plays well with TypeScript's `experimentalDecorators`. Closest mental model to Spring's `@Component` + constructor injection. Alternatives (awilix, InversifyJS) listed in the registry; tsyringe is the v1 default. + +## Setup + +1. `tsconfig.json` MUST have `experimentalDecorators: true` and `emitDecoratorMetadata: true` (set by the scaffolder; do not remove). +2. The very first import in your entry point MUST be `import 'reflect-metadata';`. Without it, decorator metadata silently breaks. +3. Tests that resolve from `container` must also import `'reflect-metadata'` at the top. + +## Registration patterns + +```typescript +// src/wiring.ts — one wiring file per module +import { container } from 'tsyringe'; +import { UserService } from './services/user-service.js'; +import { db } from './db/client.js'; + +container.register('Db', { useValue: db }); +container.register('UserService', { useClass: UserService }); +container.register('Clock', { useFactory: () => ({ now: () => new Date() }) }); +``` + +## Class-level decoration + +```typescript +import { injectable, inject } from 'tsyringe'; +import type { Database } from 'drizzle-orm/node-postgres'; + +@injectable() +export class UserService { + constructor(@inject('Db') private db: Database) {} + + async getUser(id: bigint): Promise { + return this.db.select().from(users).where(eq(users.id, id)).then((r) => r[0]); + } +} +``` + +## What does NOT translate + +| Spring | Why and what to do instead | +|---|---| +| `@Autowired` on fields | tsyringe only supports constructor injection. Refactor. | +| Classpath scanning | Register beans explicitly in `wiring.ts`. | +| AOP (`@Around`, `@Before`) | Wrap manually: write a higher-order function and apply at the call site. | +| `@PostConstruct` | Initialize in the constructor; for async init, expose an `async init()` and call from `wiring.ts`. | +| Bean lifecycle (`@PreDestroy`) | Wire shutdown handlers in `server.ts` (see `runtimes/node.md`). | +| Spring profiles | Drive via env + zod-validated config (see `categories/config.md`). | + +## Method-level authorization + +Spring's `@PreAuthorize("hasRole('ADMIN')")` → an explicit guard helper called at the start of the method: + +```typescript +async deleteUser(id: bigint, actor: Actor): Promise { + requireRole(actor, 'ADMIN'); + // ... +} +``` + +Do not introduce an aspect framework. diff --git a/skills/java-to-typescript/references/categories/logging.md b/skills/java-to-typescript/references/categories/logging.md new file mode 100644 index 0000000..1d29d38 --- /dev/null +++ b/skills/java-to-typescript/references/categories/logging.md @@ -0,0 +1,67 @@ +# Logging (SLF4J + Logback → pino) + +## Why pino + +Structured by default, fast, MIT, no native deps, supports per-module child loggers cheaply. Configurable transports for files / syslog / OTel without a separate config file. + +## Setup + +```typescript +// src/logger.ts (one per module) +import pino from 'pino'; +import { config } from './config.js'; + +export const baseLogger = pino({ + level: config.LOG_LEVEL, + base: { service: config.SERVICE_NAME, env: config.NODE_ENV }, + timestamp: pino.stdTimeFunctions.isoTime, +}); + +export const logger = baseLogger; // for the module entry +``` + +In each file inside the module: + +```typescript +import { baseLogger } from '../logger.js'; +const log = baseLogger.child({ module: 'user-service' }); + +log.info({ userId: id }, 'fetched user'); +log.warn({ err }, 'retrying upstream'); +``` + +## SLF4J → pino mapping + +| SLF4J | pino | +|---|---| +| `LoggerFactory.getLogger(Foo.class)` | `baseLogger.child({ module: 'Foo' })` | +| `log.info("user {} fetched", id)` | `log.info({ userId: id }, 'user fetched')` — structured fields, not interpolated message | +| `log.error("failed", e)` | `log.error({ err: e }, 'failed')` — pino serializes Error via the `err` key automatically | +| `MDC.put("requestId", x)` | Bind via `child({ requestId: x })` or `AsyncLocalStorage` (see below) | +| `@Slf4j` on a class | `import { baseLogger }; const log = baseLogger.child({ module: 'X' })` | + +## Request-scoped fields (MDC equivalent) + +Use `AsyncLocalStorage`: + +```typescript +import { AsyncLocalStorage } from 'node:async_hooks'; + +export const requestContext = new AsyncLocalStorage<{ requestId: string; userId?: string }>(); + +// middleware +app.use((req, _res, next) => { + const ctx = { requestId: crypto.randomUUID() }; + requestContext.run(ctx, () => next()); +}); + +// usage in any module +const ctx = requestContext.getStore(); +log.info({ ...ctx, op: 'createUser' }, 'creating'); +``` + +## What NOT to do + +- No `console.log`. Ever. The lint rule `no-console` should be enabled. +- No string interpolation in messages — pino keeps structured fields searchable. +- No global logger that swallows context — always use a child with module/route context. diff --git a/skills/java-to-typescript/references/categories/persistence.md b/skills/java-to-typescript/references/categories/persistence.md new file mode 100644 index 0000000..f8eef5c --- /dev/null +++ b/skills/java-to-typescript/references/categories/persistence.md @@ -0,0 +1,95 @@ +# Persistence (JPA / Hibernate → Drizzle) + +## Mental-model shift + +JPA is a session-aware ORM with lazy loading, cascades, and dirty-checking. Drizzle is a typed query builder. The shift is from "save an entity graph" to "execute explicit queries". Encode all joins, transactions, and cascade behavior in a query module — don't try to recreate JPA semantics. + +## Entity → Drizzle table + +JPA: + +```java +@Entity @Table(name = "users") +public class User { + @Id @GeneratedValue Long id; + @Column(nullable = false) String name; + @Column(precision = 19, scale = 4) BigDecimal balance; + @Column(name = "created_at") Instant createdAt; +} +``` + +Drizzle: + +```typescript +import { pgTable, bigserial, text, numeric, timestamp } from 'drizzle-orm/pg-core'; + +export const users = pgTable('users', { + id: bigserial('id', { mode: 'bigint' }).primaryKey(), + name: text('name').notNull(), + balance: numeric('balance', { precision: 19, scale: 4 }).notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull(), +}); + +export type User = typeof users.$inferSelect; +export type NewUser = typeof users.$inferInsert; +``` + +Per type-fidelity policy: `Long` ID → `bigint` (drizzle's `bigserial { mode: 'bigint' }`); `BigDecimal` → numeric column → read as `string`, wrap in `Decimal` at the service boundary. + +## Repository → query module + +JPA `interface UserRepository extends JpaRepository` → a TS module of named functions: + +```typescript +import { eq } from 'drizzle-orm'; +import { container } from 'tsyringe'; +import { users } from '../db/schema.js'; + +export function makeUserRepo(db: Database) { + return { + async findById(id: bigint): Promise { + const [row] = await db.select().from(users).where(eq(users.id, id)); + return row; + }, + async insert(u: NewUser): Promise { + const [row] = await db.insert(users).values(u).returning(); + return row!; + }, + }; +} +``` + +No abstract repository interface. No JpaRepository hierarchy. Plain functions, returned from a factory, registered in tsyringe. + +## Transactions + +JPA `@Transactional` → wrap the query module call in `db.transaction`: + +```typescript +await db.transaction(async (tx) => { + const repo = makeUserRepo(tx); + await repo.insert(...); + // multiple ops, all in one tx +}); +``` + +## Lazy loading + +There is no equivalent. Make joins explicit: + +```typescript +db.select().from(users).leftJoin(orders, eq(users.id, orders.userId)); +``` + +If the Java code relies heavily on lazy loading across boundaries, refactor to fetch-and-return; do not introduce a proxy layer. + +## Migrations + +JPA + Flyway/Liquibase → drizzle-kit: + +``` +npx drizzle-kit generate # generate SQL from schema diff +npx drizzle-kit migrate # apply +``` + +Migration files are SQL; commit them to the repo. Drizzle-kit has no Java equivalent of Liquibase changesets — keep migrations linear. diff --git a/skills/java-to-typescript/references/categories/testing.md b/skills/java-to-typescript/references/categories/testing.md new file mode 100644 index 0000000..d41a8f0 --- /dev/null +++ b/skills/java-to-typescript/references/categories/testing.md @@ -0,0 +1,103 @@ +# Testing (JUnit / Mockito / AssertJ / Testcontainers → Vitest) + +## Direct mapping table + +| Java | Vitest | Notes | +|---|---|---| +| `@Test` | `it('description', () => {...})` | One `it` per behavior. | +| `@BeforeEach` | `beforeEach(() => {...})` | | +| `@AfterEach` | `afterEach(() => {...})` | | +| `@BeforeAll` | `beforeAll(() => {...})` | | +| `@ParameterizedTest` + `@ValueSource` | `it.each([...])('msg %s', (v) => {...})` | | +| `assertThat(x).isEqualTo(y)` | `expect(x).toEqual(y)` | AssertJ → expect chains. | +| `assertThrows(Foo.class, () -> ...)` | `expect(() => fn()).toThrow(Foo)` | Async: `await expect(fn()).rejects.toThrow(Foo)`. | +| `@Mock Foo foo;` | `const foo = vi.mocked(...)` | Module-level: `vi.mock('./foo.js')`. | +| `when(foo.bar()).thenReturn(x)` | `vi.mocked(foo.bar).mockReturnValue(x)` | | +| `verify(foo).bar()` | `expect(foo.bar).toHaveBeenCalled()` | | +| `MockMvc.perform(get("/x"))` | `supertest(app).get('/x')` | No server boot needed. | +| `@Testcontainers` + `@Container PostgreSQLContainer` | `import { GenericContainer } from 'testcontainers';` in `beforeAll` | | + +## File layout + +``` +src/services/user-service.ts +src/services/user-service.test.ts # co-located unit test +tests/integration/users.test.ts # integration tests (testcontainers) +``` + +## Unit test skeleton + +```typescript +import 'reflect-metadata'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { UserService } from './user-service.js'; + +describe('UserService', () => { + let repo: { findById: ReturnType }; + let svc: UserService; + + beforeEach(() => { + repo = { findById: vi.fn() }; + svc = new UserService(repo as any); + }); + + it('returns the user when present', async () => { + repo.findById.mockResolvedValue({ id: 1n, name: 'Ada' }); + expect(await svc.getUser(1n)).toEqual({ id: 1n, name: 'Ada' }); + }); + + it('throws when missing', async () => { + repo.findById.mockResolvedValue(undefined); + await expect(svc.getUser(1n)).rejects.toThrow(/not found/); + }); +}); +``` + +## Integration test skeleton (testcontainers) + +```typescript +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { GenericContainer, StartedTestContainer } from 'testcontainers'; +import { drizzle } from 'drizzle-orm/node-postgres'; +import { Pool } from 'pg'; +import { buildApp } from '../src/app.js'; +import supertest from 'supertest'; + +let pg: StartedTestContainer; +let pool: Pool; +let app: ReturnType; + +beforeAll(async () => { + pg = await new GenericContainer('postgres:16') + .withEnvironment({ POSTGRES_USER: 't', POSTGRES_PASSWORD: 't', POSTGRES_DB: 't' }) + .withExposedPorts(5432) + .start(); + pool = new Pool({ host: pg.getHost(), port: pg.getMappedPort(5432), user: 't', password: 't', database: 't' }); + process.env.DATABASE_URL = `postgresql://t:t@${pg.getHost()}:${pg.getMappedPort(5432)}/t`; + // run drizzle migrations against pool + app = buildApp(); +}); + +afterAll(async () => { + await pool.end(); + await pg.stop(); +}); + +describe('POST /users', () => { + it('creates a user', async () => { + const res = await supertest(app).post('/users').send({ name: 'Ada', email: 'ada@example.com' }); + expect(res.status).toBe(201); + expect(res.body.id).toBeDefined(); + }); +}); +``` + +## What does NOT translate + +| Java | Why and what to do | +|---|---| +| `@SpringBootTest` (full context load) | Use `buildApp()` directly + supertest. No full-app boot needed. | +| `@MockBean` | `vi.mock` at module scope. | +| `@DirtiesContext` | Recreate state in `beforeEach`. | +| `@Transactional` on test | Use a transaction-rollback wrapper around each test, OR truncate tables in `beforeEach`. | +| TestNG `@Test(dataProvider = ...)` | `it.each([...])` | diff --git a/skills/java-to-typescript/references/categories/validation.md b/skills/java-to-typescript/references/categories/validation.md new file mode 100644 index 0000000..8b3736e --- /dev/null +++ b/skills/java-to-typescript/references/categories/validation.md @@ -0,0 +1,55 @@ +# Validation (Bean Validation → Zod) + +Hand-translate Bean Validation annotations to zod schemas. No codemod, no auto-generator. + +## Annotation → zod mapping table + +| Bean Validation | zod | +|---|---| +| `@NotNull` | (default — non-optional fields are non-null) | +| `@NotBlank` | `z.string().min(1).regex(/\S/)` | +| `@NotEmpty` on `String` | `z.string().min(1)` | +| `@NotEmpty` on `List` | `z.array(T).min(1)` | +| `@Size(min = m, max = M)` on `String` | `z.string().min(m).max(M)` | +| `@Size(min, max)` on `List` | `z.array(T).min(m).max(M)` | +| `@Min(n)` / `@Max(n)` | `z.number().min(n)` / `.max(n)` | +| `@DecimalMin("0.01")` | `z.string().refine((s) => new Decimal(s).gte('0.01'))` (preserve BigDecimal precision via Decimal.js) | +| `@Email` | `z.string().email()` | +| `@Pattern(regexp)` | `z.string().regex(/.../)` | +| `@Past` / `@Future` (`Instant`) | `.refine((d) => d < new Date())` / `> new Date()` | +| `@Valid` on nested | nested schema inside `z.object` | +| `@AssertTrue` on method | `.refine((obj) => predicate(obj), { message })` | + +## Per-DTO file structure + +```typescript +// src/schemas/create-user.ts +import { z } from 'zod'; + +export const CreateUserSchema = z.object({ + name: z.string().min(1).max(120), + email: z.string().email(), + balance: z.string().regex(/^-?\d+(\.\d{1,4})?$/, 'must be decimal'), +}).strict(); // strict() rejects unknown keys, matching Jackson FAIL_ON_UNKNOWN_PROPERTIES=true + +export type CreateUser = z.infer; +``` + +Validate at every HTTP boundary: + +```typescript +const dto = CreateUserSchema.parse(req.body); +``` + +## Validation groups + +Java validation groups (Default, OnCreate, OnUpdate) → distinct zod schemas per group. Do NOT try to share schemas via discriminated unions unless the API itself is genuinely polymorphic. + +```typescript +export const CreateUserSchema = z.object({ name: z.string(), email: z.string().email() }); +export const UpdateUserSchema = z.object({ name: z.string().optional(), email: z.string().email().optional() }); +``` + +## Error mapping + +Map `ZodError` to HTTP 400 in the error-handling middleware (see `targets/express.md`). Don't catch and re-throw at the handler. diff --git a/skills/java-to-typescript/references/frameworks/spring-boot.md b/skills/java-to-typescript/references/frameworks/spring-boot.md new file mode 100644 index 0000000..8f88d9c --- /dev/null +++ b/skills/java-to-typescript/references/frameworks/spring-boot.md @@ -0,0 +1,42 @@ +# Spring Boot porting reference + +This reference covers translation patterns from Spring Boot to TypeScript (default target: Express + tsyringe + zod, runtime Node). For specific TS-framework idioms, consult the matching file under `targets/`. + +## Annotation → TS translation table + +| Spring Boot | TypeScript equivalent | Notes | +|---|---|---| +| `@SpringBootApplication` | An `app.ts` that wires DI container + middleware + routes and calls `app.listen()` | No auto-config; everything explicit. | +| `@RestController` | A module of route handler functions registered with the Express router | One handler per file. | +| `@RequestMapping` / `@GetMapping("/users/{id}")` | `router.get('/users/:id', handler)` | URL params come via `req.params`. | +| `@RequestBody Foo dto` | `const dto = FooSchema.parse(req.body)` | DTO ≡ zod schema. | +| `@PathVariable` / `@RequestParam` | `req.params.id` / `req.query.q` | Type via zod schema in route input. | +| `@Service` / `@Component` / `@Repository` | `@injectable()` class registered in tsyringe container | Constructor injection only. | +| `@Autowired` (constructor) | `constructor(@inject('Foo') private foo: Foo)` | Field injection not supported. | +| `@Configuration` + `@Bean` | tsyringe `container.register('Token', { useFactory })` calls in `wiring.ts` | One wiring file per module. | +| `@ConfigurationProperties("app")` | `const config = AppConfigSchema.parse(process.env)` in `config.ts` | See `categories/config.md`. | +| `@Transactional` | Wrap query module calls in `db.transaction(async (tx) => ...)` | drizzle exposes `db.transaction`. | +| `@ExceptionHandler` | Single error-handling middleware mapping per-module Error subclasses → HTTP status | See `categories/di.md`. | +| `@Valid` on `@RequestBody` | `FooSchema.parse()` (throws on invalid) | Wrap in middleware to translate ZodError → 400. | +| `ResponseEntity` | `res.status(s).json(body)` | Don't introduce a `ResponseEntity` shim. | +| `@PreAuthorize("hasRole('ADMIN')")` | per-route `requireRole('ADMIN')` middleware | Method-level auth → explicit guard call. | +| Spring DI `@Qualifier("name")` | tsyringe string token: `@inject('name')` | Use injection tokens consistently. | + +## Layered structure mapping + +| Spring Boot package | TS module path | +|---|---| +| `com.example.controller` | `src/routes/` (one file per handler) | +| `com.example.service` | `src/services/` (one class per service) | +| `com.example.repository` | `src/repos/` (one module per repository) | +| `com.example.dto` | `src/schemas/` (zod schemas, types via z.infer) | +| `com.example.entity` | `src/db/schema.ts` (drizzle tables) | +| `com.example.config` | `src/config.ts` (single zod-parsed env object) | + +## Things explicitly to skip + +- `BeanPostProcessor`, `@PostConstruct` chains, AOP aspects — wire explicitly in `wiring.ts`. +- `ApplicationContext` global lookups — replace with explicit `container.resolve`. +- Auto-discovery via classpath scanning — register beans in `wiring.ts` by hand. +- `@EntityListeners`, `@PrePersist` callbacks — encode in the query module, not in entity decorators. +- Spring Profiles — use environment variables + a single `config.ts`. diff --git a/skills/java-to-typescript/references/library-map.yaml b/skills/java-to-typescript/references/library-map.yaml new file mode 100644 index 0000000..6d4f8c1 --- /dev/null +++ b/skills/java-to-typescript/references/library-map.yaml @@ -0,0 +1,187 @@ +# library-map.yaml — Java → TypeScript dependency registry +# Schema: see docs/superpowers/specs/2026-05-17-java-to-typescript-migration-skill-design.md §5.1 +# v0.1.0 (M0): 20 entries. Coverage expands in M1. + +default_picks: + di: { name: tsyringe, why: "lightweight; decorators map well to @Inject/@Autowired" } + validation: { name: zod, why: "runtime parse + zod-to-json-schema for OpenAPI" } + logging: { name: pino, why: "structured by default; pino-pretty for dev only" } + http_server: { name: express, why: "user-selected; composes with tsyringe + zod" } + http_client: { name: undici, why: "native fetch backbone; air-gap friendly" } + orm: { name: drizzle-orm, why: "schema-as-code; type-safe; no codegen; no daemon" } + migrations: { name: drizzle-kit, why: "same toolchain as orm; SQL-first migrations" } + cache: { name: keyv, why: "pluggable backends; MIT" } + decimal: { name: decimal.js, why: "BigDecimal parity" } + temporal: { name: temporal-polyfill, why: "LocalDate / ZonedDateTime parity; spec-track" } + date_idiomatic: { name: native-Date, why: "Instant parity; no dep" } + config: { name: zod, why: "env validated by same schema lib used for HTTP" } + observability: { name: "@opentelemetry/sdk-node", why: "Micrometer + OTel-java port" } + resilience: { name: cockatiel, why: "retry / circuit-breaker / bulkhead" } + testing_runner: { name: vitest, why: "JUnit5 mapping; vi.mock for Mockito; unit + integration" } + testing_containers: { name: testcontainers, why: "1:1 with Testcontainers-Java" } + json_idiomatic: { name: native-JSON, why: "default per type-fidelity policy" } + json_high_fid: { name: class-transformer, why: "Jackson polymorphism / custom serdes" } + +entries: + + - java: { groupId: org.springframework.boot, artifactId: spring-boot-starter-web, category: framework } + ts: + primary: { name: express, notes: "Default per default_picks.http_server. tsyringe + zod compose cleanly." } + alternatives: + - { name: koa, when: "source uses heavy middleware chains and async generators" } + - { name: hono, when: "target runtime is Bun or Deno" } + gotchas: + - { id: sb-actuator, summary: "spring-boot-starter-actuator endpoints (/health, /metrics) are not auto-ported; add explicit routes wired to @opentelemetry/sdk-node.", severity: medium } + - { id: sb-static-resources, summary: "Spring's static resource handler (/static, /public) -> use express.static or equivalent.", severity: low } + category_ref: categories/di.md + fidelity: { idiomatic: express, high_fidelity: express } + + - java: { groupId: org.springframework.boot, artifactId: spring-boot-starter-data-jpa, category: persistence } + ts: + primary: { name: drizzle-orm, notes: "Schema-as-code; type-safe. Map JPA entities -> drizzle table definitions; JpaRepository -> query module." } + gotchas: + - { id: jpa-lazy-loading, summary: "JPA lazy associations don't have a drop-in TS equivalent. Make joins explicit in the query module; document the cost.", severity: high } + - { id: jpa-cascade, summary: "@OneToMany(cascade = ALL) -> emulate via transaction wrapping in the query module; drizzle has no cascade declaration.", severity: high } + category_ref: categories/persistence.md + fidelity: { idiomatic: drizzle-orm, high_fidelity: drizzle-orm } + + - java: { groupId: org.springframework.boot, artifactId: spring-boot-starter-validation, category: validation } + ts: + primary: { name: zod, notes: "Hand-translate Bean Validation annotations to zod schemas. One schema per DTO." } + gotchas: + - { id: bv-class-level, summary: "@AssertTrue on a method-level constraint -> zod refine().", severity: medium } + - { id: bv-groups, summary: "Validation groups (Default, Create, Update) -> distinct zod schemas per group; do not try to share via discriminated unions unless the API itself is polymorphic.", severity: medium } + category_ref: categories/validation.md + fidelity: { idiomatic: zod, high_fidelity: zod } + + - java: { groupId: org.springframework.boot, artifactId: spring-boot-starter-security, category: security } + ts: + primary: { name: hand-rolled-middleware, notes: "No 1:1 TS framework. Build Express middleware backed by jose (JWT) + a per-route guard helper." } + alternatives: + - { name: passport, when: "OAuth2 / SAML flows; trade ergonomics for breadth" } + gotchas: + - { id: sec-method-level, summary: "@PreAuthorize annotations -> method-level checks via a guard helper; cannot rely on framework AOP.", severity: high } + - { id: sec-csrf, summary: "Spring Security's CSRF token model -> don't replicate for stateless JWT APIs; document the change.", severity: medium } + category_ref: null # no categories/security.md in M0; follow entry notes (jose-backed Express middleware + per-route guard helper) + fidelity: { idiomatic: hand-rolled-middleware, high_fidelity: hand-rolled-middleware } + + - java: { groupId: org.springframework.boot, artifactId: spring-boot-starter-test, category: testing } + ts: + primary: { name: vitest, notes: "Replaces JUnit 5; vi.mock for Mockito; expect API for AssertJ-style assertions." } + gotchas: + - { id: sbtest-mockmvc, summary: "MockMvc -> supertest against the Express app instance, no server boot needed.", severity: low } + category_ref: categories/testing.md + fidelity: { idiomatic: vitest, high_fidelity: vitest } + + - java: { groupId: org.hibernate.orm, artifactId: hibernate-core, category: persistence } + ts: + primary: { name: drizzle-orm, notes: "See spring-boot-starter-data-jpa entry." } + gotchas: + - { id: hib-second-level-cache, summary: "Hibernate L2 cache (Ehcache/Caffeine) -> not auto-replicated; add keyv-backed cache wrapper at the query module.", severity: medium } + category_ref: categories/persistence.md + fidelity: { idiomatic: drizzle-orm, high_fidelity: drizzle-orm } + + - java: { groupId: com.fasterxml.jackson.core, artifactId: jackson-databind, category: serialization } + ts: + primary: + name: native-json + notes: | + JSON.stringify / JSON.parse handle the 90% case. No runtime dep. + Loses: polymorphic type info (@JsonTypeInfo), custom (de)serializers, + date-format coercion. + alternatives: + - { name: class-transformer, when: "source uses @JsonProperty / @JsonCreator heavily AND high-fidelity policy is in scope" } + gotchas: + - { id: jackson-bigdecimal, summary: "Jackson serializes BigDecimal to JSON number; JSON.stringify on Decimal.js value emits {}. Force toString() on the boundary.", severity: high } + - { id: jackson-nested-generics, summary: "Jackson resolves Map>; native JSON loses the Foo type. Wrap with Zod.parse at the boundary.", severity: medium } + category_ref: categories/validation.md + fidelity: { idiomatic: native-json, high_fidelity: class-transformer } + + - java: { groupId: com.fasterxml.jackson.core, artifactId: jackson-core, category: serialization } + ts: + primary: { name: native-json, notes: "See jackson-databind." } + category_ref: categories/validation.md + fidelity: { idiomatic: native-json, high_fidelity: class-transformer } + + - java: { groupId: org.slf4j, artifactId: slf4j-api, category: logging } + ts: + primary: { name: pino, notes: "Module-scoped child logger. LoggerFactory.getLogger(Foo.class) -> const log = base.child({ module: 'Foo' })." } + gotchas: + - { id: slf4j-mdc, summary: "MDC.put('userId', x) -> bind via pino child or AsyncLocalStorage. Don't rely on thread-local semantics.", severity: medium } + category_ref: categories/logging.md + fidelity: { idiomatic: pino, high_fidelity: pino } + + - java: { groupId: ch.qos.logback, artifactId: logback-classic, category: logging } + ts: + primary: { name: pino, notes: "logback.xml appenders -> pino transports; XML config -> JSON object passed to pino()." } + category_ref: categories/logging.md + fidelity: { idiomatic: pino, high_fidelity: pino } + + - java: { groupId: org.junit.jupiter, artifactId: junit-jupiter, category: testing } + ts: + primary: { name: vitest, notes: "@Test -> it(); @BeforeEach -> beforeEach; @ParameterizedTest -> it.each." } + category_ref: categories/testing.md + fidelity: { idiomatic: vitest, high_fidelity: vitest } + + - java: { groupId: org.mockito, artifactId: mockito-core, category: testing } + ts: + primary: { name: vitest, notes: "vi.mock for module-level mocks; vi.fn for spies; vi.spyOn for partial. Mockito @Mock fields -> vi.mocked()." } + category_ref: categories/testing.md + fidelity: { idiomatic: vitest, high_fidelity: vitest } + + - java: { groupId: org.assertj, artifactId: assertj-core, category: testing } + ts: + primary: { name: vitest, notes: "assertThat(x).isEqualTo(y) -> expect(x).toEqual(y). AssertJ fluent chains -> expect chains." } + category_ref: categories/testing.md + fidelity: { idiomatic: vitest, high_fidelity: vitest } + + - java: { groupId: org.testcontainers, artifactId: testcontainers, category: testing } + ts: + primary: { name: testcontainers, notes: "1:1 mapping. PostgreSQLContainer -> @testcontainers/postgresql equivalent." } + category_ref: categories/testing.md + fidelity: { idiomatic: testcontainers, high_fidelity: testcontainers } + + - java: { groupId: org.projectlombok, artifactId: lombok, category: utility } + ts: + primary: { name: drop, notes: "TS classes don't need @Data — public fields + structural typing replace getters/setters." } + gotchas: + - { id: lombok-builder, summary: "@Builder pattern -> use a plain factory function or zod schema with .parse(). Do not port Builder mechanically.", severity: medium } + - { id: lombok-slf4j, summary: "@Slf4j -> import { logger } and rename log.* calls.", severity: low } + category_ref: categories/logging.md + fidelity: { idiomatic: drop, high_fidelity: drop } + + - java: { groupId: org.mapstruct, artifactId: mapstruct, category: utility } + ts: + primary: { name: hand-written-mappers, notes: "TS has no MapStruct equivalent worth introducing. Hand-write a mapper module per DTO ⇔ entity pair." } + gotchas: + - { id: mapstruct-zod, summary: "If the source and target both have zod schemas, use schema-to-schema transforms via .transform() instead of a separate mapper.", severity: low } + category_ref: categories/validation.md + fidelity: { idiomatic: hand-written-mappers, high_fidelity: hand-written-mappers } + + - java: { groupId: jakarta.validation, artifactId: jakarta.validation-api, category: validation } + ts: + primary: { name: zod, notes: "See spring-boot-starter-validation." } + category_ref: categories/validation.md + fidelity: { idiomatic: zod, high_fidelity: zod } + + - java: { groupId: org.hibernate.validator, artifactId: hibernate-validator, category: validation } + ts: + primary: { name: zod, notes: "See spring-boot-starter-validation." } + category_ref: categories/validation.md + fidelity: { idiomatic: zod, high_fidelity: zod } + + - java: { groupId: io.github.resilience4j, artifactId: resilience4j-spring-boot3, category: resilience } + ts: + primary: { name: cockatiel, notes: "@CircuitBreaker / @Retry -> cockatiel policies wrapping the call site. No annotation-style declaration." } + gotchas: + - { id: r4j-bulkhead, summary: "Semaphore bulkhead -> cockatiel bulkheadPolicy. ThreadPool bulkhead doesn't translate; redesign as worker pool if used.", severity: high } + category_ref: null # no categories/resilience.md in M0; follow entry notes (cockatiel policies wrapping call sites) + fidelity: { idiomatic: cockatiel, high_fidelity: cockatiel } + + - java: { groupId: com.google.guava, artifactId: guava, category: utility } + ts: + primary: { name: native-and-lodash, notes: "ImmutableList -> readonly T[]; Multimap -> Map wrapper; Cache -> keyv; Preconditions -> throw new Error." } + gotchas: + - { id: guava-cache, summary: "CacheBuilder.expireAfterAccess -> keyv with TTL; loading caches -> wrap with a memoize helper.", severity: medium } + category_ref: null # no categories/utility.md in M0; follow entry notes (native + selective lodash; keyv for caches) + fidelity: { idiomatic: native-and-lodash, high_fidelity: native-and-lodash } diff --git a/skills/java-to-typescript/references/migration-modes.md b/skills/java-to-typescript/references/migration-modes.md new file mode 100644 index 0000000..f5ae20b --- /dev/null +++ b/skills/java-to-typescript/references/migration-modes.md @@ -0,0 +1,62 @@ +# Migration modes + +Three modes. The skill asks the user to pick at Phase 2. + +## Decision matrix + +| Question | Full rewrite | Strangler-fig | Module-by-module | +|---|---|---|---| +| Can the team afford a multi-week freeze on Java changes? | Yes | No | Partial | +| Is there a reverse proxy in front of the service? | N/A | Required | Optional | +| Do modules have clean dependency boundaries? | N/A | Required for shared state | Required | +| Does the service have a public API SLA? | Risky | Safe | Safe | +| Estimated effort multiplier vs full rewrite | 1× | 1.5–2× | 1.2–1.5× | + +## Full rewrite + +Use when: the service is small (<10 KLOC Java), the team can freeze Java changes for the duration, and there's no live multi-team SLA. + +Procedure: +1. Phase 1 over the whole repo. +2. Phase 2 plan covers all modules at once. +3. Phase 3 ports all modules; no Java code runs in parallel. +4. Cut over at deployment. + +Risk: every bug in the port is a production bug at cut-over. + +## Strangler-fig (Martin Fowler pattern) + +Use when: the service has a live SLA, multiple teams depend on it, and you can put a reverse proxy in front. + +Procedure: +1. Phase 1 over the whole repo. +2. Phase 2 plan picks ONE route (or a small set) to port first. +3. Phase 3 ports that route to TS; deploy the TS service alongside Java. +4. Configure the proxy (Nginx, Envoy, HAProxy) to route `/users/*` to TS, everything else to Java. +5. Run Phase 4 to verify the ported routes match Java byte-for-byte. +6. Repeat 2–5 for the next route until Java has nothing left. +7. Decommission Java. + +Skill scaffolds: +- `proxy/nginx.conf` (or equivalent) with explicit route-by-route mapping +- `migration/proxy-routes.md` documenting which routes live in which stack at each migration step +- Shared `contracts/` directory with zod schemas that both stacks must conform to (Java uses jsonschema generated from zod via zod-to-json-schema) + +## Module-by-module + +Use when: the Java repo has clean module boundaries (e.g., `api/`, `core/`, `worker/`), and you can ship modules independently without a live proxy. + +Procedure: +1. Phase 1 builds the dependency DAG between modules. +2. Phase 2 plans the port order — leaves of the DAG first (modules with no internal deps), root last. +3. Phase 3 ports modules one at a time; intermediate state is `core (TS) + api (Java still calling core via JNI shim or process boundary)`. Use whichever IPC mechanism the existing Java already supports. +4. Phase 4 runs once per ported module (against its public API). + +This mode is the middle ground: lower risk than full rewrite, less infra than strangler-fig. + +## Always (regardless of mode) + +- `migration/` is gitignored in the Java repo (skill adds the entry). +- Every phase's artifact is committed-by-user-choice only — the skill never auto-commits. +- The skill never starts services. The user starts Java and TS. +- Phase 2 user gate is hard. Phase 4 acceptance gate is hard. diff --git a/skills/java-to-typescript/references/platform-adaptation.md b/skills/java-to-typescript/references/platform-adaptation.md new file mode 100644 index 0000000..345dcd2 --- /dev/null +++ b/skills/java-to-typescript/references/platform-adaptation.md @@ -0,0 +1,76 @@ +# Platform adaptation + +`java-to-typescript` is a **standalone skill**. It does not depend on any other skill. It does, however, use two platform-native primitives that have different names across host environments: + +1. **Interactive prompts** — to ask the user for migration mode, target framework, unmapped-library resolutions, plan approval, etc. +2. **Parallel work dispatch** — to port independent Maven/Gradle modules concurrently in Phase 3. + +This reference maps both primitives across the three supported host platforms. If you are reading this file as the LLM driving the skill, pick the row matching your platform. + +## Platform → primitive map + +| Concern | Claude Code | Copilot CLI | Codex CLI | Fallback (no native primitive) | +|---|---|---|---|---| +| Interactive prompt | `AskUserQuestion` tool | `prompt_user` tool (per Copilot's tool surface) | `ask_user` / structured-question tool | Print question to stdout, read line from stdin, validate against expected answer set | +| Parallel module dispatch | `Agent` tool with `subagent_type` + `run_in_background` (or `superpowers:subagent-driven-development` if installed) | `worker` / `task` tool with parallel execution | `parallel_tasks` primitive | Port modules sequentially in dependency-DAG order — correctness is unaffected, only wall-clock cost | +| File I/O for migration artifacts | `Write` / `Edit` / `Read` | equivalent file tools | equivalent file tools | Standard `fs` module from the runtime | +| Shell invocation (for `tsx scripts/...`) | `Bash` tool | `shell` / `bash` tool | `shell` tool | `child_process.spawn` from Node | + +## Read this if you're running on Claude Code + +You have everything you need natively. The skill's `SKILL.md` mentions `AskUserQuestion` directly because that's the canonical Claude Code primitive. For Phase 3 parallelization, your `Agent` tool with `subagent_type: "general-purpose"` is the simplest dispatch; if the user has the `superpowers:subagent-driven-development` skill installed, that adds a per-task review loop on top — useful but not required. + +## Read this if you're running on Copilot CLI + +The skill ships as a Maven artifact (`io.github.randomcodespace.ai:java-to-typescript`). Install it into your Copilot CLI plugin directory: + +```bash +mvn dependency:copy \ + -Dartifact=io.github.randomcodespace.ai:java-to-typescript:0.0.1 \ + -DoutputDirectory=./tmp +unzip -o tmp/java-to-typescript-0.0.1-bin.zip -d ~/.copilot/plugins/ +``` + +Then your `skill` tool auto-discovers the SKILL.md. When you see `AskUserQuestion`, substitute Copilot's interactive-prompt tool. When Phase 3 step 6 says "parallelize independent modules", use Copilot's worker dispatch. + +The companion scripts under `scripts/` are pure Node + Vitest — they run identically. No code changes needed. + +## Read this if you're running on Codex CLI + +Same install pattern, target dir `~/.codex/plugins/` (or wherever Codex looks for plugins on your install). Codex's tool surface is closer to Claude Code's than Copilot's; the substitutions are minor: + +| Claude Code | Codex CLI | +|---|---| +| `AskUserQuestion` | structured `ask_user` | +| `Agent` + `subagent_type` | `parallel_tasks` or single-task delegation | +| `Bash` | `shell` | + +The scripts and references are unchanged. + +## Read this if your platform has neither primitive + +The skill degrades gracefully: + +- **No interactive prompt:** Print questions to stdout, accept `--answers ` on the command line as a non-interactive override. The user provides answers up-front in JSON; the skill never blocks. +- **No parallel dispatch:** Port modules sequentially, leaves of the dependency DAG first. The four phases and all gates still work; only wall-clock time changes. + +In both fallback modes, the skill's deliverable (a working TS workspace + verify report) is identical. + +## What the skill explicitly does NOT depend on + +To remain standalone, the skill avoids: + +- **No cross-skill invocations.** `SKILL.md` references no other skill by name. Earlier drafts mentioned `superpowers:subagent-driven-development`; that's been removed in favor of platform-agnostic guidance. +- **No platform-specific scaffolding tools.** Workspace scaffolding goes through `scripts/pom-to-workspace.ts scaffold`, which is pure Node. +- **No runtime internet calls.** The library registry is the source of truth; `context7` and the npm registry are NEVER called at skill runtime (see hard constraint 2 in `SKILL.md`). +- **No specific LLM provider for the orchestration.** The skill works with any LLM that has tool-use and structured-output capability. Only the eval harness (E2/E3 in real mode) optionally calls Anthropic — and that's opt-in via `ANTHROPIC_API_KEY`; see `evals/README.md`. + +## Distribution + +| Platform | Install command | +|---|---| +| **From source** | `git clone` + `cp -r skills/java-to-typescript ~/.claude/skills/` (or equivalent skill dir) | +| **Maven Central** | `mvn dependency:copy -Dartifact=io.github.randomcodespace.ai:java-to-typescript: -DoutputDirectory=./tmp && unzip tmp/java-to-typescript--bin.zip -d ` | +| **Air-gapped enterprise** | Mirror the artifact to your internal Maven repo (Artifactory / Nexus); the skill bundle is a self-contained zip with vendored dependencies — no npm registry needed at install time | + +The bundle is the same zip on every platform. Tool-name shims live in the host's adapter layer, not in the skill. diff --git a/skills/java-to-typescript/references/runtimes/node.md b/skills/java-to-typescript/references/runtimes/node.md new file mode 100644 index 0000000..7c23b63 --- /dev/null +++ b/skills/java-to-typescript/references/runtimes/node.md @@ -0,0 +1,58 @@ +# Node runtime reference + +Default runtime. Use Node 20 LTS or later. + +## Tooling + +- **Compiler:** `tsc` for production builds (emits to `dist/`). +- **Dev runner:** `tsx` for running `.ts` directly (`tsx src/server.ts`). +- **Test runner:** `vitest` (configured in `vitest.config.ts`). +- **Process manager:** none in dev; in prod, whatever the user runs (PM2, systemd, Kubernetes). + +## Module system + +- `"type": "module"` in every `package.json`. +- `module: "NodeNext"` and `moduleResolution: "NodeNext"` in tsconfig. +- All relative imports MUST include the `.js` extension (even from `.ts` sources): `import { foo } from './foo.js'`. +- CJS interop via `esModuleInterop: true`. Avoid `require()` in new code. + +## Startup convention + +```typescript +// src/server.ts +import { buildApp } from './app.js'; +import { config } from './config.js'; +import { logger } from './logger.js'; + +const app = buildApp(); +const server = app.listen(config.PORT, () => { + logger.info({ port: config.PORT }, 'listening'); +}); + +// Graceful shutdown +const shutdown = () => { + server.close(() => process.exit(0)); + setTimeout(() => process.exit(1), 10_000).unref(); +}; +process.on('SIGTERM', shutdown); +process.on('SIGINT', shutdown); +``` + +## Things to know that catch Java devs out + +- No thread pool. All I/O is async/event-loop driven. Don't introduce worker threads unless CPU-bound. +- No checked exceptions. The type system doesn't reflect what a function can throw. +- `process.env.X` is `string | undefined`. Always validate via zod at startup. +- Top-level `await` works in ESM modules but not in CJS. +- `package.json` `exports` field is significant — drizzle, pino, etc. use it for sub-paths. + +## Air-gap considerations + +- Use a proxied npm registry (Verdaccio, Artifactory, Nexus). Configure via `.npmrc`: + +``` +registry=https://npm.internal.example.com/ +``` + +- Never commit a registry URL containing credentials. Inject via `NPM_CONFIG_REGISTRY` env if needed. +- Pre-cache critical packages: `undici`, `pino`, `zod`, `express`, `drizzle-orm`, `tsyringe`, `decimal.js`, `temporal-polyfill`. diff --git a/skills/java-to-typescript/references/targets/express.md b/skills/java-to-typescript/references/targets/express.md new file mode 100644 index 0000000..30fd135 --- /dev/null +++ b/skills/java-to-typescript/references/targets/express.md @@ -0,0 +1,96 @@ +# Express target reference + +Default target for Spring Boot / Quarkus / Micronaut / Spring MVC ports unless the user overrides. + +## Skeleton module structure + +``` +src/ +├── app.ts # builds the express app; exports for tests +├── server.ts # imports app + listens; the actual entry point +├── wiring.ts # tsyringe container.register calls +├── config.ts # const AppConfig = z.object(...).parse(process.env) +├── logger.ts # const baseLogger = pino(...); export logger +├── routes/ # one file per handler +├── services/ # one file per service class +├── repos/ # one file per repository (drizzle query module) +├── schemas/ # one file per DTO (zod schema + z.infer type) +└── db/ + └── schema.ts # drizzle table definitions +``` + +## `app.ts` skeleton + +```typescript +import 'reflect-metadata'; +import express from 'express'; +import { container } from 'tsyringe'; +import { logger } from './logger.js'; +import './wiring.js'; +import { errorHandler } from './middleware/errors.js'; +import { mountUserRoutes } from './routes/index.js'; + +export function buildApp(): express.Express { + const app = express(); + app.use(express.json({ limit: '1mb' })); + app.use((req, _res, next) => { logger.info({ m: req.method, u: req.url }, 'req'); next(); }); + mountUserRoutes(app); + app.use(errorHandler); + return app; +} +``` + +## DI integration (tsyringe) + +- `import 'reflect-metadata'` MUST be the first import in `app.ts` (and in tests that resolve DI). +- `tsconfig.json` must have `experimentalDecorators: true` and `emitDecoratorMetadata: true`. +- Register tokens in `wiring.ts`: + +```typescript +import { container } from 'tsyringe'; +import { UserService } from './services/user-service.js'; +import { db } from './db/client.js'; + +container.register('Db', { useValue: db }); +container.register('UserService', { useClass: UserService }); +``` + +## Validation (zod) + +Validate every HTTP boundary: + +```typescript +import { z } from 'zod'; +import type { Request, Response } from 'express'; + +export const CreateUserSchema = z.object({ name: z.string().min(1), email: z.string().email() }); +export type CreateUser = z.infer; + +export async function createUserHandler(req: Request, res: Response): Promise { + const dto = CreateUserSchema.parse(req.body); // throws ZodError → caught by errorHandler + // ... call service ... + res.status(201).json(/* created */); +} +``` + +## Error mapping middleware + +```typescript +import { ZodError } from 'zod'; +import type { ErrorRequestHandler } from 'express'; + +export const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => { + if (err instanceof ZodError) { res.status(400).json({ errors: err.errors }); return; } + if (err instanceof UsersError) { res.status(err.status).json({ error: err.message }); return; } + res.status(500).json({ error: 'internal' }); +}; +``` + +## What Express does NOT give you (and how to add it) + +| Need | Add | +|---|---| +| Schema-first → OpenAPI | `zod-to-json-schema` + manual `/openapi.json` route | +| Async route handlers without try/catch boilerplate | Tiny `asyncRoute(fn)` wrapper or `express-async-errors` (small, MIT) | +| Request-scoped DI | Use tsyringe child container per request via middleware | +| Built-in CORS | `cors` package | diff --git a/skills/java-to-typescript/references/type-fidelity.md b/skills/java-to-typescript/references/type-fidelity.md new file mode 100644 index 0000000..42829cf --- /dev/null +++ b/skills/java-to-typescript/references/type-fidelity.md @@ -0,0 +1,93 @@ +# Type fidelity policy + +Hybrid: idiomatic TypeScript by default, high-fidelity escape for known-risky types. Deviations from this table require a one-line entry in the migration's `port-log.md` explaining why. + +## Per-type table + +| Java type | Idiomatic default | High-fidelity escape | Trigger | +|---|---|---|---| +| `Optional` | `T \| undefined` | `Option` from `@mobily/ts-belt` | user opts module in | +| `@Nullable T` | `T \| null` | same | always (consistent with Java intent) | +| `BigDecimal` | `Decimal` from `decimal.js` | same | **always** | +| `BigInteger` | `bigint` | same | always | +| `long` (non-ID) | `number` | `bigint` | range can exceed `Number.MAX_SAFE_INTEGER` | +| `Long` used as entity ID | `bigint` serialized as string on the wire | same | **always** | +| `Instant` | `Date` | same | — | +| `LocalDate` / `LocalDateTime` / `ZonedDateTime` | `Temporal.*` via `temporal-polyfill` | same | **always** | +| `UUID` | branded `string` (`type UUID = string & { readonly __brand: 'UUID' }`) | same | always | +| `Stream` | `T[]` | `AsyncIterable` | genuinely lazy / paginated | +| `Collection` / `List` | `T[]` | `readonly T[]` | exposed on public DTO | +| `Map` | `Record` | `Map` | iteration order / non-string keys | +| `Set` | `Set` | same | — | +| `enum` | TS `enum` (narrow ordinals), const-union (string values) | discriminated union | enum carries methods | +| `record Foo` | `interface Foo` (readonly fields) | `class Foo` w/ frozen ctor | `instanceof` matters | +| `sealed interface` | discriminated union with `kind` tag | same | always | +| Checked exception on public API | tagged-union `Result` (hand-rolled) | same | **always** for public API | +| Unchecked exception (internal) | `throw` | same | — | +| Lombok `@Data` | plain class with public fields | class + structural equality | used in `Set`/`Map` keys | +| Generic bound `` | preserve as TS constraint | same | always | +| Wildcards (`? extends T` / `? super T`) | relax to invariant; flag in `port-log.md` | variance helper type | API depends on variance | +| `equals` / `hashCode` | drop unless used | implement `equals(other)` + structural hash | actually used in collections | +| `Comparable` | export `compare: (a, b) => number` | same | always | + +## High-fidelity sketches + +### `Result` (replaces checked exceptions on public APIs) + +```typescript +export type Result = { ok: true; value: T } | { ok: false; error: E }; +export const ok = (value: T): Result => ({ ok: true, value }); +export const err = (error: E): Result => ({ ok: false, error }); +``` + +### Branded UUID + +```typescript +declare const __brand: unique symbol; +export type UUID = string & { readonly [__brand]: 'UUID' }; +export const asUuid = (s: string): UUID => s as UUID; // validate caller-side +``` + +### bigint IDs on the wire + +JSON.stringify cannot serialize bigint. Convert to string at the boundary: + +```typescript +const responseBody = { id: user.id.toString(), name: user.name }; +``` + +Document the format in OpenAPI as `string` with pattern `^-?\d+$`. + +## tsconfig (non-negotiable) + +```jsonc +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "useDefineForClassFields": false, + "isolatedModules": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": false + } +} +``` + +`experimentalDecorators` is required for tsyringe and class-transformer. Plan the move to Stage 3 decorators when those libraries support them. + +## Non-negotiable patterns + +- One handler per file. `@RestController` with N methods → N TS files under `routes/`. +- DTO ≡ zod schema. No interfaces on HTTP boundaries without a schema. +- Repository ≡ Drizzle query module. No faked JPA repository interfaces. +- Config ≡ zod-validated env at startup. No scattered `process.env.FOO`. +- Logger ≡ module-scoped pino child. No `console.log`. +- Errors ≡ subclasses of a per-module base class. HTTP mapping via a single middleware. diff --git a/skills/java-to-typescript/scripts/__tests__/fixtures/pom-analyze/api/pom.xml b/skills/java-to-typescript/scripts/__tests__/fixtures/pom-analyze/api/pom.xml new file mode 100644 index 0000000..8cb9b13 --- /dev/null +++ b/skills/java-to-typescript/scripts/__tests__/fixtures/pom-analyze/api/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + com.example + parent + 1.0.0 + + api + + + org.springframework.boot + spring-boot-starter-web + 3.3.0 + + + com.example + core + 1.0.0 + + + diff --git a/skills/java-to-typescript/scripts/__tests__/fixtures/pom-analyze/core/pom.xml b/skills/java-to-typescript/scripts/__tests__/fixtures/pom-analyze/core/pom.xml new file mode 100644 index 0000000..77b1657 --- /dev/null +++ b/skills/java-to-typescript/scripts/__tests__/fixtures/pom-analyze/core/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + com.example + parent + 1.0.0 + + core + + + org.hibernate.orm + hibernate-core + 6.5.0.Final + + + com.acme.internal + weird-thing + 1.0 + + + diff --git a/skills/java-to-typescript/scripts/__tests__/fixtures/pom-analyze/pom.xml b/skills/java-to-typescript/scripts/__tests__/fixtures/pom-analyze/pom.xml new file mode 100644 index 0000000..aca520f --- /dev/null +++ b/skills/java-to-typescript/scripts/__tests__/fixtures/pom-analyze/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + com.example + parent + 1.0.0 + pom + + api + core + + diff --git a/skills/java-to-typescript/scripts/__tests__/pom-to-workspace.analyze.test.ts b/skills/java-to-typescript/scripts/__tests__/pom-to-workspace.analyze.test.ts new file mode 100644 index 0000000..6633f50 --- /dev/null +++ b/skills/java-to-typescript/scripts/__tests__/pom-to-workspace.analyze.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest'; +import { analyze } from '../pom-to-workspace.js'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const fixture = path.join(here, 'fixtures/pom-analyze'); + +describe('analyze', () => { + it('detects build system and modules', async () => { + const result = await analyze(fixture); + expect(result.buildSystem).toBe('maven'); + expect(result.rootGroupId).toBe('com.example'); + expect(result.modules.map((m) => m.path).sort()).toEqual(['api', 'core']); + }); + + it('detects Spring Boot via dependency coords', async () => { + const result = await analyze(fixture); + const api = result.modules.find((m) => m.path === 'api')!; + expect(api.detectedFramework).toBe('spring-boot'); + }); + + it('flags unmapped dependencies', async () => { + const result = await analyze(fixture); + expect(result.unmappedDependencies).toContainEqual({ + groupId: 'com.acme.internal', + artifactId: 'weird-thing', + usedBy: ['core'], + }); + }); + + it('does not flag known dependencies as unmapped', async () => { + const result = await analyze(fixture); + const unmapped = result.unmappedDependencies.map((d) => `${d.groupId}:${d.artifactId}`); + expect(unmapped).not.toContain('org.springframework.boot:spring-boot-starter-web'); + expect(unmapped).not.toContain('org.hibernate.orm:hibernate-core'); + }); + + it('treats intra-project deps (same rootGroupId) as not unmapped', async () => { + const result = await analyze(fixture); + const unmapped = result.unmappedDependencies.map((d) => `${d.groupId}:${d.artifactId}`); + expect(unmapped).not.toContain('com.example:core'); + }); +}); diff --git a/skills/java-to-typescript/scripts/__tests__/pom-to-workspace.scaffold.test.ts b/skills/java-to-typescript/scripts/__tests__/pom-to-workspace.scaffold.test.ts new file mode 100644 index 0000000..8940537 --- /dev/null +++ b/skills/java-to-typescript/scripts/__tests__/pom-to-workspace.scaffold.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { scaffold, type ScaffoldPlan } from '../pom-to-workspace.js'; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import * as os from 'node:os'; + +let outDir: string; + +beforeEach(async () => { + outDir = await fs.mkdtemp(path.join(os.tmpdir(), 'scaffold-')); +}); + +const plan: ScaffoldPlan = { + packageManager: 'npm', + runtime: 'node', + rootName: '@example/services', + modules: [ + { + path: 'users', + name: '@example/users-service', + tsFramework: 'express', + dependencies: { express: '^4', tsyringe: '^4', zod: '^3', pino: '^9' }, + devDependencies: { typescript: '^5', vitest: '^2', tsx: '^4', '@types/express': '^4' }, + }, + ], +}; + +describe('scaffold', () => { + it('writes root package.json with workspaces array', async () => { + await scaffold(plan, outDir); + const root = JSON.parse(await fs.readFile(path.join(outDir, 'package.json'), 'utf8')); + expect(root.name).toBe('@example/services'); + expect(root.workspaces).toEqual(['users']); + expect(root.private).toBe(true); + }); + + it('writes per-module package.json', async () => { + await scaffold(plan, outDir); + const mod = JSON.parse(await fs.readFile(path.join(outDir, 'users', 'package.json'), 'utf8')); + expect(mod.name).toBe('@example/users-service'); + expect(mod.dependencies.express).toBe('^4'); + expect(mod.devDependencies.vitest).toBe('^2'); + }); + + it('writes root tsconfig.base.json', async () => { + await scaffold(plan, outDir); + const base = JSON.parse(await fs.readFile(path.join(outDir, 'tsconfig.base.json'), 'utf8')); + expect(base.compilerOptions.strict).toBe(true); + expect(base.compilerOptions.noUncheckedIndexedAccess).toBe(true); + expect(base.compilerOptions.exactOptionalPropertyTypes).toBe(true); + expect(base.compilerOptions.experimentalDecorators).toBe(true); + }); + + it('writes per-module tsconfig that extends base', async () => { + await scaffold(plan, outDir); + const tsc = JSON.parse(await fs.readFile(path.join(outDir, 'users', 'tsconfig.json'), 'utf8')); + expect(tsc.extends).toBe('../tsconfig.base.json'); + }); + + it('writes .gitignore including migration/ and node_modules/', async () => { + await scaffold(plan, outDir); + const gi = await fs.readFile(path.join(outDir, '.gitignore'), 'utf8'); + expect(gi).toContain('node_modules/'); + expect(gi).toContain('migration/'); + expect(gi).toContain('dist/'); + }); + + it('rejects unsupported runtime/PM combos', async () => { + const bad: ScaffoldPlan = { ...plan, runtime: 'node', packageManager: 'unknown' as any }; + await expect(scaffold(bad, outDir)).rejects.toThrow(/unsupported/); + }); + + it('never writes source code', async () => { + await scaffold(plan, outDir); + const usersSrc = await fs.readdir(path.join(outDir, 'users')).catch(() => []); + expect(usersSrc.filter((f) => f.endsWith('.ts'))).toEqual([]); + }); +}); diff --git a/skills/java-to-typescript/scripts/__tests__/record-fixtures.test.ts b/skills/java-to-typescript/scripts/__tests__/record-fixtures.test.ts new file mode 100644 index 0000000..8f45e5f --- /dev/null +++ b/skills/java-to-typescript/scripts/__tests__/record-fixtures.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { createServer, type Server } from 'node:http'; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import * as os from 'node:os'; +import { recordCorpus, type CorpusEntry } from '../record-fixtures.js'; + +let server: Server; +let baseUrl: string; +let tmp: string; + +beforeAll(async () => { + server = createServer((req, res) => { + res.setHeader('content-type', 'application/json'); + res.setHeader('x-trace', 'abc'); + if (req.method === 'GET' && req.url === '/users/42') { + res.statusCode = 200; + res.end(JSON.stringify({ id: 42, name: 'Ada' })); + } else if (req.method === 'POST' && req.url === '/users') { + res.statusCode = 201; + res.end(JSON.stringify({ id: 99, name: 'Lovelace' })); + } else { + res.statusCode = 404; + res.end(JSON.stringify({ error: 'not found' })); + } + }); + await new Promise((r) => server.listen(0, '127.0.0.1', () => r())); + const addr = server.address(); + baseUrl = `http://127.0.0.1:${typeof addr === 'object' && addr ? addr.port : 0}`; + tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'rec-')); +}); + +afterAll(() => new Promise((r) => server.close(() => r()))); + +describe('recordCorpus', () => { + it('records a GET response with status, headers, body', async () => { + const corpus: CorpusEntry[] = [ + { name: 'GET-user', method: 'GET', path: '/users/42', headers: { accept: 'application/json' } }, + ]; + const out = path.join(tmp, 'fixtures.jsonl'); + await recordCorpus(baseUrl, corpus, out); + const lines = (await fs.readFile(out, 'utf8')).trim().split('\n').map((l) => JSON.parse(l)); + expect(lines).toHaveLength(1); + expect(lines[0].response.status).toBe(200); + expect(lines[0].response.body).toEqual({ id: 42, name: 'Ada' }); + expect(lines[0].response.headers['x-trace']).toBe('abc'); + }); + + it('marks unexpected 5xx (and 4xx) responses', async () => { + const corpus: CorpusEntry[] = [ + { name: 'GET-missing', method: 'GET', path: '/nope' }, + ]; + const out = path.join(tmp, 'fixtures-404.jsonl'); + await recordCorpus(baseUrl, corpus, out); + const line = JSON.parse((await fs.readFile(out, 'utf8')).trim()); + expect(line.response.status).toBe(404); + expect(line.unexpectedStatus).toBe(true); + }); + + it('rejects non-loopback base URLs', async () => { + await expect(recordCorpus('http://example.com', [], path.join(tmp, 'x.jsonl'))).rejects.toThrow(/loopback/); + }); +}); diff --git a/skills/java-to-typescript/scripts/__tests__/replay-fixtures.test.ts b/skills/java-to-typescript/scripts/__tests__/replay-fixtures.test.ts new file mode 100644 index 0000000..6f4b9d9 --- /dev/null +++ b/skills/java-to-typescript/scripts/__tests__/replay-fixtures.test.ts @@ -0,0 +1,85 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { createServer, type Server } from 'node:http'; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import * as os from 'node:os'; +import { replayFixtures } from '../replay-fixtures.js'; + +let server: Server; +let baseUrl: string; +let tmp: string; + +beforeAll(async () => { + server = createServer((req, res) => { + res.setHeader('content-type', 'application/json'); + res.statusCode = 200; + if (req.url === '/users/42') { + res.end(JSON.stringify({ id: 42, name: 'Ada', createdAt: '2026-05-17T11:00:00Z' })); + } else if (req.url === '/divergent/1') { + res.end(JSON.stringify({ id: 1, name: 'CHANGED' })); + } else { + res.statusCode = 404; + res.end('{}'); + } + }); + await new Promise((r) => server.listen(0, '127.0.0.1', () => r())); + const addr = server.address(); + baseUrl = `http://127.0.0.1:${typeof addr === 'object' && addr ? addr.port : 0}`; + tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'rep-')); +}); + +afterAll(() => new Promise((r) => server.close(() => r()))); + +describe('replayFixtures', () => { + it('reports zero diffs for an exact match', async () => { + const fixtures = path.join(tmp, 'f.jsonl'); + await fs.writeFile(fixtures, JSON.stringify({ + name: 'GET-user', + request: { method: 'GET', path: '/users/42', headers: {}, body: null }, + response: { status: 200, headers: { 'content-type': 'application/json' }, body: { id: 42, name: 'Ada', createdAt: '2026-05-01T00:00:00Z' } }, + capturedAt: '2026-05-17T10:00:00Z', + }) + '\n'); + const allow = path.join(tmp, 'a.json'); + await fs.writeFile(allow, JSON.stringify({ bodyPaths: ['$.createdAt'] })); + const report = path.join(tmp, 'r.md'); + const json = path.join(tmp, 'r.json'); + const result = await replayFixtures(baseUrl, fixtures, allow, report, json); + expect(result.totalFixtures).toBe(1); + expect(result.failed).toBe(0); + const reportJson = JSON.parse(await fs.readFile(json, 'utf8')); + expect(reportJson.results[0].pass).toBe(true); + }); + + it('reports diffs when bodies differ', async () => { + const fixtures = path.join(tmp, 'fd.jsonl'); + await fs.writeFile(fixtures, JSON.stringify({ + name: 'GET-div', + request: { method: 'GET', path: '/divergent/1', headers: {}, body: null }, + response: { status: 200, headers: { 'content-type': 'application/json' }, body: { id: 1, name: 'Original' } }, + capturedAt: '2026-05-17T10:00:00Z', + }) + '\n'); + const allow = path.join(tmp, 'ad.json'); + await fs.writeFile(allow, '{}'); + const report = path.join(tmp, 'rd.md'); + const json = path.join(tmp, 'rd.json'); + const result = await replayFixtures(baseUrl, fixtures, allow, report, json); + expect(result.failed).toBe(1); + const md = await fs.readFile(report, 'utf8'); + expect(md).toContain('GET-div'); + expect(md).toContain('$.name'); + }); + + it('exits non-zero indication via result.failed > 0', async () => { + const fixtures = path.join(tmp, 'fail.jsonl'); + await fs.writeFile(fixtures, JSON.stringify({ + name: 'GET-missing', + request: { method: 'GET', path: '/does-not-exist', headers: {}, body: null }, + response: { status: 200, headers: {}, body: { ok: true } }, + capturedAt: '2026-05-17T10:00:00Z', + }) + '\n'); + const allow = path.join(tmp, 'a2.json'); + await fs.writeFile(allow, '{}'); + const result = await replayFixtures(baseUrl, fixtures, allow, path.join(tmp, 'r2.md'), path.join(tmp, 'r2.json')); + expect(result.failed).toBeGreaterThan(0); + }); +}); diff --git a/skills/java-to-typescript/scripts/lib/__tests__/http.test.ts b/skills/java-to-typescript/scripts/lib/__tests__/http.test.ts new file mode 100644 index 0000000..131a57d --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/__tests__/http.test.ts @@ -0,0 +1,60 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { createServer, Server } from 'node:http'; +import { request, isLoopback } from '../http.js'; + +let server: Server; +let port: number; + +beforeAll(async () => { + server = createServer((req, res) => { + let body = ''; + req.on('data', (c) => (body += c)); + req.on('end', () => { + res.setHeader('content-type', 'application/json'); + res.statusCode = 200; + res.end(JSON.stringify({ method: req.method, url: req.url, body: body ? JSON.parse(body) : null })); + }); + }); + await new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve())); + const addr = server.address(); + if (addr && typeof addr === 'object') port = addr.port; +}); + +afterAll(() => new Promise((resolve) => server.close(() => resolve()))); + +describe('isLoopback', () => { + it('accepts localhost variants', () => { + expect(isLoopback('http://localhost:8080')).toBe(true); + expect(isLoopback('http://127.0.0.1:8080')).toBe(true); + expect(isLoopback('http://[::1]:8080')).toBe(true); + }); + it('rejects non-loopback', () => { + expect(isLoopback('http://example.com')).toBe(false); + expect(isLoopback('http://10.0.0.1')).toBe(false); + }); +}); + +describe('request', () => { + it('GETs from a loopback server', async () => { + const res = await request({ baseUrl: `http://127.0.0.1:${port}`, method: 'GET', path: '/hello' }); + expect(res.status).toBe(200); + expect(res.body).toEqual({ method: 'GET', url: '/hello', body: null }); + }); + + it('POSTs JSON body', async () => { + const res = await request({ + baseUrl: `http://127.0.0.1:${port}`, + method: 'POST', + path: '/users', + headers: { 'content-type': 'application/json' }, + body: { name: 'Ada' }, + }); + expect(res.body).toEqual({ method: 'POST', url: '/users', body: { name: 'Ada' } }); + }); + + it('rejects non-loopback baseUrl', async () => { + await expect( + request({ baseUrl: 'http://example.com', method: 'GET', path: '/' }) + ).rejects.toThrow(/loopback/); + }); +}); diff --git a/skills/java-to-typescript/scripts/lib/__tests__/json-diff.test.ts b/skills/java-to-typescript/scripts/lib/__tests__/json-diff.test.ts new file mode 100644 index 0000000..d9e0061 --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/__tests__/json-diff.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from 'vitest'; +import { diff } from '../json-diff.js'; + +describe('diff (no allowlist)', () => { + it('returns no diffs for equal objects', () => { + expect(diff({ a: 1 }, { a: 1 }, {})).toEqual([]); + }); + + it('reports a value change', () => { + expect(diff({ a: 1 }, { a: 2 }, {})).toEqual([{ path: '$.a', expected: 1, actual: 2 }]); + }); + + it('reports a missing field on actual', () => { + const d = diff({ a: 1, b: 2 }, { a: 1 }, {}); + expect(d).toEqual([{ path: '$.b', expected: 2, actual: undefined }]); + }); + + it('reports an extra field on actual', () => { + const d = diff({ a: 1 }, { a: 1, b: 2 }, {}); + expect(d).toEqual([{ path: '$.b', expected: undefined, actual: 2 }]); + }); + + it('reports a type mismatch', () => { + expect(diff({ a: 1 }, { a: '1' }, {})).toEqual([{ path: '$.a', expected: 1, actual: '1' }]); + }); + + it('recurses into nested objects', () => { + expect(diff({ a: { b: 1 } }, { a: { b: 2 } }, {})).toEqual([ + { path: '$.a.b', expected: 1, actual: 2 }, + ]); + }); + + it('order-sensitive arrays by default', () => { + const d = diff({ items: [1, 2] }, { items: [2, 1] }, {}); + expect(d.length).toBe(2); + }); +}); + +describe('diff (with allowlist)', () => { + it('ignores allowlisted body paths', () => { + const d = diff( + { createdAt: 'a', name: 'Ada' }, + { createdAt: 'b', name: 'Ada' }, + { bodyPaths: ['$.createdAt'] } + ); + expect(d).toEqual([]); + }); + + it('order-insensitive array via arrayKeys', () => { + const exp = { items: [{ id: 1, v: 'a' }, { id: 2, v: 'b' }] }; + const act = { items: [{ id: 2, v: 'b' }, { id: 1, v: 'a' }] }; + expect(diff(exp, act, { arrayKeys: { '$.items': 'id' } })).toEqual([]); + }); + + it('arrayKeys detects per-element diff', () => { + const exp = { items: [{ id: 1, v: 'a' }, { id: 2, v: 'b' }] }; + const act = { items: [{ id: 2, v: 'CHANGED' }, { id: 1, v: 'a' }] }; + const d = diff(exp, act, { arrayKeys: { '$.items': 'id' } }); + expect(d).toEqual([{ path: '$.items[id=2].v', expected: 'b', actual: 'CHANGED' }]); + }); +}); diff --git a/skills/java-to-typescript/scripts/lib/__tests__/jsonpath-lite.test.ts b/skills/java-to-typescript/scripts/lib/__tests__/jsonpath-lite.test.ts new file mode 100644 index 0000000..9a4ef00 --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/__tests__/jsonpath-lite.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect } from 'vitest'; +import { matchPaths, applyMask } from '../jsonpath-lite.js'; + +describe('matchPaths', () => { + it('matches a top-level field', () => { + expect(matchPaths({ foo: 1 }, '$.foo')).toEqual(['$.foo']); + }); + + it('matches a nested field', () => { + expect(matchPaths({ a: { b: { c: 1 } } }, '$.a.b.c')).toEqual(['$.a.b.c']); + }); + + it('matches an array index', () => { + expect(matchPaths({ items: [10, 20, 30] }, '$.items[1]')).toEqual(['$.items[1]']); + }); + + it('recursive descent finds all matches at any depth', () => { + const obj = { a: { uuid: 'x' }, b: { c: { uuid: 'y' } }, items: [{ uuid: 'z' }] }; + const matches = matchPaths(obj, '$..uuid'); + expect(matches.sort()).toEqual(['$.a.uuid', '$.b.c.uuid', '$.items[0].uuid'].sort()); + }); + + it('wildcard in recursive descent', () => { + const obj = { items: [{ id: 1, meta: { foo: 'a' } }, { id: 2, meta: { foo: 'b' } }] }; + const matches = matchPaths(obj, '$..*.foo'); + expect(matches.sort()).toEqual(['$.items[0].meta.foo', '$.items[1].meta.foo'].sort()); + }); + + it('returns empty on no match', () => { + expect(matchPaths({ foo: 1 }, '$.bar')).toEqual([]); + }); +}); + +describe('applyMask', () => { + it('removes matched leaves', () => { + const obj = { a: 1, b: 2 }; + expect(applyMask(obj, ['$.a'])).toEqual({ b: 2 }); + }); + + it('removes nested matches', () => { + const obj = { user: { name: 'Ada', uuid: 'x' } }; + expect(applyMask(obj, ['$.user.uuid'])).toEqual({ user: { name: 'Ada' } }); + }); + + it('removes recursive matches', () => { + const obj = { a: { uuid: 'x' }, b: { uuid: 'y' } }; + expect(applyMask(obj, ['$.a.uuid', '$.b.uuid'])).toEqual({ a: {}, b: {} }); + }); + + it('leaves unmatched paths intact', () => { + const obj = { a: 1, b: 2 }; + expect(applyMask(obj, ['$.c'])).toEqual({ a: 1, b: 2 }); + }); +}); diff --git a/skills/java-to-typescript/scripts/lib/fast-xml-parser/LICENSE b/skills/java-to-typescript/scripts/lib/fast-xml-parser/LICENSE new file mode 100644 index 0000000..d7da622 --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/fast-xml-parser/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Amit Kumar Gupta + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/java-to-typescript/scripts/lib/fast-xml-parser/README.md b/skills/java-to-typescript/scripts/lib/fast-xml-parser/README.md new file mode 100644 index 0000000..f87a0f3 --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/fast-xml-parser/README.md @@ -0,0 +1,78 @@ +# Vendored: fast-xml-parser + +- **Upstream:** https://github.com/NaturalIntelligence/fast-xml-parser +- **Version vendored:** 4.5.0 +- **License:** MIT (see `LICENSE` in this directory) +- **Why vendored:** the `java-to-typescript` skill must run in air-gapped + environments. Vendoring avoids a runtime `npm` registry fetch. + +## Layout + +``` +fast-xml-parser/ + LICENSE # MIT, copied verbatim from upstream + README.md # this file + index.js # ESM shim that re-exports XMLParser/XMLValidator/XMLBuilder + index.d.ts # TypeScript types re-export + src/ # CommonJS source from upstream src/, minus v5/ and cli/ + package.json # pins this subtree to "type": "commonjs" + fxp.js # upstream entry (referenced by upstream package.json `main`) + fxp.d.ts # upstream type declarations + util.js + validator.js + ignoreAttributes.js + xmlparser/ + XMLParser.js + OptionsBuilder.js + OrderedObjParser.js # patched: `require("strnum")` -> relative path + DocTypeReader.js + node2json.js + xmlNode.js + xmlbuilder/ + json2xml.js + orderedJs2Xml.js + vendor/ + package.json # pins this subtree to "type": "commonjs" + strnum/ # upstream `strnum@1.1.2` (MIT) — required by OrderedObjParser + LICENSE + strnum.js +``` + +## Consumer usage + +The outer `java-to-typescript` package is `"type": "module"`. Import the +vendored entry as ESM: + +```ts +import { XMLParser } from './lib/fast-xml-parser/index.js'; +``` + +The ESM `index.js` uses `createRequire` to load the underlying CommonJS +modules. The nested `src/package.json` and `vendor/package.json` pin +those subtrees to `"type": "commonjs"` so Node loads `.js` files there as +CJS even though the outer package is ESM. + +## Files omitted vs. upstream + +- `src/v5/**` — experimental v5 parser, not used by `XMLParser`. +- `src/cli/**` — `fxparser` CLI binary, not used by the skill. +- `CHANGELOG.md`, `README.md`, upstream `package.json` — not needed at runtime. +- Upstream `strnum` test file and README/CHANGELOG. + +## Local modifications + +- `src/xmlparser/OrderedObjParser.js`: replaced + `require("strnum")` with `require("../../vendor/strnum/strnum.js")` so + the parser resolves the vendored copy without any `node_modules` lookup. + +## Update policy + +To upgrade: + +1. Re-run the steps in plan Task 8 with the new upstream version. +2. Re-verify the layout — `package.json.main`/`typings` and the internal + `require` graph may shift between minor releases. +3. Re-apply the `strnum` path patch (or revise it if upstream changes + that import). +4. Re-run the M0 eval suite to confirm parse stability against + `evals/fixtures/spring-boot-users/java/`. diff --git a/skills/java-to-typescript/scripts/lib/fast-xml-parser/index.d.ts b/skills/java-to-typescript/scripts/lib/fast-xml-parser/index.d.ts new file mode 100644 index 0000000..20ae387 --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/fast-xml-parser/index.d.ts @@ -0,0 +1,12 @@ +// Type re-exports for the vendored fast-xml-parser 4.5.0 ESM shim. +// See ./src/fxp.d.ts for the upstream type declarations. + +export { XMLParser, XMLValidator, XMLBuilder } from './src/fxp.js'; + +declare const _default: { + XMLParser: typeof import('./src/fxp.js').XMLParser; + XMLValidator: typeof import('./src/fxp.js').XMLValidator; + XMLBuilder: typeof import('./src/fxp.js').XMLBuilder; +}; + +export default _default; diff --git a/skills/java-to-typescript/scripts/lib/fast-xml-parser/index.js b/skills/java-to-typescript/scripts/lib/fast-xml-parser/index.js new file mode 100644 index 0000000..480352d --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/fast-xml-parser/index.js @@ -0,0 +1,18 @@ +// ESM shim over the vendored CommonJS fast-xml-parser 4.5.0 entry point. +// The upstream entry (`src/fxp.js`) is CJS. The `src/package.json` in this +// vendored copy pins that subtree to `type: commonjs` so Node loads it +// correctly even though the outer `java-to-typescript` package is ESM. +// +// We use `createRequire` rather than a bare `import` so Node's static +// named-export detection does not need to introspect the CJS module shape. + +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); +const fxp = require('./src/fxp.js'); + +export const XMLParser = fxp.XMLParser; +export const XMLValidator = fxp.XMLValidator; +export const XMLBuilder = fxp.XMLBuilder; + +export default fxp; diff --git a/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/fxp.d.ts b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/fxp.d.ts new file mode 100644 index 0000000..7a48b9d --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/fxp.d.ts @@ -0,0 +1,418 @@ +type X2jOptions = { + /** + * Preserve the order of tags in resulting JS object + * + * Defaults to `false` + */ + preserveOrder?: boolean; + + /** + * Give a prefix to the attribute name in the resulting JS object + * + * Defaults to '@_' + */ + attributeNamePrefix?: string; + + /** + * A name to group all attributes of a tag under, or `false` to disable + * + * Defaults to `false` + */ + attributesGroupName?: false | string; + + /** + * The name of the next node in the resulting JS + * + * Defaults to `#text` + */ + textNodeName?: string; + + /** + * Whether to ignore attributes when parsing + * + * When `true` - ignores all the attributes + * + * When `false` - parses all the attributes + * + * When `Array` - filters out attributes that match provided patterns + * + * When `Function` - calls the function for each attribute and filters out those for which the function returned `true` + * + * Defaults to `true` + */ + ignoreAttributes?: boolean | (string | RegExp)[] | ((attrName: string, jPath: string) => boolean); + + /** + * Whether to remove namespace string from tag and attribute names + * + * Defaults to `false` + */ + removeNSPrefix?: boolean; + + /** + * Whether to allow attributes without value + * + * Defaults to `false` + */ + allowBooleanAttributes?: boolean; + + /** + * Whether to parse tag value with `strnum` package + * + * Defaults to `true` + */ + parseTagValue?: boolean; + + /** + * Whether to parse tag value with `strnum` package + * + * Defaults to `false` + */ + parseAttributeValue?: boolean; + + /** + * Whether to remove surrounding whitespace from tag or attribute value + * + * Defaults to `true` + */ + trimValues?: boolean; + + /** + * Give a property name to set CDATA values to instead of merging to tag's text value + * + * Defaults to `false` + */ + cdataPropName?: false | string; + + /** + * If set, parse comments and set as this property + * + * Defaults to `false` + */ + commentPropName?: false | string; + + /** + * Control how tag value should be parsed. Called only if tag value is not empty + * + * @returns {undefined|null} `undefined` or `null` to set original value. + * @returns {unknown} + * + * 1. Different value or value with different data type to set new value. + * 2. Same value to set parsed value if `parseTagValue: true`. + * + * Defaults to `(tagName, val, jPath, hasAttributes, isLeafNode) => val` + */ + tagValueProcessor?: (tagName: string, tagValue: string, jPath: string, hasAttributes: boolean, isLeafNode: boolean) => unknown; + + /** + * Control how attribute value should be parsed + * + * @param attrName + * @param attrValue + * @param jPath + * @returns {undefined|null} `undefined` or `null` to set original value + * @returns {unknown} + * + * Defaults to `(attrName, val, jPath) => val` + */ + attributeValueProcessor?: (attrName: string, attrValue: string, jPath: string) => unknown; + + /** + * Options to pass to `strnum` for parsing numbers + * + * Defaults to `{ hex: true, leadingZeros: true, eNotation: true }` + */ + numberParseOptions?: strnumOptions; + + /** + * Nodes to stop parsing at + * + * Defaults to `[]` + */ + stopNodes?: string[]; + + /** + * List of tags without closing tags + * + * Defaults to `[]` + */ + unpairedTags?: string[]; + + /** + * Whether to always create a text node + * + * Defaults to `false` + */ + alwaysCreateTextNode?: boolean; + + /** + * Determine whether a tag should be parsed as an array + * + * @param tagName + * @param jPath + * @param isLeafNode + * @param isAttribute + * @returns {boolean} + * + * Defaults to `() => false` + */ + isArray?: (tagName: string, jPath: string, isLeafNode: boolean, isAttribute: boolean) => boolean; + + /** + * Whether to process default and DOCTYPE entities + * + * Defaults to `true` + */ + processEntities?: boolean; + + /** + * Whether to process HTML entities + * + * Defaults to `false` + */ + htmlEntities?: boolean; + + /** + * Whether to ignore the declaration tag from output + * + * Defaults to `false` + */ + ignoreDeclaration?: boolean; + + /** + * Whether to ignore Pi tags + * + * Defaults to `false` + */ + ignorePiTags?: boolean; + + /** + * Transform tag names + * + * Defaults to `false` + */ + transformTagName?: ((tagName: string) => string) | false; + + /** + * Transform attribute names + * + * Defaults to `false` + */ + transformAttributeName?: ((attributeName: string) => string) | false; + + /** + * Change the tag name when a different name is returned. Skip the tag from parsed result when false is returned. + * Modify `attrs` object to control attributes for the given tag. + * + * @returns {string} new tag name. + * @returns false to skip the tag + * + * Defaults to `(tagName, jPath, attrs) => tagName` + */ + updateTag?: (tagName: string, jPath: string, attrs: {[k: string]: string}) => string | boolean; +}; + +type strnumOptions = { + hex: boolean; + leadingZeros: boolean, + skipLike?: RegExp, + eNotation?: boolean +} + +type validationOptions = { + /** + * Whether to allow attributes without value + * + * Defaults to `false` + */ + allowBooleanAttributes?: boolean; + + /** + * List of tags without closing tags + * + * Defaults to `[]` + */ + unpairedTags?: string[]; +}; + +type XmlBuilderOptions = { + /** + * Give a prefix to the attribute name in the resulting JS object + * + * Defaults to '@_' + */ + attributeNamePrefix?: string; + + /** + * A name to group all attributes of a tag under, or `false` to disable + * + * Defaults to `false` + */ + attributesGroupName?: false | string; + + /** + * The name of the next node in the resulting JS + * + * Defaults to `#text` + */ + textNodeName?: string; + + /** + * Whether to ignore attributes when building + * + * When `true` - ignores all the attributes + * + * When `false` - builds all the attributes + * + * When `Array` - filters out attributes that match provided patterns + * + * When `Function` - calls the function for each attribute and filters out those for which the function returned `true` + * + * Defaults to `true` + */ + ignoreAttributes?: boolean | (string | RegExp)[] | ((attrName: string, jPath: string) => boolean); + + /** + * Give a property name to set CDATA values to instead of merging to tag's text value + * + * Defaults to `false` + */ + cdataPropName?: false | string; + + /** + * If set, parse comments and set as this property + * + * Defaults to `false` + */ + commentPropName?: false | string; + + /** + * Whether to make output pretty instead of single line + * + * Defaults to `false` + */ + format?: boolean; + + + /** + * If `format` is set to `true`, sets the indent string + * + * Defaults to ` ` + */ + indentBy?: string; + + /** + * Give a name to a top-level array + * + * Defaults to `undefined` + */ + arrayNodeName?: string; + + /** + * Create empty tags for tags with no text value + * + * Defaults to `false` + */ + suppressEmptyNode?: boolean; + + /** + * Suppress an unpaired tag + * + * Defaults to `true` + */ + suppressUnpairedNode?: boolean; + + /** + * Don't put a value for boolean attributes + * + * Defaults to `true` + */ + suppressBooleanAttributes?: boolean; + + /** + * Preserve the order of tags in resulting JS object + * + * Defaults to `false` + */ + preserveOrder?: boolean; + + /** + * List of tags without closing tags + * + * Defaults to `[]` + */ + unpairedTags?: string[]; + + /** + * Nodes to stop parsing at + * + * Defaults to `[]` + */ + stopNodes?: string[]; + + /** + * Control how tag value should be parsed. Called only if tag value is not empty + * + * @returns {undefined|null} `undefined` or `null` to set original value. + * @returns {unknown} + * + * 1. Different value or value with different data type to set new value. + * 2. Same value to set parsed value if `parseTagValue: true`. + * + * Defaults to `(tagName, val, jPath, hasAttributes, isLeafNode) => val` + */ + tagValueProcessor?: (name: string, value: unknown) => unknown; + + /** + * Control how attribute value should be parsed + * + * @param attrName + * @param attrValue + * @param jPath + * @returns {undefined|null} `undefined` or `null` to set original value + * @returns {unknown} + * + * Defaults to `(attrName, val, jPath) => val` + */ + attributeValueProcessor?: (name: string, value: unknown) => unknown; + + /** + * Whether to process default and DOCTYPE entities + * + * Defaults to `true` + */ + processEntities?: boolean; + + + oneListGroup?: boolean; +}; + +type ESchema = string | object | Array; + +type ValidationError = { + err: { + code: string; + msg: string, + line: number, + col: number + }; +}; + +export class XMLParser { + constructor(options?: X2jOptions); + parse(xmlData: string | Buffer ,validationOptions?: validationOptions | boolean): any; + /** + * Add Entity which is not by default supported by this library + * @param entityIdentifier {string} Eg: 'ent' for &ent; + * @param entityValue {string} Eg: '\r' + */ + addEntity(entityIdentifier: string, entityValue: string): void; +} + +export class XMLValidator{ + static validate( xmlData: string, options?: validationOptions): true | ValidationError; +} +export class XMLBuilder { + constructor(options?: XmlBuilderOptions); + build(jObj: any): any; +} diff --git a/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/fxp.js b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/fxp.js new file mode 100644 index 0000000..9cfa0ac --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/fxp.js @@ -0,0 +1,11 @@ +'use strict'; + +const validator = require('./validator'); +const XMLParser = require('./xmlparser/XMLParser'); +const XMLBuilder = require('./xmlbuilder/json2xml'); + +module.exports = { + XMLParser: XMLParser, + XMLValidator: validator, + XMLBuilder: XMLBuilder +} \ No newline at end of file diff --git a/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/ignoreAttributes.js b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/ignoreAttributes.js new file mode 100644 index 0000000..9fb346b --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/ignoreAttributes.js @@ -0,0 +1,20 @@ +function getIgnoreAttributesFn(ignoreAttributes) { + if (typeof ignoreAttributes === 'function') { + return ignoreAttributes + } + if (Array.isArray(ignoreAttributes)) { + return (attrName) => { + for (const pattern of ignoreAttributes) { + if (typeof pattern === 'string' && attrName === pattern) { + return true + } + if (pattern instanceof RegExp && pattern.test(attrName)) { + return true + } + } + } + } + return () => false +} + +module.exports = getIgnoreAttributesFn \ No newline at end of file diff --git a/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/package.json b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/package.json new file mode 100644 index 0000000..5bbefff --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/util.js b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/util.js new file mode 100644 index 0000000..df0a60d --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/util.js @@ -0,0 +1,72 @@ +'use strict'; + +const nameStartChar = ':A-Za-z_\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD'; +const nameChar = nameStartChar + '\\-.\\d\\u00B7\\u0300-\\u036F\\u203F-\\u2040'; +const nameRegexp = '[' + nameStartChar + '][' + nameChar + ']*' +const regexName = new RegExp('^' + nameRegexp + '$'); + +const getAllMatches = function(string, regex) { + const matches = []; + let match = regex.exec(string); + while (match) { + const allmatches = []; + allmatches.startIndex = regex.lastIndex - match[0].length; + const len = match.length; + for (let index = 0; index < len; index++) { + allmatches.push(match[index]); + } + matches.push(allmatches); + match = regex.exec(string); + } + return matches; +}; + +const isName = function(string) { + const match = regexName.exec(string); + return !(match === null || typeof match === 'undefined'); +}; + +exports.isExist = function(v) { + return typeof v !== 'undefined'; +}; + +exports.isEmptyObject = function(obj) { + return Object.keys(obj).length === 0; +}; + +/** + * Copy all the properties of a into b. + * @param {*} target + * @param {*} a + */ +exports.merge = function(target, a, arrayMode) { + if (a) { + const keys = Object.keys(a); // will return an array of own properties + const len = keys.length; //don't make it inline + for (let i = 0; i < len; i++) { + if (arrayMode === 'strict') { + target[keys[i]] = [ a[keys[i]] ]; + } else { + target[keys[i]] = a[keys[i]]; + } + } + } +}; +/* exports.merge =function (b,a){ + return Object.assign(b,a); +} */ + +exports.getValue = function(v) { + if (exports.isExist(v)) { + return v; + } else { + return ''; + } +}; + +// const fakeCall = function(a) {return a;}; +// const fakeCallNoReturn = function() {}; + +exports.isName = isName; +exports.getAllMatches = getAllMatches; +exports.nameRegexp = nameRegexp; diff --git a/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/validator.js b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/validator.js new file mode 100644 index 0000000..3b1b2ef --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/validator.js @@ -0,0 +1,425 @@ +'use strict'; + +const util = require('./util'); + +const defaultOptions = { + allowBooleanAttributes: false, //A tag can have attributes without any value + unpairedTags: [] +}; + +//const tagsPattern = new RegExp("<\\/?([\\w:\\-_\.]+)\\s*\/?>","g"); +exports.validate = function (xmlData, options) { + options = Object.assign({}, defaultOptions, options); + + //xmlData = xmlData.replace(/(\r\n|\n|\r)/gm,"");//make it single line + //xmlData = xmlData.replace(/(^\s*<\?xml.*?\?>)/g,"");//Remove XML starting tag + //xmlData = xmlData.replace(/()/g,"");//Remove DOCTYPE + const tags = []; + let tagFound = false; + + //indicates that the root tag has been closed (aka. depth 0 has been reached) + let reachedRoot = false; + + if (xmlData[0] === '\ufeff') { + // check for byte order mark (BOM) + xmlData = xmlData.substr(1); + } + + for (let i = 0; i < xmlData.length; i++) { + + if (xmlData[i] === '<' && xmlData[i+1] === '?') { + i+=2; + i = readPI(xmlData,i); + if (i.err) return i; + }else if (xmlData[i] === '<') { + //starting of tag + //read until you reach to '>' avoiding any '>' in attribute value + let tagStartPos = i; + i++; + + if (xmlData[i] === '!') { + i = readCommentAndCDATA(xmlData, i); + continue; + } else { + let closingTag = false; + if (xmlData[i] === '/') { + //closing tag + closingTag = true; + i++; + } + //read tagname + let tagName = ''; + for (; i < xmlData.length && + xmlData[i] !== '>' && + xmlData[i] !== ' ' && + xmlData[i] !== '\t' && + xmlData[i] !== '\n' && + xmlData[i] !== '\r'; i++ + ) { + tagName += xmlData[i]; + } + tagName = tagName.trim(); + //console.log(tagName); + + if (tagName[tagName.length - 1] === '/') { + //self closing tag without attributes + tagName = tagName.substring(0, tagName.length - 1); + //continue; + i--; + } + if (!validateTagName(tagName)) { + let msg; + if (tagName.trim().length === 0) { + msg = "Invalid space after '<'."; + } else { + msg = "Tag '"+tagName+"' is an invalid name."; + } + return getErrorObject('InvalidTag', msg, getLineNumberForPosition(xmlData, i)); + } + + const result = readAttributeStr(xmlData, i); + if (result === false) { + return getErrorObject('InvalidAttr', "Attributes for '"+tagName+"' have open quote.", getLineNumberForPosition(xmlData, i)); + } + let attrStr = result.value; + i = result.index; + + if (attrStr[attrStr.length - 1] === '/') { + //self closing tag + const attrStrStart = i - attrStr.length; + attrStr = attrStr.substring(0, attrStr.length - 1); + const isValid = validateAttributeString(attrStr, options); + if (isValid === true) { + tagFound = true; + //continue; //text may presents after self closing tag + } else { + //the result from the nested function returns the position of the error within the attribute + //in order to get the 'true' error line, we need to calculate the position where the attribute begins (i - attrStr.length) and then add the position within the attribute + //this gives us the absolute index in the entire xml, which we can use to find the line at last + return getErrorObject(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, attrStrStart + isValid.err.line)); + } + } else if (closingTag) { + if (!result.tagClosed) { + return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' doesn't have proper closing.", getLineNumberForPosition(xmlData, i)); + } else if (attrStr.trim().length > 0) { + return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' can't have attributes or invalid starting.", getLineNumberForPosition(xmlData, tagStartPos)); + } else if (tags.length === 0) { + return getErrorObject('InvalidTag', "Closing tag '"+tagName+"' has not been opened.", getLineNumberForPosition(xmlData, tagStartPos)); + } else { + const otg = tags.pop(); + if (tagName !== otg.tagName) { + let openPos = getLineNumberForPosition(xmlData, otg.tagStartPos); + return getErrorObject('InvalidTag', + "Expected closing tag '"+otg.tagName+"' (opened in line "+openPos.line+", col "+openPos.col+") instead of closing tag '"+tagName+"'.", + getLineNumberForPosition(xmlData, tagStartPos)); + } + + //when there are no more tags, we reached the root level. + if (tags.length == 0) { + reachedRoot = true; + } + } + } else { + const isValid = validateAttributeString(attrStr, options); + if (isValid !== true) { + //the result from the nested function returns the position of the error within the attribute + //in order to get the 'true' error line, we need to calculate the position where the attribute begins (i - attrStr.length) and then add the position within the attribute + //this gives us the absolute index in the entire xml, which we can use to find the line at last + return getErrorObject(isValid.err.code, isValid.err.msg, getLineNumberForPosition(xmlData, i - attrStr.length + isValid.err.line)); + } + + //if the root level has been reached before ... + if (reachedRoot === true) { + return getErrorObject('InvalidXml', 'Multiple possible root nodes found.', getLineNumberForPosition(xmlData, i)); + } else if(options.unpairedTags.indexOf(tagName) !== -1){ + //don't push into stack + } else { + tags.push({tagName, tagStartPos}); + } + tagFound = true; + } + + //skip tag text value + //It may include comments and CDATA value + for (i++; i < xmlData.length; i++) { + if (xmlData[i] === '<') { + if (xmlData[i + 1] === '!') { + //comment or CADATA + i++; + i = readCommentAndCDATA(xmlData, i); + continue; + } else if (xmlData[i+1] === '?') { + i = readPI(xmlData, ++i); + if (i.err) return i; + } else{ + break; + } + } else if (xmlData[i] === '&') { + const afterAmp = validateAmpersand(xmlData, i); + if (afterAmp == -1) + return getErrorObject('InvalidChar', "char '&' is not expected.", getLineNumberForPosition(xmlData, i)); + i = afterAmp; + }else{ + if (reachedRoot === true && !isWhiteSpace(xmlData[i])) { + return getErrorObject('InvalidXml', "Extra text at the end", getLineNumberForPosition(xmlData, i)); + } + } + } //end of reading tag text value + if (xmlData[i] === '<') { + i--; + } + } + } else { + if ( isWhiteSpace(xmlData[i])) { + continue; + } + return getErrorObject('InvalidChar', "char '"+xmlData[i]+"' is not expected.", getLineNumberForPosition(xmlData, i)); + } + } + + if (!tagFound) { + return getErrorObject('InvalidXml', 'Start tag expected.', 1); + }else if (tags.length == 1) { + return getErrorObject('InvalidTag', "Unclosed tag '"+tags[0].tagName+"'.", getLineNumberForPosition(xmlData, tags[0].tagStartPos)); + }else if (tags.length > 0) { + return getErrorObject('InvalidXml', "Invalid '"+ + JSON.stringify(tags.map(t => t.tagName), null, 4).replace(/\r?\n/g, '')+ + "' found.", {line: 1, col: 1}); + } + + return true; +}; + +function isWhiteSpace(char){ + return char === ' ' || char === '\t' || char === '\n' || char === '\r'; +} +/** + * Read Processing insstructions and skip + * @param {*} xmlData + * @param {*} i + */ +function readPI(xmlData, i) { + const start = i; + for (; i < xmlData.length; i++) { + if (xmlData[i] == '?' || xmlData[i] == ' ') { + //tagname + const tagname = xmlData.substr(start, i - start); + if (i > 5 && tagname === 'xml') { + return getErrorObject('InvalidXml', 'XML declaration allowed only at the start of the document.', getLineNumberForPosition(xmlData, i)); + } else if (xmlData[i] == '?' && xmlData[i + 1] == '>') { + //check if valid attribut string + i++; + break; + } else { + continue; + } + } + } + return i; +} + +function readCommentAndCDATA(xmlData, i) { + if (xmlData.length > i + 5 && xmlData[i + 1] === '-' && xmlData[i + 2] === '-') { + //comment + for (i += 3; i < xmlData.length; i++) { + if (xmlData[i] === '-' && xmlData[i + 1] === '-' && xmlData[i + 2] === '>') { + i += 2; + break; + } + } + } else if ( + xmlData.length > i + 8 && + xmlData[i + 1] === 'D' && + xmlData[i + 2] === 'O' && + xmlData[i + 3] === 'C' && + xmlData[i + 4] === 'T' && + xmlData[i + 5] === 'Y' && + xmlData[i + 6] === 'P' && + xmlData[i + 7] === 'E' + ) { + let angleBracketsCount = 1; + for (i += 8; i < xmlData.length; i++) { + if (xmlData[i] === '<') { + angleBracketsCount++; + } else if (xmlData[i] === '>') { + angleBracketsCount--; + if (angleBracketsCount === 0) { + break; + } + } + } + } else if ( + xmlData.length > i + 9 && + xmlData[i + 1] === '[' && + xmlData[i + 2] === 'C' && + xmlData[i + 3] === 'D' && + xmlData[i + 4] === 'A' && + xmlData[i + 5] === 'T' && + xmlData[i + 6] === 'A' && + xmlData[i + 7] === '[' + ) { + for (i += 8; i < xmlData.length; i++) { + if (xmlData[i] === ']' && xmlData[i + 1] === ']' && xmlData[i + 2] === '>') { + i += 2; + break; + } + } + } + + return i; +} + +const doubleQuote = '"'; +const singleQuote = "'"; + +/** + * Keep reading xmlData until '<' is found outside the attribute value. + * @param {string} xmlData + * @param {number} i + */ +function readAttributeStr(xmlData, i) { + let attrStr = ''; + let startChar = ''; + let tagClosed = false; + for (; i < xmlData.length; i++) { + if (xmlData[i] === doubleQuote || xmlData[i] === singleQuote) { + if (startChar === '') { + startChar = xmlData[i]; + } else if (startChar !== xmlData[i]) { + //if vaue is enclosed with double quote then single quotes are allowed inside the value and vice versa + } else { + startChar = ''; + } + } else if (xmlData[i] === '>') { + if (startChar === '') { + tagClosed = true; + break; + } + } + attrStr += xmlData[i]; + } + if (startChar !== '') { + return false; + } + + return { + value: attrStr, + index: i, + tagClosed: tagClosed + }; +} + +/** + * Select all the attributes whether valid or invalid. + */ +const validAttrStrRegxp = new RegExp('(\\s*)([^\\s=]+)(\\s*=)?(\\s*([\'"])(([\\s\\S])*?)\\5)?', 'g'); + +//attr, ="sd", a="amit's", a="sd"b="saf", ab cd="" + +function validateAttributeString(attrStr, options) { + //console.log("start:"+attrStr+":end"); + + //if(attrStr.trim().length === 0) return true; //empty string + + const matches = util.getAllMatches(attrStr, validAttrStrRegxp); + const attrNames = {}; + + for (let i = 0; i < matches.length; i++) { + if (matches[i][1].length === 0) { + //nospace before attribute name: a="sd"b="saf" + return getErrorObject('InvalidAttr', "Attribute '"+matches[i][2]+"' has no space in starting.", getPositionFromMatch(matches[i])) + } else if (matches[i][3] !== undefined && matches[i][4] === undefined) { + return getErrorObject('InvalidAttr', "Attribute '"+matches[i][2]+"' is without value.", getPositionFromMatch(matches[i])); + } else if (matches[i][3] === undefined && !options.allowBooleanAttributes) { + //independent attribute: ab + return getErrorObject('InvalidAttr', "boolean attribute '"+matches[i][2]+"' is not allowed.", getPositionFromMatch(matches[i])); + } + /* else if(matches[i][6] === undefined){//attribute without value: ab= + return { err: { code:"InvalidAttr",msg:"attribute " + matches[i][2] + " has no value assigned."}}; + } */ + const attrName = matches[i][2]; + if (!validateAttrName(attrName)) { + return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is an invalid name.", getPositionFromMatch(matches[i])); + } + if (!attrNames.hasOwnProperty(attrName)) { + //check for duplicate attribute. + attrNames[attrName] = 1; + } else { + return getErrorObject('InvalidAttr', "Attribute '"+attrName+"' is repeated.", getPositionFromMatch(matches[i])); + } + } + + return true; +} + +function validateNumberAmpersand(xmlData, i) { + let re = /\d/; + if (xmlData[i] === 'x') { + i++; + re = /[\da-fA-F]/; + } + for (; i < xmlData.length; i++) { + if (xmlData[i] === ';') + return i; + if (!xmlData[i].match(re)) + break; + } + return -1; +} + +function validateAmpersand(xmlData, i) { + // https://www.w3.org/TR/xml/#dt-charref + i++; + if (xmlData[i] === ';') + return -1; + if (xmlData[i] === '#') { + i++; + return validateNumberAmpersand(xmlData, i); + } + let count = 0; + for (; i < xmlData.length; i++, count++) { + if (xmlData[i].match(/\w/) && count < 20) + continue; + if (xmlData[i] === ';') + break; + return -1; + } + return i; +} + +function getErrorObject(code, message, lineNumber) { + return { + err: { + code: code, + msg: message, + line: lineNumber.line || lineNumber, + col: lineNumber.col, + }, + }; +} + +function validateAttrName(attrName) { + return util.isName(attrName); +} + +// const startsWithXML = /^xml/i; + +function validateTagName(tagname) { + return util.isName(tagname) /* && !tagname.match(startsWithXML) */; +} + +//this function returns the line number for the character at the given index +function getLineNumberForPosition(xmlData, index) { + const lines = xmlData.substring(0, index).split(/\r?\n/); + return { + line: lines.length, + + // column number is last line's length + 1, because column numbering starts at 1: + col: lines[lines.length - 1].length + 1 + }; +} + +//this function returns the position of the first character of match within attrStr +function getPositionFromMatch(match) { + return match.startIndex + match[1].length; +} diff --git a/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlbuilder/json2xml.js b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlbuilder/json2xml.js new file mode 100644 index 0000000..d35c692 --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlbuilder/json2xml.js @@ -0,0 +1,284 @@ +'use strict'; +//parse Empty Node as self closing node +const buildFromOrderedJs = require('./orderedJs2Xml'); +const getIgnoreAttributesFn = require('../ignoreAttributes') + +const defaultOptions = { + attributeNamePrefix: '@_', + attributesGroupName: false, + textNodeName: '#text', + ignoreAttributes: true, + cdataPropName: false, + format: false, + indentBy: ' ', + suppressEmptyNode: false, + suppressUnpairedNode: true, + suppressBooleanAttributes: true, + tagValueProcessor: function(key, a) { + return a; + }, + attributeValueProcessor: function(attrName, a) { + return a; + }, + preserveOrder: false, + commentPropName: false, + unpairedTags: [], + entities: [ + { regex: new RegExp("&", "g"), val: "&" },//it must be on top + { regex: new RegExp(">", "g"), val: ">" }, + { regex: new RegExp("<", "g"), val: "<" }, + { regex: new RegExp("\'", "g"), val: "'" }, + { regex: new RegExp("\"", "g"), val: """ } + ], + processEntities: true, + stopNodes: [], + // transformTagName: false, + // transformAttributeName: false, + oneListGroup: false +}; + +function Builder(options) { + this.options = Object.assign({}, defaultOptions, options); + if (this.options.ignoreAttributes === true || this.options.attributesGroupName) { + this.isAttribute = function(/*a*/) { + return false; + }; + } else { + this.ignoreAttributesFn = getIgnoreAttributesFn(this.options.ignoreAttributes) + this.attrPrefixLen = this.options.attributeNamePrefix.length; + this.isAttribute = isAttribute; + } + + this.processTextOrObjNode = processTextOrObjNode + + if (this.options.format) { + this.indentate = indentate; + this.tagEndChar = '>\n'; + this.newLine = '\n'; + } else { + this.indentate = function() { + return ''; + }; + this.tagEndChar = '>'; + this.newLine = ''; + } +} + +Builder.prototype.build = function(jObj) { + if(this.options.preserveOrder){ + return buildFromOrderedJs(jObj, this.options); + }else { + if(Array.isArray(jObj) && this.options.arrayNodeName && this.options.arrayNodeName.length > 1){ + jObj = { + [this.options.arrayNodeName] : jObj + } + } + return this.j2x(jObj, 0, []).val; + } +}; + +Builder.prototype.j2x = function(jObj, level, ajPath) { + let attrStr = ''; + let val = ''; + const jPath = ajPath.join('.') + for (let key in jObj) { + if(!Object.prototype.hasOwnProperty.call(jObj, key)) continue; + if (typeof jObj[key] === 'undefined') { + // supress undefined node only if it is not an attribute + if (this.isAttribute(key)) { + val += ''; + } + } else if (jObj[key] === null) { + // null attribute should be ignored by the attribute list, but should not cause the tag closing + if (this.isAttribute(key)) { + val += ''; + } else if (key[0] === '?') { + val += this.indentate(level) + '<' + key + '?' + this.tagEndChar; + } else { + val += this.indentate(level) + '<' + key + '/' + this.tagEndChar; + } + // val += this.indentate(level) + '<' + key + '/' + this.tagEndChar; + } else if (jObj[key] instanceof Date) { + val += this.buildTextValNode(jObj[key], key, '', level); + } else if (typeof jObj[key] !== 'object') { + //premitive type + const attr = this.isAttribute(key); + if (attr && !this.ignoreAttributesFn(attr, jPath)) { + attrStr += this.buildAttrPairStr(attr, '' + jObj[key]); + } else if (!attr) { + //tag value + if (key === this.options.textNodeName) { + let newval = this.options.tagValueProcessor(key, '' + jObj[key]); + val += this.replaceEntitiesValue(newval); + } else { + val += this.buildTextValNode(jObj[key], key, '', level); + } + } + } else if (Array.isArray(jObj[key])) { + //repeated nodes + const arrLen = jObj[key].length; + let listTagVal = ""; + let listTagAttr = ""; + for (let j = 0; j < arrLen; j++) { + const item = jObj[key][j]; + if (typeof item === 'undefined') { + // supress undefined node + } else if (item === null) { + if(key[0] === "?") val += this.indentate(level) + '<' + key + '?' + this.tagEndChar; + else val += this.indentate(level) + '<' + key + '/' + this.tagEndChar; + // val += this.indentate(level) + '<' + key + '/' + this.tagEndChar; + } else if (typeof item === 'object') { + if(this.options.oneListGroup){ + const result = this.j2x(item, level + 1, ajPath.concat(key)); + listTagVal += result.val; + if (this.options.attributesGroupName && item.hasOwnProperty(this.options.attributesGroupName)) { + listTagAttr += result.attrStr + } + }else{ + listTagVal += this.processTextOrObjNode(item, key, level, ajPath) + } + } else { + if (this.options.oneListGroup) { + let textValue = this.options.tagValueProcessor(key, item); + textValue = this.replaceEntitiesValue(textValue); + listTagVal += textValue; + } else { + listTagVal += this.buildTextValNode(item, key, '', level); + } + } + } + if(this.options.oneListGroup){ + listTagVal = this.buildObjectNode(listTagVal, key, listTagAttr, level); + } + val += listTagVal; + } else { + //nested node + if (this.options.attributesGroupName && key === this.options.attributesGroupName) { + const Ks = Object.keys(jObj[key]); + const L = Ks.length; + for (let j = 0; j < L; j++) { + attrStr += this.buildAttrPairStr(Ks[j], '' + jObj[key][Ks[j]]); + } + } else { + val += this.processTextOrObjNode(jObj[key], key, level, ajPath) + } + } + } + return {attrStr: attrStr, val: val}; +}; + +Builder.prototype.buildAttrPairStr = function(attrName, val){ + val = this.options.attributeValueProcessor(attrName, '' + val); + val = this.replaceEntitiesValue(val); + if (this.options.suppressBooleanAttributes && val === "true") { + return ' ' + attrName; + } else return ' ' + attrName + '="' + val + '"'; +} + +function processTextOrObjNode (object, key, level, ajPath) { + const result = this.j2x(object, level + 1, ajPath.concat(key)); + if (object[this.options.textNodeName] !== undefined && Object.keys(object).length === 1) { + return this.buildTextValNode(object[this.options.textNodeName], key, result.attrStr, level); + } else { + return this.buildObjectNode(result.val, key, result.attrStr, level); + } +} + +Builder.prototype.buildObjectNode = function(val, key, attrStr, level) { + if(val === ""){ + if(key[0] === "?") return this.indentate(level) + '<' + key + attrStr+ '?' + this.tagEndChar; + else { + return this.indentate(level) + '<' + key + attrStr + this.closeTag(key) + this.tagEndChar; + } + }else{ + + let tagEndExp = '' + val + tagEndExp ); + } else if (this.options.commentPropName !== false && key === this.options.commentPropName && piClosingChar.length === 0) { + return this.indentate(level) + `` + this.newLine; + }else { + return ( + this.indentate(level) + '<' + key + attrStr + piClosingChar + this.tagEndChar + + val + + this.indentate(level) + tagEndExp ); + } + } +} + +Builder.prototype.closeTag = function(key){ + let closeTag = ""; + if(this.options.unpairedTags.indexOf(key) !== -1){ //unpaired + if(!this.options.suppressUnpairedNode) closeTag = "/" + }else if(this.options.suppressEmptyNode){ //empty + closeTag = "/"; + }else{ + closeTag = `>` + this.newLine; + }else if (this.options.commentPropName !== false && key === this.options.commentPropName) { + return this.indentate(level) + `` + this.newLine; + }else if(key[0] === "?") {//PI tag + return this.indentate(level) + '<' + key + attrStr+ '?' + this.tagEndChar; + }else{ + let textValue = this.options.tagValueProcessor(key, val); + textValue = this.replaceEntitiesValue(textValue); + + if( textValue === ''){ + return this.indentate(level) + '<' + key + attrStr + this.closeTag(key) + this.tagEndChar; + }else{ + return this.indentate(level) + '<' + key + attrStr + '>' + + textValue + + ' 0 && this.options.processEntities){ + for (let i=0; i 0) { + indentation = EOL; + } + return arrToStr(jArray, options, "", indentation); +} + +function arrToStr(arr, options, jPath, indentation) { + let xmlStr = ""; + let isPreviousElementTag = false; + + for (let i = 0; i < arr.length; i++) { + const tagObj = arr[i]; + const tagName = propName(tagObj); + if(tagName === undefined) continue; + + let newJPath = ""; + if (jPath.length === 0) newJPath = tagName + else newJPath = `${jPath}.${tagName}`; + + if (tagName === options.textNodeName) { + let tagText = tagObj[tagName]; + if (!isStopNode(newJPath, options)) { + tagText = options.tagValueProcessor(tagName, tagText); + tagText = replaceEntitiesValue(tagText, options); + } + if (isPreviousElementTag) { + xmlStr += indentation; + } + xmlStr += tagText; + isPreviousElementTag = false; + continue; + } else if (tagName === options.cdataPropName) { + if (isPreviousElementTag) { + xmlStr += indentation; + } + xmlStr += ``; + isPreviousElementTag = false; + continue; + } else if (tagName === options.commentPropName) { + xmlStr += indentation + ``; + isPreviousElementTag = true; + continue; + } else if (tagName[0] === "?") { + const attStr = attr_to_str(tagObj[":@"], options); + const tempInd = tagName === "?xml" ? "" : indentation; + let piTextNodeName = tagObj[tagName][0][options.textNodeName]; + piTextNodeName = piTextNodeName.length !== 0 ? " " + piTextNodeName : ""; //remove extra spacing + xmlStr += tempInd + `<${tagName}${piTextNodeName}${attStr}?>`; + isPreviousElementTag = true; + continue; + } + let newIdentation = indentation; + if (newIdentation !== "") { + newIdentation += options.indentBy; + } + const attStr = attr_to_str(tagObj[":@"], options); + const tagStart = indentation + `<${tagName}${attStr}`; + const tagValue = arrToStr(tagObj[tagName], options, newJPath, newIdentation); + if (options.unpairedTags.indexOf(tagName) !== -1) { + if (options.suppressUnpairedNode) xmlStr += tagStart + ">"; + else xmlStr += tagStart + "/>"; + } else if ((!tagValue || tagValue.length === 0) && options.suppressEmptyNode) { + xmlStr += tagStart + "/>"; + } else if (tagValue && tagValue.endsWith(">")) { + xmlStr += tagStart + `>${tagValue}${indentation}`; + } else { + xmlStr += tagStart + ">"; + if (tagValue && indentation !== "" && (tagValue.includes("/>") || tagValue.includes("`; + } + isPreviousElementTag = true; + } + + return xmlStr; +} + +function propName(obj) { + const keys = Object.keys(obj); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if(!obj.hasOwnProperty(key)) continue; + if (key !== ":@") return key; + } +} + +function attr_to_str(attrMap, options) { + let attrStr = ""; + if (attrMap && !options.ignoreAttributes) { + for (let attr in attrMap) { + if(!attrMap.hasOwnProperty(attr)) continue; + let attrVal = options.attributeValueProcessor(attr, attrMap[attr]); + attrVal = replaceEntitiesValue(attrVal, options); + if (attrVal === true && options.suppressBooleanAttributes) { + attrStr += ` ${attr.substr(options.attributeNamePrefix.length)}`; + } else { + attrStr += ` ${attr.substr(options.attributeNamePrefix.length)}="${attrVal}"`; + } + } + } + return attrStr; +} + +function isStopNode(jPath, options) { + jPath = jPath.substr(0, jPath.length - options.textNodeName.length - 1); + let tagName = jPath.substr(jPath.lastIndexOf(".") + 1); + for (let index in options.stopNodes) { + if (options.stopNodes[index] === jPath || options.stopNodes[index] === "*." + tagName) return true; + } + return false; +} + +function replaceEntitiesValue(textValue, options) { + if (textValue && textValue.length > 0 && options.processEntities) { + for (let i = 0; i < options.entities.length; i++) { + const entity = options.entities[i]; + textValue = textValue.replace(entity.regex, entity.val); + } + } + return textValue; +} +module.exports = toXml; diff --git a/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlparser/DocTypeReader.js b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlparser/DocTypeReader.js new file mode 100644 index 0000000..bcf9dee --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlparser/DocTypeReader.js @@ -0,0 +1,152 @@ +const util = require('../util'); + +//TODO: handle comments +function readDocType(xmlData, i){ + + const entities = {}; + if( xmlData[i + 3] === 'O' && + xmlData[i + 4] === 'C' && + xmlData[i + 5] === 'T' && + xmlData[i + 6] === 'Y' && + xmlData[i + 7] === 'P' && + xmlData[i + 8] === 'E') + { + i = i+9; + let angleBracketsCount = 1; + let hasBody = false, comment = false; + let exp = ""; + for(;i') { //Read tag content + if(comment){ + if( xmlData[i - 1] === "-" && xmlData[i - 2] === "-"){ + comment = false; + angleBracketsCount--; + } + }else{ + angleBracketsCount--; + } + if (angleBracketsCount === 0) { + break; + } + }else if( xmlData[i] === '['){ + hasBody = true; + }else{ + exp += xmlData[i]; + } + } + if(angleBracketsCount !== 0){ + throw new Error(`Unclosed DOCTYPE`); + } + }else{ + throw new Error(`Invalid Tag instead of DOCTYPE`); + } + return {entities, i}; +} + +function readEntityExp(xmlData,i){ + //External entities are not supported + // + + //Parameter entities are not supported + // + + //Internal entities are supported + // + + //read EntityName + let entityName = ""; + for (; i < xmlData.length && (xmlData[i] !== "'" && xmlData[i] !== '"' ); i++) { + // if(xmlData[i] === " ") continue; + // else + entityName += xmlData[i]; + } + entityName = entityName.trim(); + if(entityName.indexOf(" ") !== -1) throw new Error("External entites are not supported"); + + //read Entity Value + const startChar = xmlData[i++]; + let val = "" + for (; i < xmlData.length && xmlData[i] !== startChar ; i++) { + val += xmlData[i]; + } + return [entityName, val, i]; +} + +function isComment(xmlData, i){ + if(xmlData[i+1] === '!' && + xmlData[i+2] === '-' && + xmlData[i+3] === '-') return true + return false +} +function isEntity(xmlData, i){ + if(xmlData[i+1] === '!' && + xmlData[i+2] === 'E' && + xmlData[i+3] === 'N' && + xmlData[i+4] === 'T' && + xmlData[i+5] === 'I' && + xmlData[i+6] === 'T' && + xmlData[i+7] === 'Y') return true + return false +} +function isElement(xmlData, i){ + if(xmlData[i+1] === '!' && + xmlData[i+2] === 'E' && + xmlData[i+3] === 'L' && + xmlData[i+4] === 'E' && + xmlData[i+5] === 'M' && + xmlData[i+6] === 'E' && + xmlData[i+7] === 'N' && + xmlData[i+8] === 'T') return true + return false +} + +function isAttlist(xmlData, i){ + if(xmlData[i+1] === '!' && + xmlData[i+2] === 'A' && + xmlData[i+3] === 'T' && + xmlData[i+4] === 'T' && + xmlData[i+5] === 'L' && + xmlData[i+6] === 'I' && + xmlData[i+7] === 'S' && + xmlData[i+8] === 'T') return true + return false +} +function isNotation(xmlData, i){ + if(xmlData[i+1] === '!' && + xmlData[i+2] === 'N' && + xmlData[i+3] === 'O' && + xmlData[i+4] === 'T' && + xmlData[i+5] === 'A' && + xmlData[i+6] === 'T' && + xmlData[i+7] === 'I' && + xmlData[i+8] === 'O' && + xmlData[i+9] === 'N') return true + return false +} + +function validateEntityName(name){ + if (util.isName(name)) + return name; + else + throw new Error(`Invalid entity name ${name}`); +} + +module.exports = readDocType; diff --git a/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlparser/OptionsBuilder.js b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlparser/OptionsBuilder.js new file mode 100644 index 0000000..bca3776 --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlparser/OptionsBuilder.js @@ -0,0 +1,48 @@ + +const defaultOptions = { + preserveOrder: false, + attributeNamePrefix: '@_', + attributesGroupName: false, + textNodeName: '#text', + ignoreAttributes: true, + removeNSPrefix: false, // remove NS from tag name or attribute name if true + allowBooleanAttributes: false, //a tag can have attributes without any value + //ignoreRootElement : false, + parseTagValue: true, + parseAttributeValue: false, + trimValues: true, //Trim string values of tag and attributes + cdataPropName: false, + numberParseOptions: { + hex: true, + leadingZeros: true, + eNotation: true + }, + tagValueProcessor: function(tagName, val) { + return val; + }, + attributeValueProcessor: function(attrName, val) { + return val; + }, + stopNodes: [], //nested tags will not be parsed even for errors + alwaysCreateTextNode: false, + isArray: () => false, + commentPropName: false, + unpairedTags: [], + processEntities: true, + htmlEntities: false, + ignoreDeclaration: false, + ignorePiTags: false, + transformTagName: false, + transformAttributeName: false, + updateTag: function(tagName, jPath, attrs){ + return tagName + }, + // skipEmptyListItem: false +}; + +const buildOptions = function(options) { + return Object.assign({}, defaultOptions, options); +}; + +exports.buildOptions = buildOptions; +exports.defaultOptions = defaultOptions; \ No newline at end of file diff --git a/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlparser/OrderedObjParser.js b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlparser/OrderedObjParser.js new file mode 100644 index 0000000..bdbdbc1 --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlparser/OrderedObjParser.js @@ -0,0 +1,606 @@ +'use strict'; +///@ts-check + +const util = require('../util'); +const xmlNode = require('./xmlNode'); +const readDocType = require("./DocTypeReader"); +const toNumber = require("../../vendor/strnum/strnum.js"); +const getIgnoreAttributesFn = require('../ignoreAttributes') + +// const regx = +// '<((!\\[CDATA\\[([\\s\\S]*?)(]]>))|((NAME:)?(NAME))([^>]*)>|((\\/)(NAME)\\s*>))([^<]*)' +// .replace(/NAME/g, util.nameRegexp); + +//const tagsRegx = new RegExp("<(\\/?[\\w:\\-\._]+)([^>]*)>(\\s*"+cdataRegx+")*([^<]+)?","g"); +//const tagsRegx = new RegExp("<(\\/?)((\\w*:)?([\\w:\\-\._]+))([^>]*)>([^<]*)("+cdataRegx+"([^<]*))*([^<]+)?","g"); + +class OrderedObjParser{ + constructor(options){ + this.options = options; + this.currentNode = null; + this.tagsNodeStack = []; + this.docTypeEntities = {}; + this.lastEntities = { + "apos" : { regex: /&(apos|#39|#x27);/g, val : "'"}, + "gt" : { regex: /&(gt|#62|#x3E);/g, val : ">"}, + "lt" : { regex: /&(lt|#60|#x3C);/g, val : "<"}, + "quot" : { regex: /&(quot|#34|#x22);/g, val : "\""}, + }; + this.ampEntity = { regex: /&(amp|#38|#x26);/g, val : "&"}; + this.htmlEntities = { + "space": { regex: /&(nbsp|#160);/g, val: " " }, + // "lt" : { regex: /&(lt|#60);/g, val: "<" }, + // "gt" : { regex: /&(gt|#62);/g, val: ">" }, + // "amp" : { regex: /&(amp|#38);/g, val: "&" }, + // "quot" : { regex: /&(quot|#34);/g, val: "\"" }, + // "apos" : { regex: /&(apos|#39);/g, val: "'" }, + "cent" : { regex: /&(cent|#162);/g, val: "¢" }, + "pound" : { regex: /&(pound|#163);/g, val: "£" }, + "yen" : { regex: /&(yen|#165);/g, val: "¥" }, + "euro" : { regex: /&(euro|#8364);/g, val: "€" }, + "copyright" : { regex: /&(copy|#169);/g, val: "©" }, + "reg" : { regex: /&(reg|#174);/g, val: "®" }, + "inr" : { regex: /&(inr|#8377);/g, val: "₹" }, + "num_dec": { regex: /&#([0-9]{1,7});/g, val : (_, str) => String.fromCharCode(Number.parseInt(str, 10)) }, + "num_hex": { regex: /&#x([0-9a-fA-F]{1,6});/g, val : (_, str) => String.fromCharCode(Number.parseInt(str, 16)) }, + }; + this.addExternalEntities = addExternalEntities; + this.parseXml = parseXml; + this.parseTextData = parseTextData; + this.resolveNameSpace = resolveNameSpace; + this.buildAttributesMap = buildAttributesMap; + this.isItStopNode = isItStopNode; + this.replaceEntitiesValue = replaceEntitiesValue; + this.readStopNodeData = readStopNodeData; + this.saveTextToParentTag = saveTextToParentTag; + this.addChild = addChild; + this.ignoreAttributesFn = getIgnoreAttributesFn(this.options.ignoreAttributes) + } + +} + +function addExternalEntities(externalEntities){ + const entKeys = Object.keys(externalEntities); + for (let i = 0; i < entKeys.length; i++) { + const ent = entKeys[i]; + this.lastEntities[ent] = { + regex: new RegExp("&"+ent+";","g"), + val : externalEntities[ent] + } + } +} + +/** + * @param {string} val + * @param {string} tagName + * @param {string} jPath + * @param {boolean} dontTrim + * @param {boolean} hasAttributes + * @param {boolean} isLeafNode + * @param {boolean} escapeEntities + */ +function parseTextData(val, tagName, jPath, dontTrim, hasAttributes, isLeafNode, escapeEntities) { + if (val !== undefined) { + if (this.options.trimValues && !dontTrim) { + val = val.trim(); + } + if(val.length > 0){ + if(!escapeEntities) val = this.replaceEntitiesValue(val); + + const newval = this.options.tagValueProcessor(tagName, val, jPath, hasAttributes, isLeafNode); + if(newval === null || newval === undefined){ + //don't parse + return val; + }else if(typeof newval !== typeof val || newval !== val){ + //overwrite + return newval; + }else if(this.options.trimValues){ + return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions); + }else{ + const trimmedVal = val.trim(); + if(trimmedVal === val){ + return parseValue(val, this.options.parseTagValue, this.options.numberParseOptions); + }else{ + return val; + } + } + } + } +} + +function resolveNameSpace(tagname) { + if (this.options.removeNSPrefix) { + const tags = tagname.split(':'); + const prefix = tagname.charAt(0) === '/' ? '/' : ''; + if (tags[0] === 'xmlns') { + return ''; + } + if (tags.length === 2) { + tagname = prefix + tags[1]; + } + } + return tagname; +} + +//TODO: change regex to capture NS +//const attrsRegx = new RegExp("([\\w\\-\\.\\:]+)\\s*=\\s*(['\"])((.|\n)*?)\\2","gm"); +const attrsRegx = new RegExp('([^\\s=]+)\\s*(=\\s*([\'"])([\\s\\S]*?)\\3)?', 'gm'); + +function buildAttributesMap(attrStr, jPath, tagName) { + if (this.options.ignoreAttributes !== true && typeof attrStr === 'string') { + // attrStr = attrStr.replace(/\r?\n/g, ' '); + //attrStr = attrStr || attrStr.trim(); + + const matches = util.getAllMatches(attrStr, attrsRegx); + const len = matches.length; //don't make it inline + const attrs = {}; + for (let i = 0; i < len; i++) { + const attrName = this.resolveNameSpace(matches[i][1]); + if (this.ignoreAttributesFn(attrName, jPath)) { + continue + } + let oldVal = matches[i][4]; + let aName = this.options.attributeNamePrefix + attrName; + if (attrName.length) { + if (this.options.transformAttributeName) { + aName = this.options.transformAttributeName(aName); + } + if(aName === "__proto__") aName = "#__proto__"; + if (oldVal !== undefined) { + if (this.options.trimValues) { + oldVal = oldVal.trim(); + } + oldVal = this.replaceEntitiesValue(oldVal); + const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPath); + if(newVal === null || newVal === undefined){ + //don't parse + attrs[aName] = oldVal; + }else if(typeof newVal !== typeof oldVal || newVal !== oldVal){ + //overwrite + attrs[aName] = newVal; + }else{ + //parse + attrs[aName] = parseValue( + oldVal, + this.options.parseAttributeValue, + this.options.numberParseOptions + ); + } + } else if (this.options.allowBooleanAttributes) { + attrs[aName] = true; + } + } + } + if (!Object.keys(attrs).length) { + return; + } + if (this.options.attributesGroupName) { + const attrCollection = {}; + attrCollection[this.options.attributesGroupName] = attrs; + return attrCollection; + } + return attrs + } +} + +const parseXml = function(xmlData) { + xmlData = xmlData.replace(/\r\n?/g, "\n"); //TODO: remove this line + const xmlObj = new xmlNode('!xml'); + let currentNode = xmlObj; + let textData = ""; + let jPath = ""; + for(let i=0; i< xmlData.length; i++){//for each char in XML data + const ch = xmlData[i]; + if(ch === '<'){ + // const nextIndex = i+1; + // const _2ndChar = xmlData[nextIndex]; + if( xmlData[i+1] === '/') {//Closing Tag + const closeIndex = findClosingIndex(xmlData, ">", i, "Closing Tag is not closed.") + let tagName = xmlData.substring(i+2,closeIndex).trim(); + + if(this.options.removeNSPrefix){ + const colonIndex = tagName.indexOf(":"); + if(colonIndex !== -1){ + tagName = tagName.substr(colonIndex+1); + } + } + + if(this.options.transformTagName) { + tagName = this.options.transformTagName(tagName); + } + + if(currentNode){ + textData = this.saveTextToParentTag(textData, currentNode, jPath); + } + + //check if last tag of nested tag was unpaired tag + const lastTagName = jPath.substring(jPath.lastIndexOf(".")+1); + if(tagName && this.options.unpairedTags.indexOf(tagName) !== -1 ){ + throw new Error(`Unpaired tag can not be used as closing tag: `); + } + let propIndex = 0 + if(lastTagName && this.options.unpairedTags.indexOf(lastTagName) !== -1 ){ + propIndex = jPath.lastIndexOf('.', jPath.lastIndexOf('.')-1) + this.tagsNodeStack.pop(); + }else{ + propIndex = jPath.lastIndexOf("."); + } + jPath = jPath.substring(0, propIndex); + + currentNode = this.tagsNodeStack.pop();//avoid recursion, set the parent tag scope + textData = ""; + i = closeIndex; + } else if( xmlData[i+1] === '?') { + + let tagData = readTagExp(xmlData,i, false, "?>"); + if(!tagData) throw new Error("Pi Tag is not closed."); + + textData = this.saveTextToParentTag(textData, currentNode, jPath); + if( (this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags){ + + }else{ + + const childNode = new xmlNode(tagData.tagName); + childNode.add(this.options.textNodeName, ""); + + if(tagData.tagName !== tagData.tagExp && tagData.attrExpPresent){ + childNode[":@"] = this.buildAttributesMap(tagData.tagExp, jPath, tagData.tagName); + } + this.addChild(currentNode, childNode, jPath) + + } + + + i = tagData.closeIndex + 1; + } else if(xmlData.substr(i + 1, 3) === '!--') { + const endIndex = findClosingIndex(xmlData, "-->", i+4, "Comment is not closed.") + if(this.options.commentPropName){ + const comment = xmlData.substring(i + 4, endIndex - 2); + + textData = this.saveTextToParentTag(textData, currentNode, jPath); + + currentNode.add(this.options.commentPropName, [ { [this.options.textNodeName] : comment } ]); + } + i = endIndex; + } else if( xmlData.substr(i + 1, 2) === '!D') { + const result = readDocType(xmlData, i); + this.docTypeEntities = result.entities; + i = result.i; + }else if(xmlData.substr(i + 1, 2) === '![') { + const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2; + const tagExp = xmlData.substring(i + 9,closeIndex); + + textData = this.saveTextToParentTag(textData, currentNode, jPath); + + let val = this.parseTextData(tagExp, currentNode.tagname, jPath, true, false, true, true); + if(val == undefined) val = ""; + + //cdata should be set even if it is 0 length string + if(this.options.cdataPropName){ + currentNode.add(this.options.cdataPropName, [ { [this.options.textNodeName] : tagExp } ]); + }else{ + currentNode.add(this.options.textNodeName, val); + } + + i = closeIndex + 2; + }else {//Opening tag + let result = readTagExp(xmlData,i, this.options.removeNSPrefix); + let tagName= result.tagName; + const rawTagName = result.rawTagName; + let tagExp = result.tagExp; + let attrExpPresent = result.attrExpPresent; + let closeIndex = result.closeIndex; + + if (this.options.transformTagName) { + tagName = this.options.transformTagName(tagName); + } + + //save text as child node + if (currentNode && textData) { + if(currentNode.tagname !== '!xml'){ + //when nested tag is found + textData = this.saveTextToParentTag(textData, currentNode, jPath, false); + } + } + + //check if last tag was unpaired tag + const lastTag = currentNode; + if(lastTag && this.options.unpairedTags.indexOf(lastTag.tagname) !== -1 ){ + currentNode = this.tagsNodeStack.pop(); + jPath = jPath.substring(0, jPath.lastIndexOf(".")); + } + if(tagName !== xmlObj.tagname){ + jPath += jPath ? "." + tagName : tagName; + } + if (this.isItStopNode(this.options.stopNodes, jPath, tagName)) { + let tagContent = ""; + //self-closing tag + if(tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1){ + if(tagName[tagName.length - 1] === "/"){ //remove trailing '/' + tagName = tagName.substr(0, tagName.length - 1); + jPath = jPath.substr(0, jPath.length - 1); + tagExp = tagName; + }else{ + tagExp = tagExp.substr(0, tagExp.length - 1); + } + i = result.closeIndex; + } + //unpaired tag + else if(this.options.unpairedTags.indexOf(tagName) !== -1){ + + i = result.closeIndex; + } + //normal tag + else{ + //read until closing tag is found + const result = this.readStopNodeData(xmlData, rawTagName, closeIndex + 1); + if(!result) throw new Error(`Unexpected end of ${rawTagName}`); + i = result.i; + tagContent = result.tagContent; + } + + const childNode = new xmlNode(tagName); + if(tagName !== tagExp && attrExpPresent){ + childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName); + } + if(tagContent) { + tagContent = this.parseTextData(tagContent, tagName, jPath, true, attrExpPresent, true, true); + } + + jPath = jPath.substr(0, jPath.lastIndexOf(".")); + childNode.add(this.options.textNodeName, tagContent); + + this.addChild(currentNode, childNode, jPath) + }else{ + //selfClosing tag + if(tagExp.length > 0 && tagExp.lastIndexOf("/") === tagExp.length - 1){ + if(tagName[tagName.length - 1] === "/"){ //remove trailing '/' + tagName = tagName.substr(0, tagName.length - 1); + jPath = jPath.substr(0, jPath.length - 1); + tagExp = tagName; + }else{ + tagExp = tagExp.substr(0, tagExp.length - 1); + } + + if(this.options.transformTagName) { + tagName = this.options.transformTagName(tagName); + } + + const childNode = new xmlNode(tagName); + if(tagName !== tagExp && attrExpPresent){ + childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName); + } + this.addChild(currentNode, childNode, jPath) + jPath = jPath.substr(0, jPath.lastIndexOf(".")); + } + //opening tag + else{ + const childNode = new xmlNode( tagName); + this.tagsNodeStack.push(currentNode); + + if(tagName !== tagExp && attrExpPresent){ + childNode[":@"] = this.buildAttributesMap(tagExp, jPath, tagName); + } + this.addChild(currentNode, childNode, jPath) + currentNode = childNode; + } + textData = ""; + i = closeIndex; + } + } + }else{ + textData += xmlData[i]; + } + } + return xmlObj.child; +} + +function addChild(currentNode, childNode, jPath){ + const result = this.options.updateTag(childNode.tagname, jPath, childNode[":@"]) + if(result === false){ + }else if(typeof result === "string"){ + childNode.tagname = result + currentNode.addChild(childNode); + }else{ + currentNode.addChild(childNode); + } +} + +const replaceEntitiesValue = function(val){ + + if(this.options.processEntities){ + for(let entityName in this.docTypeEntities){ + const entity = this.docTypeEntities[entityName]; + val = val.replace( entity.regx, entity.val); + } + for(let entityName in this.lastEntities){ + const entity = this.lastEntities[entityName]; + val = val.replace( entity.regex, entity.val); + } + if(this.options.htmlEntities){ + for(let entityName in this.htmlEntities){ + const entity = this.htmlEntities[entityName]; + val = val.replace( entity.regex, entity.val); + } + } + val = val.replace( this.ampEntity.regex, this.ampEntity.val); + } + return val; +} +function saveTextToParentTag(textData, currentNode, jPath, isLeafNode) { + if (textData) { //store previously collected data as textNode + if(isLeafNode === undefined) isLeafNode = Object.keys(currentNode.child).length === 0 + + textData = this.parseTextData(textData, + currentNode.tagname, + jPath, + false, + currentNode[":@"] ? Object.keys(currentNode[":@"]).length !== 0 : false, + isLeafNode); + + if (textData !== undefined && textData !== "") + currentNode.add(this.options.textNodeName, textData); + textData = ""; + } + return textData; +} + +//TODO: use jPath to simplify the logic +/** + * + * @param {string[]} stopNodes + * @param {string} jPath + * @param {string} currentTagName + */ +function isItStopNode(stopNodes, jPath, currentTagName){ + const allNodesExp = "*." + currentTagName; + for (const stopNodePath in stopNodes) { + const stopNodeExp = stopNodes[stopNodePath]; + if( allNodesExp === stopNodeExp || jPath === stopNodeExp ) return true; + } + return false; +} + +/** + * Returns the tag Expression and where it is ending handling single-double quotes situation + * @param {string} xmlData + * @param {number} i starting index + * @returns + */ +function tagExpWithClosingIndex(xmlData, i, closingChar = ">"){ + let attrBoundary; + let tagExp = ""; + for (let index = i; index < xmlData.length; index++) { + let ch = xmlData[index]; + if (attrBoundary) { + if (ch === attrBoundary) attrBoundary = "";//reset + } else if (ch === '"' || ch === "'") { + attrBoundary = ch; + } else if (ch === closingChar[0]) { + if(closingChar[1]){ + if(xmlData[index + 1] === closingChar[1]){ + return { + data: tagExp, + index: index + } + } + }else{ + return { + data: tagExp, + index: index + } + } + } else if (ch === '\t') { + ch = " " + } + tagExp += ch; + } +} + +function findClosingIndex(xmlData, str, i, errMsg){ + const closingIndex = xmlData.indexOf(str, i); + if(closingIndex === -1){ + throw new Error(errMsg) + }else{ + return closingIndex + str.length - 1; + } +} + +function readTagExp(xmlData,i, removeNSPrefix, closingChar = ">"){ + const result = tagExpWithClosingIndex(xmlData, i+1, closingChar); + if(!result) return; + let tagExp = result.data; + const closeIndex = result.index; + const separatorIndex = tagExp.search(/\s/); + let tagName = tagExp; + let attrExpPresent = true; + if(separatorIndex !== -1){//separate tag name and attributes expression + tagName = tagExp.substring(0, separatorIndex); + tagExp = tagExp.substring(separatorIndex + 1).trimStart(); + } + + const rawTagName = tagName; + if(removeNSPrefix){ + const colonIndex = tagName.indexOf(":"); + if(colonIndex !== -1){ + tagName = tagName.substr(colonIndex+1); + attrExpPresent = tagName !== result.data.substr(colonIndex + 1); + } + } + + return { + tagName: tagName, + tagExp: tagExp, + closeIndex: closeIndex, + attrExpPresent: attrExpPresent, + rawTagName: rawTagName, + } +} +/** + * find paired tag for a stop node + * @param {string} xmlData + * @param {string} tagName + * @param {number} i + */ +function readStopNodeData(xmlData, tagName, i){ + const startIndex = i; + // Starting at 1 since we already have an open tag + let openTagCount = 1; + + for (; i < xmlData.length; i++) { + if( xmlData[i] === "<"){ + if (xmlData[i+1] === "/") {//close tag + const closeIndex = findClosingIndex(xmlData, ">", i, `${tagName} is not closed`); + let closeTagName = xmlData.substring(i+2,closeIndex).trim(); + if(closeTagName === tagName){ + openTagCount--; + if (openTagCount === 0) { + return { + tagContent: xmlData.substring(startIndex, i), + i : closeIndex + } + } + } + i=closeIndex; + } else if(xmlData[i+1] === '?') { + const closeIndex = findClosingIndex(xmlData, "?>", i+1, "StopNode is not closed.") + i=closeIndex; + } else if(xmlData.substr(i + 1, 3) === '!--') { + const closeIndex = findClosingIndex(xmlData, "-->", i+3, "StopNode is not closed.") + i=closeIndex; + } else if(xmlData.substr(i + 1, 2) === '![') { + const closeIndex = findClosingIndex(xmlData, "]]>", i, "StopNode is not closed.") - 2; + i=closeIndex; + } else { + const tagData = readTagExp(xmlData, i, '>') + + if (tagData) { + const openTagName = tagData && tagData.tagName; + if (openTagName === tagName && tagData.tagExp[tagData.tagExp.length-1] !== "/") { + openTagCount++; + } + i=tagData.closeIndex; + } + } + } + }//end for loop +} + +function parseValue(val, shouldParse, options) { + if (shouldParse && typeof val === 'string') { + //console.log(options) + const newval = val.trim(); + if(newval === 'true' ) return true; + else if(newval === 'false' ) return false; + else return toNumber(val, options); + } else { + if (util.isExist(val)) { + return val; + } else { + return ''; + } + } +} + + +module.exports = OrderedObjParser; diff --git a/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlparser/XMLParser.js b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlparser/XMLParser.js new file mode 100644 index 0000000..ffaf59b --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlparser/XMLParser.js @@ -0,0 +1,58 @@ +const { buildOptions} = require("./OptionsBuilder"); +const OrderedObjParser = require("./OrderedObjParser"); +const { prettify} = require("./node2json"); +const validator = require('../validator'); + +class XMLParser{ + + constructor(options){ + this.externalEntities = {}; + this.options = buildOptions(options); + + } + /** + * Parse XML dats to JS object + * @param {string|Buffer} xmlData + * @param {boolean|Object} validationOption + */ + parse(xmlData,validationOption){ + if(typeof xmlData === "string"){ + }else if( xmlData.toString){ + xmlData = xmlData.toString(); + }else{ + throw new Error("XML data is accepted in String or Bytes[] form.") + } + if( validationOption){ + if(validationOption === true) validationOption = {}; //validate with default options + + const result = validator.validate(xmlData, validationOption); + if (result !== true) { + throw Error( `${result.err.msg}:${result.err.line}:${result.err.col}` ) + } + } + const orderedObjParser = new OrderedObjParser(this.options); + orderedObjParser.addExternalEntities(this.externalEntities); + const orderedResult = orderedObjParser.parseXml(xmlData); + if(this.options.preserveOrder || orderedResult === undefined) return orderedResult; + else return prettify(orderedResult, this.options); + } + + /** + * Add Entity which is not by default supported by this library + * @param {string} key + * @param {string} value + */ + addEntity(key, value){ + if(value.indexOf("&") !== -1){ + throw new Error("Entity value can't have '&'") + }else if(key.indexOf("&") !== -1 || key.indexOf(";") !== -1){ + throw new Error("An entity must be set without '&' and ';'. Eg. use '#xD' for ' '") + }else if(value === "&"){ + throw new Error("An entity with value '&' is not permitted"); + }else{ + this.externalEntities[key] = value; + } + } +} + +module.exports = XMLParser; \ No newline at end of file diff --git a/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlparser/node2json.js b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlparser/node2json.js new file mode 100644 index 0000000..3045573 --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlparser/node2json.js @@ -0,0 +1,113 @@ +'use strict'; + +/** + * + * @param {array} node + * @param {any} options + * @returns + */ +function prettify(node, options){ + return compress( node, options); +} + +/** + * + * @param {array} arr + * @param {object} options + * @param {string} jPath + * @returns object + */ +function compress(arr, options, jPath){ + let text; + const compressedObj = {}; + for (let i = 0; i < arr.length; i++) { + const tagObj = arr[i]; + const property = propName(tagObj); + let newJpath = ""; + if(jPath === undefined) newJpath = property; + else newJpath = jPath + "." + property; + + if(property === options.textNodeName){ + if(text === undefined) text = tagObj[property]; + else text += "" + tagObj[property]; + }else if(property === undefined){ + continue; + }else if(tagObj[property]){ + + let val = compress(tagObj[property], options, newJpath); + const isLeaf = isLeafTag(val, options); + + if(tagObj[":@"]){ + assignAttributes( val, tagObj[":@"], newJpath, options); + }else if(Object.keys(val).length === 1 && val[options.textNodeName] !== undefined && !options.alwaysCreateTextNode){ + val = val[options.textNodeName]; + }else if(Object.keys(val).length === 0){ + if(options.alwaysCreateTextNode) val[options.textNodeName] = ""; + else val = ""; + } + + if(compressedObj[property] !== undefined && compressedObj.hasOwnProperty(property)) { + if(!Array.isArray(compressedObj[property])) { + compressedObj[property] = [ compressedObj[property] ]; + } + compressedObj[property].push(val); + }else{ + //TODO: if a node is not an array, then check if it should be an array + //also determine if it is a leaf node + if (options.isArray(property, newJpath, isLeaf )) { + compressedObj[property] = [val]; + }else{ + compressedObj[property] = val; + } + } + } + + } + // if(text && text.length > 0) compressedObj[options.textNodeName] = text; + if(typeof text === "string"){ + if(text.length > 0) compressedObj[options.textNodeName] = text; + }else if(text !== undefined) compressedObj[options.textNodeName] = text; + return compressedObj; +} + +function propName(obj){ + const keys = Object.keys(obj); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if(key !== ":@") return key; + } +} + +function assignAttributes(obj, attrMap, jpath, options){ + if (attrMap) { + const keys = Object.keys(attrMap); + const len = keys.length; //don't make it inline + for (let i = 0; i < len; i++) { + const atrrName = keys[i]; + if (options.isArray(atrrName, jpath + "." + atrrName, true, true)) { + obj[atrrName] = [ attrMap[atrrName] ]; + } else { + obj[atrrName] = attrMap[atrrName]; + } + } + } +} + +function isLeafTag(obj, options){ + const { textNodeName } = options; + const propCount = Object.keys(obj).length; + + if (propCount === 0) { + return true; + } + + if ( + propCount === 1 && + (obj[textNodeName] || typeof obj[textNodeName] === "boolean" || obj[textNodeName] === 0) + ) { + return true; + } + + return false; +} +exports.prettify = prettify; diff --git a/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlparser/xmlNode.js b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlparser/xmlNode.js new file mode 100644 index 0000000..9319524 --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/fast-xml-parser/src/xmlparser/xmlNode.js @@ -0,0 +1,25 @@ +'use strict'; + +class XmlNode{ + constructor(tagname) { + this.tagname = tagname; + this.child = []; //nested tags, text, cdata, comments in order + this[":@"] = {}; //attributes map + } + add(key,val){ + // this.child.push( {name : key, val: val, isCdata: isCdata }); + if(key === "__proto__") key = "#__proto__"; + this.child.push( {[key]: val }); + } + addChild(node) { + if(node.tagname === "__proto__") node.tagname = "#__proto__"; + if(node[":@"] && Object.keys(node[":@"]).length > 0){ + this.child.push( { [node.tagname]: node.child, [":@"]: node[":@"] }); + }else{ + this.child.push( { [node.tagname]: node.child }); + } + }; +}; + + +module.exports = XmlNode; \ No newline at end of file diff --git a/skills/java-to-typescript/scripts/lib/fast-xml-parser/vendor/package.json b/skills/java-to-typescript/scripts/lib/fast-xml-parser/vendor/package.json new file mode 100644 index 0000000..5bbefff --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/fast-xml-parser/vendor/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/skills/java-to-typescript/scripts/lib/fast-xml-parser/vendor/strnum/LICENSE b/skills/java-to-typescript/scripts/lib/fast-xml-parser/vendor/strnum/LICENSE new file mode 100644 index 0000000..6450554 --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/fast-xml-parser/vendor/strnum/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Natural Intelligence + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/java-to-typescript/scripts/lib/fast-xml-parser/vendor/strnum/strnum.js b/skills/java-to-typescript/scripts/lib/fast-xml-parser/vendor/strnum/strnum.js new file mode 100644 index 0000000..c3bd08e --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/fast-xml-parser/vendor/strnum/strnum.js @@ -0,0 +1,111 @@ +const hexRegex = /^[-+]?0x[a-fA-F0-9]+$/; +const numRegex = /^([\-\+])?(0*)([0-9]*(\.[0-9]*)?)$/; +// const octRegex = /^0x[a-z0-9]+/; +// const binRegex = /0x[a-z0-9]+/; + + +const consider = { + hex : true, + // oct: false, + leadingZeros: true, + decimalPoint: "\.", + eNotation: true, + //skipLike: /regex/ +}; + +function toNumber(str, options = {}){ + options = Object.assign({}, consider, options ); + if(!str || typeof str !== "string" ) return str; + + let trimmedStr = str.trim(); + + if(options.skipLike !== undefined && options.skipLike.test(trimmedStr)) return str; + else if(str==="0") return 0; + else if (options.hex && hexRegex.test(trimmedStr)) { + return parse_int(trimmedStr, 16); + // }else if (options.oct && octRegex.test(str)) { + // return Number.parseInt(val, 8); + }else if (trimmedStr.search(/[eE]/)!== -1) { //eNotation + const notation = trimmedStr.match(/^([-\+])?(0*)([0-9]*(\.[0-9]*)?[eE][-\+]?[0-9]+)$/); + // +00.123 => [ , '+', '00', '.123', .. + if(notation){ + // console.log(notation) + if(options.leadingZeros){ //accept with leading zeros + trimmedStr = (notation[1] || "") + notation[3]; + }else{ + if(notation[2] === "0" && notation[3][0]=== "."){ //valid number + }else{ + return str; + } + } + return options.eNotation ? Number(trimmedStr) : str; + }else{ + return str; + } + // }else if (options.parseBin && binRegex.test(str)) { + // return Number.parseInt(val, 2); + }else{ + //separate negative sign, leading zeros, and rest number + const match = numRegex.exec(trimmedStr); + // +00.123 => [ , '+', '00', '.123', .. + if(match){ + const sign = match[1]; + const leadingZeros = match[2]; + let numTrimmedByZeros = trimZeros(match[3]); //complete num without leading zeros + //trim ending zeros for floating number + + if(!options.leadingZeros && leadingZeros.length > 0 && sign && trimmedStr[2] !== ".") return str; //-0123 + else if(!options.leadingZeros && leadingZeros.length > 0 && !sign && trimmedStr[1] !== ".") return str; //0123 + else if(options.leadingZeros && leadingZeros===str) return 0; //00 + + else{//no leading zeros or leading zeros are allowed + const num = Number(trimmedStr); + const numStr = "" + num; + + if(numStr.search(/[eE]/) !== -1){ //given number is long and parsed to eNotation + if(options.eNotation) return num; + else return str; + }else if(trimmedStr.indexOf(".") !== -1){ //floating number + if(numStr === "0" && (numTrimmedByZeros === "") ) return num; //0.0 + else if(numStr === numTrimmedByZeros) return num; //0.456. 0.79000 + else if( sign && numStr === "-"+numTrimmedByZeros) return num; + else return str; + } + + if(leadingZeros){ + return (numTrimmedByZeros === numStr) || (sign+numTrimmedByZeros === numStr) ? num : str + }else { + return (trimmedStr === numStr) || (trimmedStr === sign+numStr) ? num : str + } + } + }else{ //non-numeric string + return str; + } + } +} + +/** + * + * @param {string} numStr without leading zeros + * @returns + */ +function trimZeros(numStr){ + if(numStr && numStr.indexOf(".") !== -1){//float + numStr = numStr.replace(/0+$/, ""); //remove ending zeros + if(numStr === ".") numStr = "0"; + else if(numStr[0] === ".") numStr = "0"+numStr; + else if(numStr[numStr.length-1] === ".") numStr = numStr.substr(0,numStr.length-1); + return numStr; + } + return numStr; +} + +function parse_int(numStr, base){ + //polyfill + if(parseInt) return parseInt(numStr, base); + else if(Number.parseInt) return Number.parseInt(numStr, base); + else if(window && window.parseInt) return window.parseInt(numStr, base); + else throw new Error("parseInt, Number.parseInt, window.parseInt are not supported") +} + +module.exports = toNumber; \ No newline at end of file diff --git a/skills/java-to-typescript/scripts/lib/http.ts b/skills/java-to-typescript/scripts/lib/http.ts new file mode 100644 index 0000000..1b1e7d6 --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/http.ts @@ -0,0 +1,78 @@ +import { request as httpRequest, type RequestOptions } from 'node:http'; +import { URL } from 'node:url'; + +export type HttpRequest = { + baseUrl: string; + method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'; + path: string; + headers?: Record; + body?: unknown; +}; + +export type HttpResponse = { + status: number; + headers: Record; + body: unknown; +}; + +const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1']); + +export function isLoopback(baseUrl: string): boolean { + try { + const u = new URL(baseUrl); + const host = u.hostname.replace(/^\[|\]$/g, ''); + return LOOPBACK_HOSTS.has(host); + } catch { + return false; + } +} + +export async function request(req: HttpRequest): Promise { + if (!isLoopback(req.baseUrl)) { + throw new Error(`refusing non-loopback baseUrl: ${req.baseUrl} (policy: loopback only)`); + } + const url = new URL(req.path, req.baseUrl); + const bodyStr = + req.body === undefined ? undefined : typeof req.body === 'string' ? req.body : JSON.stringify(req.body); + const headers = { ...(req.headers ?? {}) }; + if (bodyStr !== undefined && !('content-length' in headers)) { + headers['content-length'] = String(Buffer.byteLength(bodyStr)); + } + const options: RequestOptions = { + protocol: url.protocol, + hostname: url.hostname, + method: req.method, + path: `${url.pathname}${url.search}`, + headers, + }; + if (url.port !== '') options.port = url.port; + return new Promise((resolve, reject) => { + const r = httpRequest(options, (res) => { + const chunks: Buffer[] = []; + res.on('data', (c: Buffer) => chunks.push(c)); + res.on('end', () => { + const raw = Buffer.concat(chunks).toString('utf8'); + const ctype = String(res.headers['content-type'] ?? ''); + const isJson = ctype.includes('application/json'); + const parsed = raw.length === 0 ? null : isJson ? safeJson(raw) : raw; + const flatHeaders: Record = {}; + for (const [k, v] of Object.entries(res.headers)) { + if (typeof v === 'string') flatHeaders[k] = v; + else if (Array.isArray(v)) flatHeaders[k] = v.join(', '); + } + resolve({ status: res.statusCode ?? 0, headers: flatHeaders, body: parsed }); + }); + }); + r.on('error', reject); + if (bodyStr !== undefined) r.write(bodyStr); + r.end(); + }); +} + +function safeJson(s: string): unknown { + try { + return JSON.parse(s); + } catch { + return s; + } +} diff --git a/skills/java-to-typescript/scripts/lib/json-diff.ts b/skills/java-to-typescript/scripts/lib/json-diff.ts new file mode 100644 index 0000000..b4dfc75 --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/json-diff.ts @@ -0,0 +1,129 @@ +import { matchPaths, applyMask } from './jsonpath-lite.js'; + +type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue }; + +export type Allowlist = { + headers?: string[]; + bodyPaths?: string[]; + arrayKeys?: Record; +}; + +export type Diff = { path: string; expected: unknown; actual: unknown }; + +export function diff(expected: JsonValue, actual: JsonValue, allow: Allowlist): Diff[] { + let exp = expected; + let act = actual; + if (allow.bodyPaths && allow.bodyPaths.length > 0) { + const allPaths = (v: JsonValue): string[] => + allow.bodyPaths!.flatMap((p) => matchPaths(v, p)); + exp = applyMask(exp, allPaths(exp)); + act = applyMask(act, allPaths(act)); + } + const arrayKeys = allow.arrayKeys ?? {}; + const out: Diff[] = []; + walk(exp, act, '$', arrayKeys, out); + return out; +} + +function walk( + exp: JsonValue, + act: JsonValue, + path: string, + arrayKeys: Record, + out: Diff[] +): void { + if (Array.isArray(exp) && Array.isArray(act)) { + const keyField = arrayKeys[path]; + if (keyField !== undefined) { + diffArrayByKey(exp, act, path, keyField, arrayKeys, out); + } else { + diffArrayOrdered(exp, act, path, arrayKeys, out); + } + return; + } + if (isObject(exp) && isObject(act)) { + const keys = new Set([...Object.keys(exp), ...Object.keys(act)]); + for (const k of keys) { + if (!(k in exp)) { + out.push({ path: `${path}.${k}`, expected: undefined, actual: act[k] }); + continue; + } + if (!(k in act)) { + out.push({ path: `${path}.${k}`, expected: exp[k], actual: undefined }); + continue; + } + walk(exp[k]!, act[k]!, `${path}.${k}`, arrayKeys, out); + } + return; + } + if (exp !== act) { + if (typeof exp === typeof act && exp !== null && act !== null && typeof exp === 'object') { + out.push({ path, expected: exp, actual: act }); + } else if (!sameLeaf(exp, act)) { + out.push({ path, expected: exp, actual: act }); + } + } +} + +function diffArrayOrdered( + exp: JsonValue[], + act: JsonValue[], + path: string, + arrayKeys: Record, + out: Diff[] +): void { + const len = Math.max(exp.length, act.length); + for (let i = 0; i < len; i++) { + if (i >= exp.length) { + out.push({ path: `${path}[${i}]`, expected: undefined, actual: act[i] }); + continue; + } + if (i >= act.length) { + out.push({ path: `${path}[${i}]`, expected: exp[i], actual: undefined }); + continue; + } + walk(exp[i]!, act[i]!, `${path}[${i}]`, arrayKeys, out); + } +} + +function diffArrayByKey( + exp: JsonValue[], + act: JsonValue[], + path: string, + keyField: string, + arrayKeys: Record, + out: Diff[] +): void { + const indexBy = (arr: JsonValue[]): Map => { + const m = new Map(); + for (const item of arr) { + if (isObject(item) && keyField in item) m.set(item[keyField], item); + } + return m; + }; + const expMap = indexBy(exp); + const actMap = indexBy(act); + const keys = new Set([...expMap.keys(), ...actMap.keys()]); + for (const k of keys) { + const expItem = expMap.get(k); + const actItem = actMap.get(k); + const itemPath = `${path}[${keyField}=${String(k)}]`; + if (expItem === undefined) { + out.push({ path: itemPath, expected: undefined, actual: actItem }); + continue; + } + if (actItem === undefined) { + out.push({ path: itemPath, expected: expItem, actual: undefined }); + continue; + } + walk(expItem, actItem, itemPath, arrayKeys, out); + } +} + +function isObject(v: unknown): v is Record { + return v !== null && typeof v === 'object' && !Array.isArray(v); +} + +function sameLeaf(a: unknown, b: unknown): boolean { + return a === b; +} diff --git a/skills/java-to-typescript/scripts/lib/jsonpath-lite.ts b/skills/java-to-typescript/scripts/lib/jsonpath-lite.ts new file mode 100644 index 0000000..e1353cb --- /dev/null +++ b/skills/java-to-typescript/scripts/lib/jsonpath-lite.ts @@ -0,0 +1,106 @@ +type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue }; + +type Segment = + | { kind: 'field'; name: string } + | { kind: 'index'; index: number } + | { kind: 'recursive' } + | { kind: 'wildcard' }; + +function parse(expr: string): Segment[] { + if (!expr.startsWith('$')) throw new Error(`jsonpath must start with $: ${expr}`); + const rest = expr.slice(1); + const segments: Segment[] = []; + let i = 0; + while (i < rest.length) { + const ch = rest[i]!; + if (ch === '.' && rest[i + 1] === '.') { + segments.push({ kind: 'recursive' }); + i += 2; + // After `..`, the next token may be a bare field/wildcard with no leading `.` + // (e.g. `$..uuid`, `$..*.foo`). `[idx]` and a further `.` fall through naturally. + if (i < rest.length && rest[i] !== '.' && rest[i] !== '[') { + const start = i; + while (i < rest.length && rest[i] !== '.' && rest[i] !== '[') i += 1; + const name = rest.slice(start, i); + segments.push(name === '*' ? { kind: 'wildcard' } : { kind: 'field', name }); + } + } else if (ch === '.') { + i += 1; + const start = i; + while (i < rest.length && rest[i] !== '.' && rest[i] !== '[') i += 1; + const name = rest.slice(start, i); + segments.push(name === '*' ? { kind: 'wildcard' } : { kind: 'field', name }); + } else if (ch === '[') { + const end = rest.indexOf(']', i); + if (end < 0) throw new Error(`unterminated [ in ${expr}`); + const inner = rest.slice(i + 1, end); + const idx = Number(inner); + if (!Number.isInteger(idx)) throw new Error(`only integer indices supported: [${inner}]`); + segments.push({ kind: 'index', index: idx }); + i = end + 1; + } else { + throw new Error(`unexpected char '${ch}' at offset ${i} in ${expr}`); + } + } + return segments; +} + +export function matchPaths(root: JsonValue, expr: string): string[] { + const segments = parse(expr); + const matches: string[] = []; + walk(root, segments, 0, '$', matches); + return matches; +} + +function walk(value: JsonValue, segs: Segment[], i: number, path: string, out: string[]): void { + if (i === segs.length) { out.push(path); return; } + const seg = segs[i]!; + if (seg.kind === 'field') { + if (value !== null && typeof value === 'object' && !Array.isArray(value) && seg.name in value) { + walk(value[seg.name]!, segs, i + 1, `${path}.${seg.name}`, out); + } + } else if (seg.kind === 'index') { + if (Array.isArray(value) && seg.index < value.length) { + walk(value[seg.index]!, segs, i + 1, `${path}[${seg.index}]`, out); + } + } else if (seg.kind === 'wildcard') { + if (Array.isArray(value)) { + value.forEach((v, idx) => walk(v, segs, i + 1, `${path}[${idx}]`, out)); + } else if (value !== null && typeof value === 'object') { + for (const [k, v] of Object.entries(value)) walk(v, segs, i + 1, `${path}.${k}`, out); + } + } else { // recursive + walk(value, segs, i + 1, path, out); + if (Array.isArray(value)) { + value.forEach((v, idx) => walk(v, segs, i, `${path}[${idx}]`, out)); + } else if (value !== null && typeof value === 'object') { + for (const [k, v] of Object.entries(value)) walk(v, segs, i, `${path}.${k}`, out); + } + } +} + +export function applyMask(root: JsonValue, paths: string[]): JsonValue { + const cloned = structuredClone(root); + for (const p of paths) removeAtPath(cloned, p); + return cloned; +} + +function removeAtPath(root: JsonValue, path: string): void { + if (!path.startsWith('$')) return; + const segs = parse(path); + let cur: JsonValue = root; + for (let i = 0; i < segs.length - 1; i++) { + const seg = segs[i]!; + if (seg.kind === 'field' && cur !== null && typeof cur === 'object' && !Array.isArray(cur)) { + cur = cur[seg.name]!; + } else if (seg.kind === 'index' && Array.isArray(cur)) { + cur = cur[seg.index]!; + } else { return; } + } + const last = segs[segs.length - 1]!; + if (last.kind === 'field' && cur !== null && typeof cur === 'object' && !Array.isArray(cur)) { + delete (cur as Record)[last.name]; + } else if (last.kind === 'index' && Array.isArray(cur)) { + cur.splice(last.index, 1); + } +} diff --git a/skills/java-to-typescript/scripts/pom-to-workspace.ts b/skills/java-to-typescript/scripts/pom-to-workspace.ts new file mode 100644 index 0000000..77c3ac8 --- /dev/null +++ b/skills/java-to-typescript/scripts/pom-to-workspace.ts @@ -0,0 +1,260 @@ +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import { XMLParser } from './lib/fast-xml-parser/index.js'; +import { parseArgs } from 'node:util'; + +export type Analysis = { + buildSystem: 'maven' | 'gradle-groovy' | 'gradle-kotlin'; + rootGroupId: string; + modules: ModuleInfo[]; + unmappedDependencies: { groupId: string; artifactId: string; usedBy: string[] }[]; + parseWarnings: string[]; +}; + +export type ModuleInfo = { + path: string; + artifactId: string; + packaging: string; + dependencies: { groupId: string; artifactId: string; scope: string }[]; + detectedFramework: 'spring-boot' | 'spring-mvc' | 'quarkus' | 'micronaut' | null; +}; + +const KNOWN_GROUP_PREFIXES = new Set([ + 'org.springframework', 'org.springframework.boot', 'org.springframework.data', + 'org.springframework.security', 'io.quarkus', 'io.micronaut', 'com.fasterxml.jackson.core', + 'com.fasterxml.jackson.databind', 'org.hibernate.orm', 'org.hibernate', 'jakarta.validation', + 'org.hibernate.validator', 'org.slf4j', 'ch.qos.logback', 'org.apache.logging.log4j', + 'org.junit.jupiter', 'org.mockito', 'org.assertj', 'org.testcontainers', + 'io.github.resilience4j', 'com.google.guava', 'org.apache.commons', 'org.projectlombok', + 'org.mapstruct', 'io.jsonwebtoken', +]); + +export async function analyze(repo: string): Promise { + const rootPom = await readPom(path.join(repo, 'pom.xml')); + if (!rootPom) { + return { buildSystem: 'gradle-groovy', rootGroupId: '', modules: [], unmappedDependencies: [], parseWarnings: ['gradle support deferred to M1; M0 only handles Maven'] }; + } + const rootGroupId = String(rootPom.groupId ?? ''); + const moduleNames: string[] = ([] as string[]) + .concat(toArray(rootPom.modules?.module)) + .map((m: unknown) => String(m)); + const modules: ModuleInfo[] = []; + for (const name of moduleNames) { + const modPom = await readPom(path.join(repo, name, 'pom.xml')); + if (!modPom) continue; + const deps = toArray(modPom.dependencies?.dependency).map((d: any) => ({ + groupId: String(d.groupId), + artifactId: String(d.artifactId), + scope: String(d.scope ?? 'compile'), + })); + modules.push({ + path: name, + artifactId: String(modPom.artifactId), + packaging: String(modPom.packaging ?? 'jar'), + dependencies: deps, + detectedFramework: detectFramework(deps), + }); + } + const unmapped: Record = {}; + for (const m of modules) { + for (const d of m.dependencies) { + if (d.groupId === rootGroupId) continue; + if (isKnown(d.groupId)) continue; + const key = `${d.groupId}:${d.artifactId}`; + (unmapped[key] ??= []).push(m.path); + } + } + return { + buildSystem: 'maven', + rootGroupId, + modules, + unmappedDependencies: Object.entries(unmapped).map(([k, usedBy]) => { + const [groupId, artifactId] = k.split(':') as [string, string]; + return { groupId, artifactId, usedBy }; + }), + parseWarnings: [], + }; +} + +function detectFramework(deps: { groupId: string; artifactId: string }[]): ModuleInfo['detectedFramework'] { + for (const d of deps) { + if (d.groupId === 'org.springframework.boot') return 'spring-boot'; + if (d.groupId.startsWith('io.quarkus')) return 'quarkus'; + if (d.groupId.startsWith('io.micronaut')) return 'micronaut'; + if (d.groupId === 'org.springframework' && d.artifactId === 'spring-webmvc') return 'spring-mvc'; + } + return null; +} + +function isKnown(groupId: string): boolean { + if (KNOWN_GROUP_PREFIXES.has(groupId)) return true; + for (const pref of KNOWN_GROUP_PREFIXES) { + if (groupId.startsWith(pref + '.')) return true; + } + return false; +} + +async function readPom(file: string): Promise { + try { + const xml = await fs.readFile(file, 'utf8'); + const parser = new XMLParser({ ignoreAttributes: true, parseTagValue: false }); + const doc = parser.parse(xml); + return doc.project ?? null; + } catch (e: any) { + if (e.code === 'ENOENT') return null; + throw e; + } +} + +function toArray(v: T | T[] | undefined): T[] { + if (v === undefined) return []; + return Array.isArray(v) ? v : [v]; +} + +export type ScaffoldPlan = { + packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun'; + runtime: 'node' | 'bun' | 'deno'; + rootName: string; + modules: { + path: string; + name: string; + tsFramework: 'express' | 'koa' | 'hono' | 'restify'; + dependencies: Record; + devDependencies: Record; + }[]; +}; + +const SUPPORTED_PM = new Set(['npm', 'pnpm', 'yarn', 'bun']); +const SUPPORTED_RUNTIME = new Set(['node', 'bun', 'deno']); +const SUPPORTED_FRAMEWORK = new Set(['express', 'koa', 'hono', 'restify']); + +export async function scaffold(plan: ScaffoldPlan, outDir: string): Promise { + if (!SUPPORTED_PM.has(plan.packageManager)) throw new Error(`unsupported packageManager: ${plan.packageManager}`); + if (!SUPPORTED_RUNTIME.has(plan.runtime)) throw new Error(`unsupported runtime: ${plan.runtime}`); + for (const m of plan.modules) { + if (!SUPPORTED_FRAMEWORK.has(m.tsFramework)) throw new Error(`unsupported tsFramework: ${m.tsFramework}`); + } + await fs.mkdir(outDir, { recursive: true }); + await writeRootPackageJson(plan, outDir); + await writeTsconfigBase(outDir); + await writeGitignore(outDir); + for (const m of plan.modules) { + await fs.mkdir(path.join(outDir, m.path), { recursive: true }); + await writeModulePackageJson(plan, m, outDir); + await writeModuleTsconfig(m, outDir); + } +} + +async function writeRootPackageJson(plan: ScaffoldPlan, outDir: string): Promise { + const root = { + name: plan.rootName, + private: true, + workspaces: plan.modules.map((m) => m.path), + scripts: { + test: 'vitest run', + typecheck: 'tsc --noEmit -p tsconfig.base.json', + }, + }; + await fs.writeFile(path.join(outDir, 'package.json'), JSON.stringify(root, null, 2) + '\n'); +} + +async function writeModulePackageJson( + plan: ScaffoldPlan, + m: ScaffoldPlan['modules'][number], + outDir: string +): Promise { + const pkg = { + name: m.name, + private: true, + type: 'module', + main: 'dist/index.js', + scripts: { + build: 'tsc', + start: plan.runtime === 'node' ? 'node dist/index.js' : `${plan.runtime} dist/index.js`, + test: 'vitest run', + }, + dependencies: m.dependencies, + devDependencies: m.devDependencies, + }; + await fs.writeFile(path.join(outDir, m.path, 'package.json'), JSON.stringify(pkg, null, 2) + '\n'); +} + +async function writeTsconfigBase(outDir: string): Promise { + const base = { + compilerOptions: { + target: 'ES2022', + module: 'NodeNext', + moduleResolution: 'NodeNext', + strict: true, + noUncheckedIndexedAccess: true, + exactOptionalPropertyTypes: true, + noImplicitOverride: true, + experimentalDecorators: true, + emitDecoratorMetadata: true, + useDefineForClassFields: false, + isolatedModules: true, + esModuleInterop: true, + forceConsistentCasingInFileNames: true, + skipLibCheck: false, + outDir: 'dist', + rootDir: 'src', + }, + }; + await fs.writeFile(path.join(outDir, 'tsconfig.base.json'), JSON.stringify(base, null, 2) + '\n'); +} + +async function writeModuleTsconfig(m: ScaffoldPlan['modules'][number], outDir: string): Promise { + const tsc = { + extends: '../tsconfig.base.json', + include: ['src/**/*'], + exclude: ['dist', 'node_modules', '**/*.test.ts'], + }; + await fs.writeFile(path.join(outDir, m.path, 'tsconfig.json'), JSON.stringify(tsc, null, 2) + '\n'); +} + +async function writeGitignore(outDir: string): Promise { + const lines = [ + 'node_modules/', + 'dist/', + '.vitest-cache/', + 'coverage/', + '', + '# migration artifacts (skill-generated; never commit)', + 'migration/', + '', + ]; + await fs.writeFile(path.join(outDir, '.gitignore'), lines.join('\n')); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch((e: unknown) => { + const msg = e instanceof Error ? e.message : String(e); + process.stderr.write(msg + '\n'); + process.exit(1); + }); +} + +async function main(): Promise { + const { positionals, values } = parseArgs({ + args: process.argv.slice(2), + options: { repo: { type: 'string' }, plan: { type: 'string' }, out: { type: 'string' } }, + allowPositionals: true, + }); + const sub = positionals[0]; + if (sub === 'analyze') { + if (!values.repo) throw new Error('analyze requires --repo '); + const result = await analyze(values.repo); + const outFile = path.join(values.repo, 'migration', 'analysis.json'); + await fs.mkdir(path.dirname(outFile), { recursive: true }); + await fs.writeFile(outFile, JSON.stringify(result, null, 2)); + process.stdout.write(`wrote ${outFile}\n`); + } else if (sub === 'scaffold') { + if (!values.plan) throw new Error('scaffold requires --plan '); + if (!values.out) throw new Error('scaffold requires --out '); + const planJson = JSON.parse(await fs.readFile(values.plan, 'utf8')) as ScaffoldPlan; + await scaffold(planJson, values.out); + process.stdout.write(`scaffolded ${values.out}\n`); + } else { + throw new Error(`unknown subcommand: ${sub}`); + } +} diff --git a/skills/java-to-typescript/scripts/record-fixtures.ts b/skills/java-to-typescript/scripts/record-fixtures.ts new file mode 100644 index 0000000..46b6d3c --- /dev/null +++ b/skills/java-to-typescript/scripts/record-fixtures.ts @@ -0,0 +1,89 @@ +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import { parseArgs } from 'node:util'; +import { request, isLoopback, type HttpRequest } from './lib/http.js'; + +export type CorpusEntry = { + name: string; + method: HttpRequest['method']; + path: string; + headers?: Record; + body?: unknown; +}; + +export type FixtureEntry = { + name: string; + request: { method: string; path: string; headers: Record; body: unknown }; + response: { status: number; headers: Record; body: unknown }; + capturedAt: string; + unexpectedStatus?: boolean; +}; + +export async function recordCorpus(baseUrl: string, corpus: CorpusEntry[], outFile: string): Promise { + if (!isLoopback(baseUrl)) { + throw new Error(`refusing non-loopback baseUrl: ${baseUrl} (policy: loopback only)`); + } + await fs.mkdir(path.dirname(outFile), { recursive: true }); + const lines: string[] = []; + for (const entry of corpus) { + const reqHeaders = entry.headers ?? {}; + const httpReq: HttpRequest = { + baseUrl, + method: entry.method, + path: entry.path, + headers: reqHeaders, + }; + if (entry.body !== undefined) httpReq.body = entry.body; + const res = await request(httpReq); + const fixture: FixtureEntry = { + name: entry.name, + request: { + method: entry.method, + path: entry.path, + headers: reqHeaders, + body: entry.body ?? null, + }, + response: { status: res.status, headers: res.headers, body: res.body }, + capturedAt: new Date().toISOString(), + }; + if (res.status >= 400) fixture.unexpectedStatus = true; + lines.push(JSON.stringify(fixture)); + } + await fs.writeFile(outFile, lines.length === 0 ? '' : lines.join('\n') + '\n'); +} + +async function readCorpus(file: string): Promise { + const text = await fs.readFile(file, 'utf8'); + return text + .trim() + .split('\n') + .filter((l) => l.length > 0) + .map((l) => JSON.parse(l) as CorpusEntry); +} + +async function main(): Promise { + const { values } = parseArgs({ + args: process.argv.slice(2), + options: { + 'java-base': { type: 'string' }, + corpus: { type: 'string' }, + out: { type: 'string' }, + }, + }); + const javaBase = values['java-base']; + const corpusFile = values.corpus; + const outFile = values.out; + if (!javaBase || !corpusFile || !outFile) { + throw new Error('usage: record-fixtures --java-base --corpus --out '); + } + const corpus = await readCorpus(corpusFile); + await recordCorpus(javaBase, corpus, outFile); + process.stdout.write(`recorded ${corpus.length} fixture(s) to ${outFile}\n`); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch((e: unknown) => { + process.stderr.write(`${e instanceof Error ? e.message : String(e)}\n`); + process.exit(1); + }); +} diff --git a/skills/java-to-typescript/scripts/replay-fixtures.ts b/skills/java-to-typescript/scripts/replay-fixtures.ts new file mode 100644 index 0000000..0c30557 --- /dev/null +++ b/skills/java-to-typescript/scripts/replay-fixtures.ts @@ -0,0 +1,151 @@ +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import { parseArgs } from 'node:util'; +import { request, isLoopback, type HttpRequest } from './lib/http.js'; +import { diff, type Allowlist, type Diff } from './lib/json-diff.js'; +import type { FixtureEntry } from './record-fixtures.js'; + +export type ReplayResult = { + totalFixtures: number; + passed: number; + failed: number; + results: Array<{ + name: string; + pass: boolean; + statusExpected: number; + statusActual: number; + diffs: Diff[]; + }>; +}; + +type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue }; + +export async function replayFixtures( + baseUrl: string, + fixturesFile: string, + allowlistFile: string, + reportMdFile: string, + reportJsonFile: string, +): Promise { + if (!isLoopback(baseUrl)) { + throw new Error(`refusing non-loopback baseUrl: ${baseUrl} (policy: loopback only)`); + } + const fixtures = await readFixtures(fixturesFile); + const allow = JSON.parse(await fs.readFile(allowlistFile, 'utf8')) as Allowlist; + const headerAllow = new Set((allow.headers ?? []).map((h) => h.toLowerCase())); + const result: ReplayResult = { totalFixtures: fixtures.length, passed: 0, failed: 0, results: [] }; + for (const fx of fixtures) { + const httpReq: HttpRequest = { + baseUrl, + method: fx.request.method as HttpRequest['method'], + path: fx.request.path, + headers: fx.request.headers ?? {}, + }; + if (fx.request.body !== undefined && fx.request.body !== null) { + httpReq.body = fx.request.body; + } + const res = await request(httpReq); + const statusMatches = fx.response.status === res.status; + const bodyDiffs = diff(fx.response.body as JsonValue, res.body as JsonValue, allow); + const headerDiffs = diffHeaders(fx.response.headers, res.headers, headerAllow); + const allDiffs = [...bodyDiffs, ...headerDiffs]; + const pass = statusMatches && allDiffs.length === 0; + if (pass) result.passed += 1; + else result.failed += 1; + result.results.push({ + name: fx.name, + pass, + statusExpected: fx.response.status, + statusActual: res.status, + diffs: allDiffs, + }); + } + await fs.mkdir(path.dirname(reportMdFile), { recursive: true }); + await fs.writeFile(reportMdFile, renderMarkdown(result)); + await fs.mkdir(path.dirname(reportJsonFile), { recursive: true }); + await fs.writeFile(reportJsonFile, JSON.stringify(result, null, 2)); + return result; +} + +function diffHeaders( + exp: Record, + act: Record, + allow: Set, +): Diff[] { + // One-way check: only verify headers present in the expected fixture. + // Additional headers added by the server (date, content-length, etc.) are ignored. + const out: Diff[] = []; + const actLower: Record = {}; + for (const [k, v] of Object.entries(act)) actLower[k.toLowerCase()] = v; + for (const [k, e] of Object.entries(exp)) { + const lower = k.toLowerCase(); + if (allow.has(lower)) continue; + const a = actLower[lower]; + if (e !== a) out.push({ path: `headers.${lower}`, expected: e, actual: a }); + } + return out; +} + +function renderMarkdown(r: ReplayResult): string { + const lines = [ + `# Replay report`, + ``, + `- Total fixtures: ${r.totalFixtures}`, + `- Passed: ${r.passed}`, + `- Failed: ${r.failed}`, + ``, + `| Fixture | Status | Diffs |`, + `|---|---|---|`, + ]; + for (const x of r.results) { + const mark = x.pass ? 'PASS' : 'FAIL'; + lines.push(`| ${x.name} | ${mark} (${x.statusActual} vs ${x.statusExpected}) | ${x.diffs.length} |`); + } + for (const x of r.results) { + if (!x.pass) { + lines.push(``, `## ${x.name}`, ``, '```json'); + lines.push(JSON.stringify(x.diffs, null, 2)); + lines.push('```'); + } + } + return lines.join('\n') + '\n'; +} + +async function readFixtures(file: string): Promise { + const text = await fs.readFile(file, 'utf8'); + return text + .trim() + .split('\n') + .filter((l) => l.length > 0) + .map((l) => JSON.parse(l) as FixtureEntry); +} + +async function main(): Promise { + const { values } = parseArgs({ + args: process.argv.slice(2), + options: { + 'ts-base': { type: 'string' }, + fixtures: { type: 'string' }, + allowlist: { type: 'string' }, + report: { type: 'string' }, + }, + }); + const tsBase = values['ts-base']; + const fixturesFile = values.fixtures; + const allowlistFile = values.allowlist; + const reportFile = values.report; + if (!tsBase || !fixturesFile || !allowlistFile || !reportFile) { + throw new Error('usage: replay-fixtures --ts-base --fixtures --allowlist --report '); + } + const reportJson = reportFile.replace(/\.md$/, '.json'); + const r = await replayFixtures(tsBase, fixturesFile, allowlistFile, reportFile, reportJson); + process.stdout.write(`replay: ${r.passed}/${r.totalFixtures} passed\n`); + if (r.failed > 0) process.exit(1); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch((e: unknown) => { + process.stderr.write(`${e instanceof Error ? e.message : String(e)}\n`); + process.exit(1); + }); +} diff --git a/skills/java-to-typescript/tsconfig.json b/skills/java-to-typescript/tsconfig.json new file mode 100644 index 0000000..80fede8 --- /dev/null +++ b/skills/java-to-typescript/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "isolatedModules": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "allowJs": false, + "noEmit": true, + "types": ["node"] + }, + "include": ["scripts/**/*.ts", "evals/**/*.ts", "vitest.config.ts"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "scripts/lib/fast-xml-parser/**", "evals/fixtures/**/sample/**"] +} diff --git a/skills/java-to-typescript/vitest.config.ts b/skills/java-to-typescript/vitest.config.ts new file mode 100644 index 0000000..26be72f --- /dev/null +++ b/skills/java-to-typescript/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['scripts/**/*.test.ts', 'evals/**/*.test.ts'], + exclude: ['**/node_modules/**', 'evals/fixtures/**/sample/**'], + environment: 'node', + testTimeout: 10_000, + cache: { dir: '.vitest-cache' }, + }, +});