From bed49bb03068838c8dfe666362015899a4c4b2e3 Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Sun, 2 Aug 2026 15:24:40 -0500 Subject: [PATCH 01/11] fix(deps): clear the remaining npm advisories docs: pnpm.overrides for postcss ^8.5.18, sharp ^0.35.0, esbuild ^0.28.1. Overrides rather than 'pnpm update' because all three were transitive and pinned below the fix by their parents' ranges - postcss in particular had a vulnerable 8.4.31 alongside a patched 8.5.25. shell: dompurify ^3.4.12 (17 alerts), vite 5.4.21 -> 6.4.3, esbuild -> 0.25.x, react-router-dom 6.30.4 -> 7.18.2. vite and esbuild also needed overrides - vitest's vite-node held vite 5 even after the direct bump. Two majors here, so verified rather than assumed: 127 tests across 11 files pass and 'pnpm build' succeeds on both. react-router 7 needed no source changes - all 18 react-router-dom imports still resolve through its v7 re-export shim. The dashboard's tracked dist/ is deliberately untouched. --- docs/package.json | 7 + docs/pnpm-lock.yaml | 546 +++++++++--------- .../dashboard/contract/shell/package.json | 11 +- .../dashboard/contract/shell/pnpm-lock.yaml | 395 +++++++------ 4 files changed, 527 insertions(+), 432 deletions(-) diff --git a/docs/package.json b/docs/package.json index fed0ccaa..94436c0e 100644 --- a/docs/package.json +++ b/docs/package.json @@ -51,5 +51,12 @@ "postcss": "^8.5.25", "tailwindcss": "^4.3.3", "typescript": "^5.9.3" + }, + "pnpm": { + "overrides": { + "postcss": "^8.5.18", + "sharp": "^0.35.0", + "esbuild": "^0.28.1" + } } } diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index 85a0dcb3..20f94082 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -4,6 +4,11 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + postcss: ^8.5.18 + sharp: ^0.35.0 + esbuild: ^0.28.1 + importers: .: @@ -25,16 +30,16 @@ importers: version: 12.43.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) fumadocs-core: specifier: 16.6.0 - version: 16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3) + version: 16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3) fumadocs-mdx: specifier: 14.2.7 - version: 14.2.7(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.18)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(mdast-util-mdx-jsx@3.2.0)(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) + version: 14.2.7(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.18)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(mdast-util-mdx-jsx@3.2.0)(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) fumadocs-twoslash: specifier: ^3.3.0 - version: 3.3.0(@types/react@19.2.18)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(fumadocs-ui@16.6.0(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@3.23.0)(typescript@5.9.3) + version: 3.3.0(@types/react@19.2.18)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(fumadocs-ui@16.6.0(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@3.23.0)(typescript@5.9.3) fumadocs-ui: specifier: 16.6.0 - version: 16.6.0(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3) + version: 16.6.0(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3) hast: specifier: ^1.0.0 version: 1.0.0 @@ -49,7 +54,7 @@ importers: version: 11.16.0 next: specifier: 16.2.12 - version: 16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -112,7 +117,7 @@ importers: specifier: ^0.181.0 version: 0.181.0 postcss: - specifier: ^8.5.25 + specifier: ^8.5.18 version: 8.5.25 tailwindcss: specifier: ^4.3.3 @@ -226,158 +231,158 @@ packages: '@emnapi/runtime@1.11.3': resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -421,136 +426,145 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-darwin-arm64@0.35.3': + resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-darwin-x64@0.35.3': + resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + '@img/sharp-freebsd-wasm32@0.35.3': + resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.2': + resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + '@img/sharp-libvips-darwin-x64@1.3.2': + resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + '@img/sharp-libvips-linux-arm64@1.3.2': + resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + '@img/sharp-libvips-linux-arm@1.3.2': + resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} cpu: [arm] os: [linux] - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + '@img/sharp-libvips-linux-ppc64@1.3.2': + resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} cpu: [ppc64] os: [linux] - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + '@img/sharp-libvips-linux-riscv64@1.3.2': + resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} cpu: [riscv64] os: [linux] - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + '@img/sharp-libvips-linux-s390x@1.3.2': + resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + '@img/sharp-libvips-linux-x64@1.3.2': + resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + '@img/sharp-libvips-linuxmusl-x64@1.3.2': + resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-arm64@0.35.3': + resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-arm@0.35.3': + resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} + engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-ppc64@0.35.3': + resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} + engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-riscv64@0.35.3': + resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} + engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-s390x@0.35.3': + resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} + engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-x64@0.35.3': + resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linuxmusl-arm64@0.35.3': + resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linuxmusl-x64@0.35.3': + resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-wasm32@0.35.3': + resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.3': + resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} + engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-arm64@0.35.3': + resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-ia32@0.35.3': + resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} + engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-x64@0.35.3': + resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] @@ -1812,8 +1826,8 @@ packages: esast-util-from-js@2.0.1: resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true @@ -2507,10 +2521,6 @@ packages: resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} engines: {node: '>=4'} - postcss@8.4.31: - resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.25: resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} engines: {node: ^10 || ^12 || >=14} @@ -2659,9 +2669,14 @@ packages: engines: {node: '>=10'} hasBin: true - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.35.3: + resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} @@ -3012,82 +3027,82 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.27.7': + '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/android-arm64@0.27.7': + '@esbuild/android-arm64@0.28.1': optional: true - '@esbuild/android-arm@0.27.7': + '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/android-x64@0.27.7': + '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/darwin-arm64@0.27.7': + '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/darwin-x64@0.27.7': + '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/freebsd-arm64@0.27.7': + '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/freebsd-x64@0.27.7': + '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/linux-arm64@0.27.7': + '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/linux-arm@0.27.7': + '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/linux-ia32@0.27.7': + '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/linux-loong64@0.27.7': + '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/linux-mips64el@0.27.7': + '@esbuild/linux-mips64el@0.28.1': optional: true - '@esbuild/linux-ppc64@0.27.7': + '@esbuild/linux-ppc64@0.28.1': optional: true - '@esbuild/linux-riscv64@0.27.7': + '@esbuild/linux-riscv64@0.28.1': optional: true - '@esbuild/linux-s390x@0.27.7': + '@esbuild/linux-s390x@0.28.1': optional: true - '@esbuild/linux-x64@0.27.7': + '@esbuild/linux-x64@0.28.1': optional: true - '@esbuild/netbsd-arm64@0.27.7': + '@esbuild/netbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.27.7': + '@esbuild/netbsd-x64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.27.7': + '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.27.7': + '@esbuild/openbsd-x64@0.28.1': optional: true - '@esbuild/openharmony-arm64@0.27.7': + '@esbuild/openharmony-arm64@0.28.1': optional: true - '@esbuild/sunos-x64@0.27.7': + '@esbuild/sunos-x64@0.28.1': optional: true - '@esbuild/win32-arm64@0.27.7': + '@esbuild/win32-arm64@0.28.1': optional: true - '@esbuild/win32-ia32@0.27.7': + '@esbuild/win32-ia32@0.28.1': optional: true - '@esbuild/win32-x64@0.27.7': + '@esbuild/win32-x64@0.28.1': optional: true '@floating-ui/core@1.8.0': @@ -3130,98 +3145,108 @@ snapshots: '@img/colour@1.1.0': optional: true - '@img/sharp-darwin-arm64@0.34.5': + '@img/sharp-darwin-arm64@0.35.3': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-arm64': 1.3.2 optional: true - '@img/sharp-darwin-x64@0.34.5': + '@img/sharp-darwin-x64@0.35.3': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.3.2 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 optional: true - '@img/sharp-libvips-darwin-arm64@1.2.4': + '@img/sharp-libvips-darwin-arm64@1.3.2': optional: true - '@img/sharp-libvips-darwin-x64@1.2.4': + '@img/sharp-libvips-darwin-x64@1.3.2': optional: true - '@img/sharp-libvips-linux-arm64@1.2.4': + '@img/sharp-libvips-linux-arm64@1.3.2': optional: true - '@img/sharp-libvips-linux-arm@1.2.4': + '@img/sharp-libvips-linux-arm@1.3.2': optional: true - '@img/sharp-libvips-linux-ppc64@1.2.4': + '@img/sharp-libvips-linux-ppc64@1.3.2': optional: true - '@img/sharp-libvips-linux-riscv64@1.2.4': + '@img/sharp-libvips-linux-riscv64@1.3.2': optional: true - '@img/sharp-libvips-linux-s390x@1.2.4': + '@img/sharp-libvips-linux-s390x@1.3.2': optional: true - '@img/sharp-libvips-linux-x64@1.2.4': + '@img/sharp-libvips-linux-x64@1.3.2': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.4': + '@img/sharp-libvips-linuxmusl-x64@1.3.2': optional: true - '@img/sharp-linux-arm64@0.34.5': + '@img/sharp-linux-arm64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.3.2 optional: true - '@img/sharp-linux-arm@0.34.5': + '@img/sharp-linux-arm@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.3.2 optional: true - '@img/sharp-linux-ppc64@0.34.5': + '@img/sharp-linux-ppc64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.3.2 optional: true - '@img/sharp-linux-riscv64@0.34.5': + '@img/sharp-linux-riscv64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.3.2 optional: true - '@img/sharp-linux-s390x@0.34.5': + '@img/sharp-linux-s390x@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.3.2 optional: true - '@img/sharp-linux-x64@0.34.5': + '@img/sharp-linux-x64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.3.2 optional: true - '@img/sharp-linuxmusl-arm64@0.34.5': + '@img/sharp-linuxmusl-arm64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 optional: true - '@img/sharp-linuxmusl-x64@0.34.5': + '@img/sharp-linuxmusl-x64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 optional: true - '@img/sharp-wasm32@0.34.5': + '@img/sharp-wasm32@0.35.3': dependencies: '@emnapi/runtime': 1.11.3 optional: true - '@img/sharp-win32-arm64@0.34.5': + '@img/sharp-webcontainers-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 + optional: true + + '@img/sharp-win32-arm64@0.35.3': optional: true - '@img/sharp-win32-ia32@0.34.5': + '@img/sharp-win32-ia32@0.35.3': optional: true - '@img/sharp-win32-x64@0.34.5': + '@img/sharp-win32-x64@0.35.3': optional: true '@jridgewell/gen-mapping@0.3.13': @@ -4567,34 +4592,34 @@ snapshots: esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 - esbuild@0.27.7: + esbuild@0.28.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.7 - '@esbuild/android-arm': 0.27.7 - '@esbuild/android-arm64': 0.27.7 - '@esbuild/android-x64': 0.27.7 - '@esbuild/darwin-arm64': 0.27.7 - '@esbuild/darwin-x64': 0.27.7 - '@esbuild/freebsd-arm64': 0.27.7 - '@esbuild/freebsd-x64': 0.27.7 - '@esbuild/linux-arm': 0.27.7 - '@esbuild/linux-arm64': 0.27.7 - '@esbuild/linux-ia32': 0.27.7 - '@esbuild/linux-loong64': 0.27.7 - '@esbuild/linux-mips64el': 0.27.7 - '@esbuild/linux-ppc64': 0.27.7 - '@esbuild/linux-riscv64': 0.27.7 - '@esbuild/linux-s390x': 0.27.7 - '@esbuild/linux-x64': 0.27.7 - '@esbuild/netbsd-arm64': 0.27.7 - '@esbuild/netbsd-x64': 0.27.7 - '@esbuild/openbsd-arm64': 0.27.7 - '@esbuild/openbsd-x64': 0.27.7 - '@esbuild/openharmony-arm64': 0.27.7 - '@esbuild/sunos-x64': 0.27.7 - '@esbuild/win32-arm64': 0.27.7 - '@esbuild/win32-ia32': 0.27.7 - '@esbuild/win32-x64': 0.27.7 + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 escape-string-regexp@5.0.0: {} @@ -4654,7 +4679,7 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3): + fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3): dependencies: '@formatjs/intl-localematcher': 0.8.13 '@orama/orama': 3.1.18 @@ -4686,21 +4711,21 @@ snapshots: '@types/mdast': 4.0.4 '@types/react': 19.2.18 lucide-react: 0.563.0(react@19.2.8) - next: 16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + next: 16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: 19.2.8 react-dom: 19.2.8(react@19.2.8) zod: 4.4.3 transitivePeerDependencies: - supports-color - fumadocs-mdx@14.2.7(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.18)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(mdast-util-mdx-jsx@3.2.0)(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8): + fumadocs-mdx@14.2.7(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.18)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(mdast-util-mdx-jsx@3.2.0)(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 chokidar: 5.0.0 - esbuild: 0.27.7 + esbuild: 0.28.1 estree-util-value-to-estree: 3.5.0 - fumadocs-core: 16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3) + fumadocs-core: 16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3) js-yaml: 4.3.1 mdast-util-to-markdown: 2.1.2 picocolors: 1.1.1 @@ -4718,18 +4743,18 @@ snapshots: '@types/mdx': 2.0.14 '@types/react': 19.2.18 mdast-util-mdx-jsx: 3.2.0 - next: 16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + next: 16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: 19.2.8 transitivePeerDependencies: - supports-color - fumadocs-twoslash@3.3.0(@types/react@19.2.18)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(fumadocs-ui@16.6.0(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@3.23.0)(typescript@5.9.3): + fumadocs-twoslash@3.3.0(@types/react@19.2.18)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(fumadocs-ui@16.6.0(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(shiki@3.23.0)(typescript@5.9.3): dependencies: '@base-ui/react': 1.6.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@shikijs/twoslash': 4.4.1(typescript@5.9.3) cnfast: 0.0.8 - fumadocs-core: 16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3) - fumadocs-ui: 16.6.0(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3) + fumadocs-core: 16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3) + fumadocs-ui: 16.6.0(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3) mdast-util-from-markdown: 2.0.3 mdast-util-gfm: 3.1.0 mdast-util-to-hast: 13.2.1 @@ -4745,7 +4770,7 @@ snapshots: - supports-color - typescript - fumadocs-ui@16.6.0(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3): + fumadocs-ui@16.6.0(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(fumadocs-core@16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3): dependencies: '@fumadocs/tailwind': 0.0.2(tailwindcss@4.3.3) '@radix-ui/react-accordion': 1.2.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -4759,7 +4784,7 @@ snapshots: '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) '@radix-ui/react-tabs': 1.1.21(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) class-variance-authority: 0.7.1 - fumadocs-core: 16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3) + fumadocs-core: 16.6.0(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@0.563.0(react@19.2.8))(next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3) lucide-react: 0.563.0(react@19.2.8) motion: 12.43.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) next-themes: 0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -4773,7 +4798,7 @@ snapshots: unist-util-visit: 5.1.0 optionalDependencies: '@types/react': 19.2.18 - next: 16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + next: 16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) transitivePeerDependencies: - '@emotion/is-prop-valid' - '@types/react-dom' @@ -5525,13 +5550,13 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - next@16.2.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + next@16.2.12(@types/node@25.9.5)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: '@next/env': 16.2.12 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.11.10 caniuse-lite: 1.0.30001806 - postcss: 8.4.31 + postcss: 8.5.25 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) styled-jsx: 5.1.6(react@19.2.8) @@ -5544,9 +5569,10 @@ snapshots: '@next/swc-linux-x64-musl': 16.2.12 '@next/swc-win32-arm64-msvc': 16.2.12 '@next/swc-win32-x64-msvc': 16.2.12 - sharp: 0.34.5 + sharp: 0.35.3(@types/node@25.9.5) transitivePeerDependencies: - '@babel/core' + - '@types/node' - babel-plugin-macros npm-to-yarn@3.2.0: {} @@ -5611,12 +5637,6 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss@8.4.31: - dependencies: - nanoid: 3.3.16 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.25: dependencies: nanoid: 3.3.16 @@ -5808,36 +5828,38 @@ snapshots: semver@7.8.5: optional: true - sharp@0.34.5: + sharp@0.35.3(@types/node@25.9.5): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 + '@img/sharp-darwin-arm64': 0.35.3 + '@img/sharp-darwin-x64': 0.35.3 + '@img/sharp-freebsd-wasm32': 0.35.3 + '@img/sharp-libvips-darwin-arm64': 1.3.2 + '@img/sharp-libvips-darwin-x64': 1.3.2 + '@img/sharp-libvips-linux-arm': 1.3.2 + '@img/sharp-libvips-linux-arm64': 1.3.2 + '@img/sharp-libvips-linux-ppc64': 1.3.2 + '@img/sharp-libvips-linux-riscv64': 1.3.2 + '@img/sharp-libvips-linux-s390x': 1.3.2 + '@img/sharp-libvips-linux-x64': 1.3.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + '@img/sharp-linux-arm': 0.35.3 + '@img/sharp-linux-arm64': 0.35.3 + '@img/sharp-linux-ppc64': 0.35.3 + '@img/sharp-linux-riscv64': 0.35.3 + '@img/sharp-linux-s390x': 0.35.3 + '@img/sharp-linux-x64': 0.35.3 + '@img/sharp-linuxmusl-arm64': 0.35.3 + '@img/sharp-linuxmusl-x64': 0.35.3 + '@img/sharp-webcontainers-wasm32': 0.35.3 + '@img/sharp-win32-arm64': 0.35.3 + '@img/sharp-win32-ia32': 0.35.3 + '@img/sharp-win32-x64': 0.35.3 + '@types/node': 25.9.5 optional: true shebang-command@2.0.0: diff --git a/extensions/dashboard/contract/shell/package.json b/extensions/dashboard/contract/shell/package.json index 00bc4115..4b671c2e 100644 --- a/extensions/dashboard/contract/shell/package.json +++ b/extensions/dashboard/contract/shell/package.json @@ -30,7 +30,7 @@ "react-dom": "^18.3.1", "react-hook-form": "^7.84.0", "react-resizable-panels": "^2.1.9", - "react-router-dom": "^6.30.4", + "react-router-dom": "^7.18.2", "recharts": "^2.15.4", "tailwind-merge": "^3.6.0", "tailwindcss-animate": "^1.0.7", @@ -50,7 +50,14 @@ "prettier": "^3.9.6", "tailwindcss": "^3.4.19", "typescript": "^5.9.3", - "vite": "^5.4.21", + "vite": "^6.4.3", "vitest": "^3.2.7" + }, + "pnpm": { + "overrides": { + "dompurify": "^3.4.12", + "vite": "^6.4.3", + "esbuild": "^0.25.0" + } } } diff --git a/extensions/dashboard/contract/shell/pnpm-lock.yaml b/extensions/dashboard/contract/shell/pnpm-lock.yaml index c3e84f00..3f9887f4 100644 --- a/extensions/dashboard/contract/shell/pnpm-lock.yaml +++ b/extensions/dashboard/contract/shell/pnpm-lock.yaml @@ -4,6 +4,11 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + dompurify: ^3.4.12 + vite: ^6.4.3 + esbuild: ^0.25.0 + importers: .: @@ -60,8 +65,8 @@ importers: specifier: ^2.1.9 version: 2.1.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-router-dom: - specifier: ^6.30.4 - version: 6.30.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^7.18.2 + version: 7.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) recharts: specifier: ^2.15.4 version: 2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -92,7 +97,7 @@ importers: version: 18.3.7(@types/react@18.3.31) '@vitejs/plugin-react': specifier: ^4.7.0 - version: 4.7.0(vite@5.4.21(@types/node@26.1.2)) + version: 4.7.0(vite@6.4.3(@types/node@26.1.2)(jiti@1.21.7)) autoprefixer: specifier: ^10.5.4 version: 10.5.4(postcss@8.5.25) @@ -115,11 +120,11 @@ importers: specifier: ^5.9.3 version: 5.9.3 vite: - specifier: ^5.4.21 - version: 5.4.21(@types/node@26.1.2) + specifier: ^6.4.3 + version: 6.4.3(@types/node@26.1.2)(jiti@1.21.7) vitest: specifier: ^3.2.7 - version: 3.2.7(@types/node@26.1.2)(jsdom@25.0.1)(msw@2.15.0(@types/node@26.1.2)(typescript@5.9.3)) + version: 3.2.7(@types/node@26.1.2)(jiti@1.21.7)(jsdom@25.0.1)(msw@2.15.0(@types/node@26.1.2)(typescript@5.9.3)) packages: @@ -290,141 +295,159 @@ packages: peerDependencies: react: '>=16.8.0' - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -634,10 +657,6 @@ packages: '@types/react': optional: true - '@remix-run/router@1.23.3': - resolution: {integrity: sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==} - engines: {node: '>=14.0.0'} - '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -898,7 +917,7 @@ packages: resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + vite: ^6.4.3 '@vitest/expect@3.2.7': resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} @@ -907,7 +926,7 @@ packages: resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} peerDependencies: msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + vite: ^6.4.3 peerDependenciesMeta: msw: optional: true @@ -1166,8 +1185,8 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} - dompurify@3.2.7: - resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} + dompurify@3.4.12: + resolution: {integrity: sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==} dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} @@ -1202,9 +1221,9 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} hasBin: true escalade@3.2.0: @@ -1657,18 +1676,22 @@ packages: react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - react-router-dom@6.30.4: - resolution: {integrity: sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==} - engines: {node: '>=14.0.0'} + react-router-dom@7.18.2: + resolution: {integrity: sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==} + engines: {node: '>=20.0.0'} peerDependencies: - react: '>=16.8' - react-dom: '>=16.8' + react: '>=18' + react-dom: '>=18' - react-router@6.30.4: - resolution: {integrity: sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==} - engines: {node: '>=14.0.0'} + react-router@7.18.2: + resolution: {integrity: sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==} + engines: {node: '>=20.0.0'} peerDependencies: - react: '>=16.8' + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true react-smooth@4.0.4: resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} @@ -1755,6 +1778,9 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-cookie-parser@3.1.2: resolution: {integrity: sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==} @@ -1937,22 +1963,27 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite@5.4.21: - resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} - engines: {node: ^18.0.0 || >=20.0.0} + vite@6.4.3: + resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' less: '*' lightningcss: ^1.21.0 sass: '*' sass-embedded: '*' stylus: '*' sugarss: '*' - terser: ^5.4.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 peerDependenciesMeta: '@types/node': optional: true + jiti: + optional: true less: optional: true lightningcss: @@ -1967,6 +1998,10 @@ packages: optional: true terser: optional: true + tsx: + optional: true + yaml: + optional: true vitest@3.2.7: resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} @@ -2271,73 +2306,82 @@ snapshots: react: 18.3.1 tslib: 2.8.1 - '@esbuild/aix-ppc64@0.21.5': + '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/android-arm64@0.21.5': + '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm@0.21.5': + '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-x64@0.21.5': + '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/darwin-arm64@0.21.5': + '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/darwin-x64@0.21.5': + '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.21.5': + '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.21.5': + '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/linux-arm64@0.21.5': + '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-arm@0.21.5': + '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-ia32@0.21.5': + '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-loong64@0.21.5': + '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-mips64el@0.21.5': + '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-ppc64@0.21.5': + '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-riscv64@0.21.5': + '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-s390x@0.21.5': + '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/linux-x64@0.21.5': + '@esbuild/linux-x64@0.25.12': optional: true - '@esbuild/netbsd-x64@0.21.5': + '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/openbsd-x64@0.21.5': + '@esbuild/netbsd-x64@0.25.12': optional: true - '@esbuild/sunos-x64@0.21.5': + '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/win32-arm64@0.21.5': + '@esbuild/openbsd-x64@0.25.12': optional: true - '@esbuild/win32-ia32@0.21.5': + '@esbuild/openharmony-arm64@0.25.12': optional: true - '@esbuild/win32-x64@0.21.5': + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': optional: true '@floating-ui/core@1.8.0': @@ -2469,8 +2513,6 @@ snapshots: optionalDependencies: '@types/react': 18.3.31 - '@remix-run/router@1.23.3': {} - '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/rollup-android-arm-eabi@4.62.4': @@ -2683,7 +2725,7 @@ snapshots: '@types/trusted-types@2.0.7': optional: true - '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@26.1.2))': + '@vitejs/plugin-react@4.7.0(vite@6.4.3(@types/node@26.1.2)(jiti@1.21.7))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) @@ -2691,7 +2733,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 5.4.21(@types/node@26.1.2) + vite: 6.4.3(@types/node@26.1.2)(jiti@1.21.7) transitivePeerDependencies: - supports-color @@ -2703,14 +2745,14 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.7(msw@2.15.0(@types/node@26.1.2)(typescript@5.9.3))(vite@5.4.21(@types/node@26.1.2))': + '@vitest/mocker@3.2.7(msw@2.15.0(@types/node@26.1.2)(typescript@5.9.3))(vite@6.4.3(@types/node@26.1.2)(jiti@1.21.7))': dependencies: '@vitest/spy': 3.2.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.15.0(@types/node@26.1.2)(typescript@5.9.3) - vite: 5.4.21(@types/node@26.1.2) + vite: 6.4.3(@types/node@26.1.2)(jiti@1.21.7) '@vitest/pretty-format@3.2.7': dependencies: @@ -2938,7 +2980,7 @@ snapshots: '@babel/runtime': 7.29.7 csstype: 3.2.3 - dompurify@3.2.7: + dompurify@3.4.12: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -2971,31 +3013,34 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.4 - esbuild@0.21.5: + esbuild@0.25.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 + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 escalade@3.2.0: {} @@ -3241,7 +3286,7 @@ snapshots: monaco-editor@0.55.1: dependencies: - dompurify: 3.2.7 + dompurify: 3.4.12 marked: 14.0.0 ms@2.1.3: {} @@ -3401,17 +3446,19 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-router-dom@6.30.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-router-dom@7.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@remix-run/router': 1.23.3 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-router: 6.30.4(react@18.3.1) + react-router: 7.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-router@6.30.4(react@18.3.1): + react-router@7.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@remix-run/router': 1.23.3 + cookie: 1.1.1 react: 18.3.1 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) react-smooth@4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: @@ -3531,6 +3578,8 @@ snapshots: semver@6.3.1: {} + set-cookie-parser@2.7.2: {} + set-cookie-parser@3.1.2: {} siginfo@2.0.0: {} @@ -3715,15 +3764,16 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-node@3.2.4(@types/node@26.1.2): + vite-node@3.2.4(@types/node@26.1.2)(jiti@1.21.7): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 5.4.21(@types/node@26.1.2) + vite: 6.4.3(@types/node@26.1.2)(jiti@1.21.7) transitivePeerDependencies: - '@types/node' + - jiti - less - lightningcss - sass @@ -3732,21 +3782,27 @@ snapshots: - sugarss - supports-color - terser + - tsx + - yaml - vite@5.4.21(@types/node@26.1.2): + vite@6.4.3(@types/node@26.1.2)(jiti@1.21.7): dependencies: - esbuild: 0.21.5 + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 postcss: 8.5.25 rollup: 4.62.4 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 26.1.2 fsevents: 2.3.3 + jiti: 1.21.7 - vitest@3.2.7(@types/node@26.1.2)(jsdom@25.0.1)(msw@2.15.0(@types/node@26.1.2)(typescript@5.9.3)): + vitest@3.2.7(@types/node@26.1.2)(jiti@1.21.7)(jsdom@25.0.1)(msw@2.15.0(@types/node@26.1.2)(typescript@5.9.3)): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.7 - '@vitest/mocker': 3.2.7(msw@2.15.0(@types/node@26.1.2)(typescript@5.9.3))(vite@5.4.21(@types/node@26.1.2)) + '@vitest/mocker': 3.2.7(msw@2.15.0(@types/node@26.1.2)(typescript@5.9.3))(vite@6.4.3(@types/node@26.1.2)(jiti@1.21.7)) '@vitest/pretty-format': 3.2.7 '@vitest/runner': 3.2.7 '@vitest/snapshot': 3.2.7 @@ -3764,13 +3820,14 @@ snapshots: tinyglobby: 0.2.17 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 5.4.21(@types/node@26.1.2) - vite-node: 3.2.4(@types/node@26.1.2) + vite: 6.4.3(@types/node@26.1.2)(jiti@1.21.7) + vite-node: 3.2.4(@types/node@26.1.2)(jiti@1.21.7) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 26.1.2 jsdom: 25.0.1 transitivePeerDependencies: + - jiti - less - lightningcss - msw @@ -3780,6 +3837,8 @@ snapshots: - sugarss - supports-color - terser + - tsx + - yaml w3c-xmlserializer@5.0.0: dependencies: From 640a5af3ebb96adc5ad2e57554e5cfec58cd0922 Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Mon, 3 Aug 2026 10:40:56 -0500 Subject: [PATCH 02/11] fix(client): parse response status codes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The status code was never read. `code` was initialised to zero and the branch meant to parse it was empty, so every response — including 200 — fell through to DefaultError and Endpoint.Responses was always empty. Nothing failed loudly. The TypeScript generator found no 2xx response, correctly typed every method `Promise`, and produced a client that compiled cleanly while discarding every response body. A client whose calls all resolve to void is worse than no client: it typechecks, so nothing tells you the data is gone. Exact codes are applied before class wildcards, because map iteration order is random and a single pass would let a "2XX" overwrite an explicit "200" on some runs and not others. A key that is neither a code, a wildcard, nor "default" is now dropped rather than filed as DefaultError — treating an unparseable status as the error shape is how a typo silently becomes an endpoint's error type. --- internal/client/spec_parser.go | 85 ++++++++++++-- internal/client/spec_parser_test.go | 167 ++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+), 8 deletions(-) diff --git a/internal/client/spec_parser.go b/internal/client/spec_parser.go index 0125b7e7..7f523c66 100644 --- a/internal/client/spec_parser.go +++ b/internal/client/spec_parser.go @@ -8,6 +8,8 @@ import ( "log" "os" "path/filepath" + "sort" + "strconv" "strings" "github.com/xraph/forge/internal/shared" @@ -395,13 +397,27 @@ func convertOperation(method, path string, op *shared.Operation) Endpoint { } } - // Extract responses - for statusCode, resp := range op.Responses { - code := 0 + // Extract responses. + // + // Status codes arrive as strings: an exact code ("200"), the catch-all + // ("default"), or a class wildcard ("2XX"). Exact codes are applied before + // wildcards, so a spec declaring both 200 and 2XX keeps the specific one — + // map iteration order is random, and a single pass would let whichever + // happened to come last win. + // + // A key that is none of these is skipped rather than filed under + // DefaultError. Treating an unparseable status as the default is how a + // typo silently becomes an endpoint's error shape, which is worse than + // dropping it: the generators read DefaultError to type failures. + type pendingResponse struct { + code int + fromWildcard bool + response *Response + } - if statusCode != "default" { - } + pending := make([]pendingResponse, 0, len(op.Responses)) + for statusCode, resp := range op.Responses { response := &Response{ Description: resp.Description, Content: make(map[string]*MediaType), @@ -426,11 +442,36 @@ func convertOperation(method, path string, op *shared.Operation) Endpoint { } } - if code == 0 { + if statusCode == "default" { endpoint.DefaultError = response - } else { - endpoint.Responses[code] = response + + continue + } + + code, wildcard, ok := parseStatusCode(statusCode) + if !ok { + continue + } + + pending = append(pending, pendingResponse{ + code: code, + fromWildcard: wildcard, + response: response, + }) + } + + sort.SliceStable(pending, func(i, j int) bool { + return !pending[i].fromWildcard && pending[j].fromWildcard + }) + + for _, p := range pending { + if p.fromWildcard { + if _, exists := endpoint.Responses[p.code]; exists { + continue + } } + + endpoint.Responses[p.code] = p.response } // Extract security @@ -446,6 +487,34 @@ func convertOperation(method, path string, op *shared.Operation) Endpoint { return endpoint } +// parseStatusCode interprets an OpenAPI response key. +// +// Returns the numeric code, whether it came from a class wildcard, and whether +// it was understood at all. Wildcards ("2XX") normalise to the base of their +// class, which is what the generators' 2xx-range scans already look for; the +// caller keeps an exact code in preference to a wildcard that lands on it. +// +// Codes outside 100-599 are rejected. A response declared as "999" is a +// mistake in the spec, and admitting it would put a body under a status no +// transport will ever produce. +func parseStatusCode(key string) (code int, wildcard bool, ok bool) { + if n, err := strconv.Atoi(key); err == nil { + if n < 100 || n > 599 { + return 0, false, false + } + + return n, false, true + } + + if len(key) == 3 && (key[1] == 'X' || key[1] == 'x') && (key[2] == 'X' || key[2] == 'x') { + if key[0] >= '1' && key[0] <= '5' { + return int(key[0]-'0') * 100, true, true + } + } + + return 0, false, false +} + func convertSchema(s *shared.Schema) *Schema { if s == nil { return nil diff --git a/internal/client/spec_parser_test.go b/internal/client/spec_parser_test.go index 8ced4400..f1900bd9 100644 --- a/internal/client/spec_parser_test.go +++ b/internal/client/spec_parser_test.go @@ -5,6 +5,7 @@ import ( "net/http" "os" "path/filepath" + "sort" "testing" "github.com/xraph/forge/internal/client" @@ -705,3 +706,169 @@ func TestSpecParserAdditionalProperties(t *testing.T) { assertAdditionalPropertiesNormalised(t, spec) }) } + +// TestSpecParserResponseStatusCodes pins the parsing of the response map's +// keys. +// +// The status code was previously never read: `code` was initialised to zero +// and the branch meant to parse it was empty, so every response — including +// 200 — fell through to DefaultError and Endpoint.Responses was always empty. +// Nothing downstream failed loudly. The TypeScript generator simply found no +// 2xx response, typed every method `Promise`, and produced a client that +// compiled cleanly while discarding every response body. +func TestSpecParserResponseStatusCodes(t *testing.T) { + spec := ` +openapi: 3.1.0 +info: + title: Status Codes + version: 1.0.0 +paths: + /exact: + get: + operationId: exact + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Thing' + '404': + description: Missing + default: + description: Error + /wildcard: + get: + operationId: wildcard + responses: + '2XX': + description: Any success + content: + application/json: + schema: + $ref: '#/components/schemas/Thing' + /both: + get: + operationId: both + responses: + '200': + description: Specific + content: + application/json: + schema: + $ref: '#/components/schemas/Thing' + '2XX': + description: Class fallback + /nonsense: + get: + operationId: nonsense + responses: + 'banana': + description: Not a status + '999': + description: Out of range +components: + schemas: + Thing: + type: object + properties: + id: + type: string +` + + dir := t.TempDir() + path := filepath.Join(dir, "spec.yaml") + + if err := os.WriteFile(path, []byte(spec), 0o600); err != nil { + t.Fatalf("write spec: %v", err) + } + + parsed, err := client.NewSpecParser().ParseFile(context.Background(), path) + if err != nil { + t.Fatalf("parse: %v", err) + } + + byPath := make(map[string]*client.Endpoint, len(parsed.Endpoints)) + + for i := range parsed.Endpoints { + byPath[parsed.Endpoints[i].Path] = &parsed.Endpoints[i] + } + + t.Run("exact codes reach Responses", func(t *testing.T) { + ep := byPath["/exact"] + if ep == nil { + t.Fatal("endpoint /exact missing") + } + + ok, found := ep.Responses[http.StatusOK] + if !found { + t.Fatalf("no 200 response; got codes %v", codesOf(ep)) + } + + if ok.Content["application/json"] == nil { + t.Fatal("200 lost its JSON content") + } + + if _, found := ep.Responses[http.StatusNotFound]; !found { + t.Errorf("no 404 response; got codes %v", codesOf(ep)) + } + + if ep.DefaultError == nil { + t.Error("default response should still land in DefaultError") + } + }) + + t.Run("class wildcards normalise to the base of the class", func(t *testing.T) { + ep := byPath["/wildcard"] + if ep == nil { + t.Fatal("endpoint /wildcard missing") + } + + if _, found := ep.Responses[http.StatusOK]; !found { + t.Fatalf("2XX did not normalise to 200; got codes %v", codesOf(ep)) + } + }) + + t.Run("an exact code beats a wildcard landing on it", func(t *testing.T) { + ep := byPath["/both"] + if ep == nil { + t.Fatal("endpoint /both missing") + } + + got := ep.Responses[http.StatusOK] + if got == nil { + t.Fatalf("no 200 response; got codes %v", codesOf(ep)) + } + + // Map iteration order is random, so a single pass would make this flap. + if got.Description != "Specific" { + t.Errorf("wildcard overwrote the exact code: description = %q", got.Description) + } + }) + + t.Run("unparseable keys are dropped, not filed as the error shape", func(t *testing.T) { + ep := byPath["/nonsense"] + if ep == nil { + t.Fatal("endpoint /nonsense missing") + } + + if len(ep.Responses) != 0 { + t.Errorf("expected no responses, got codes %v", codesOf(ep)) + } + + if ep.DefaultError != nil { + t.Error("a malformed status must not become the endpoint's error type") + } + }) +} + +func codesOf(ep *client.Endpoint) []int { + codes := make([]int, 0, len(ep.Responses)) + for code := range ep.Responses { + codes = append(codes, code) + } + + sort.Ints(codes) + + return codes +} From e46b3e5e194c02b8bf0023359e0ba161997b35a1 Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Mon, 3 Aug 2026 10:40:56 -0500 Subject: [PATCH 03/11] fix(client): escape interpolated values in the TypeScript manifest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit package.json is assembled from a format string so its keys keep a fixed order, which means every interpolated value has to arrive escaped. None of them did. A specification whose info.description spanned more than one line — the normal case for an API that documents itself — wrote raw newlines inside a JSON string, and npm refused to parse the manifest at all. The generated client could not be installed, let alone built. The description is also reduced to its first paragraph with line breaks collapsed. npm renders this field as a single line, so the full API description belongs in the README the generator already writes. --- .../client/generators/typescript/generator.go | 42 ++++++- .../typescript/manifest_internal_test.go | 109 ++++++++++++++++++ 2 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 internal/client/generators/typescript/manifest_internal_test.go diff --git a/internal/client/generators/typescript/generator.go b/internal/client/generators/typescript/generator.go index c5711574..de6bde64 100644 --- a/internal/client/generators/typescript/generator.go +++ b/internal/client/generators/typescript/generator.go @@ -468,9 +468,9 @@ func (g *Generator) generatePackageJSON(spec *client.APISpec, config client.Gene // Modern dual package structure return fmt.Sprintf(`{ - "name": "%s", - "version": "%s", - "description": "%s", + "name": %s, + "version": %s, + "description": %s, "type": "module", "main": "./dist/index.cjs", "module": "./dist/index.mjs", @@ -509,7 +509,41 @@ func (g *Generator) generatePackageJSON(spec *client.APISpec, config client.Gene "node": ">=18.0.0" } } -`, packageName, config.Version, spec.Info.Description, depsJSON) +`, jsonString(packageName), jsonString(config.Version), + jsonString(packageSummary(spec.Info.Description)), depsJSON) +} + +// jsonString renders a Go string as a JSON string literal, quotes included. +// +// The package manifest is assembled from a format string so its keys stay in a +// fixed order, which means every interpolated value has to arrive already +// escaped. It previously did not: a spec whose info.description spanned more +// than one line wrote raw newlines inside a JSON string, and the result was a +// package.json that npm refused to parse at all — the generated client could +// not be installed, let alone built. +func jsonString(s string) string { + encoded, err := json.Marshal(s) + if err != nil { + // json.Marshal only fails here on invalid UTF-8. An empty string is a + // worse manifest than a lossy one, so fall back to the empty literal + // rather than emitting something unparseable. + return `""` + } + + return string(encoded) +} + +// packageSummary reduces a specification description to the one-line summary a +// package manifest expects. +// +// npm renders this field as a single line in search results and on the package +// page, so a multi-paragraph API description belongs in the README the +// generator also writes, not here. The first paragraph is taken and its +// internal line breaks collapsed. +func packageSummary(description string) string { + paragraph, _, _ := strings.Cut(strings.TrimSpace(description), "\n\n") + + return strings.Join(strings.Fields(paragraph), " ") } // generateTSConfig generates tsconfig.json. diff --git a/internal/client/generators/typescript/manifest_internal_test.go b/internal/client/generators/typescript/manifest_internal_test.go new file mode 100644 index 00000000..d459ba4f --- /dev/null +++ b/internal/client/generators/typescript/manifest_internal_test.go @@ -0,0 +1,109 @@ +package typescript + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/xraph/forge/internal/client" +) + +// TestPackageManifestIsValidJSON pins the manifest against the specification +// text that broke it. +// +// The name, version and description were interpolated into a format string +// without escaping. Any specification whose info.description spanned more than +// one line — the normal case for an API that documents itself — wrote literal +// newlines inside a JSON string, and npm rejected the manifest outright. The +// generated client could not be installed, let alone built. +func TestPackageManifestIsValidJSON(t *testing.T) { + multiline := "Weather-driven outage risk for electric power systems.\n\n" + + "**Advisory only.** Nothing here commands anything — that is an\n" + + "architectural boundary, and CI checks it. Quote: \"no write path\".\n" + + "Backslash: C:\\path\\to\\thing" + + spec := &client.APISpec{ + Info: client.APIInfo{ + Title: "Test API", + Version: "1.0.0", + Description: multiline, + }, + } + + g := &Generator{} + manifest := g.generatePackageJSON(spec, client.GeneratorConfig{ + PackageName: "@scope/client", + Version: "2.3.4", + }) + + var parsed map[string]any + if err := json.Unmarshal([]byte(manifest), &parsed); err != nil { + t.Fatalf("generated package.json is not valid JSON: %v\n\n%s", err, manifest) + } + + if got := parsed["name"]; got != "@scope/client" { + t.Errorf("name = %v, want @scope/client", got) + } + + if got := parsed["version"]; got != "2.3.4" { + t.Errorf("version = %v, want 2.3.4", got) + } + + desc, _ := parsed["description"].(string) + + if strings.Contains(desc, "\n") { + t.Errorf("description should be a single line, got %q", desc) + } + + if !strings.HasPrefix(desc, "Weather-driven outage risk") { + t.Errorf("description lost its opening: %q", desc) + } + + if strings.Contains(desc, "Advisory only") { + t.Errorf("description should stop at the first paragraph, got %q", desc) + } +} + +// TestPackageManifestEscapesHostileNames covers the two other interpolated +// values. They are far less likely to contain a quote than a description is, +// which is exactly why nothing would have caught it. +func TestPackageManifestEscapesHostileNames(t *testing.T) { + g := &Generator{} + manifest := g.generatePackageJSON(&client.APISpec{}, client.GeneratorConfig{ + PackageName: `weird"name`, + Version: `1.0.0"`, + }) + + var parsed map[string]any + if err := json.Unmarshal([]byte(manifest), &parsed); err != nil { + t.Fatalf("a quote in the package name broke the manifest: %v", err) + } + + if got := parsed["name"]; got != `weird"name` { + t.Errorf("name = %v, want the quote preserved", got) + } +} + +// TestPackageSummary covers the reduction on its own, including the shapes +// with no second paragraph to cut at. +func TestPackageSummary(t *testing.T) { + cases := []struct { + name string + in string + want string + }{ + {"single line", "A grid API.", "A grid API."}, + {"wrapped one paragraph", "A grid\nAPI.", "A grid API."}, + {"stops at the paragraph break", "First.\n\nSecond.", "First."}, + {"collapses runs of spaces", " A grid API. ", "A grid API."}, + {"empty", "", ""}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := packageSummary(tc.in); got != tc.want { + t.Errorf("packageSummary(%q) = %q, want %q", tc.in, got, tc.want) + } + }) + } +} From 20de17b589ebfa45944a9b2bc4d1b8012d4e0d39 Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Mon, 3 Aug 2026 10:40:56 -0500 Subject: [PATCH 04/11] fix(client): cast a binary request body to BodyInit From TypeScript 5.7 the DOM lib parameterises ArrayBufferView, and BodyInit accepts only ArrayBufferView. The generated cast to a bare `ArrayBuffer | ArrayBufferView` therefore widened to include SharedArrayBuffer and stopped being assignable, so every generated client failed to typecheck against a current lib.dom. BodyInit is what the field is declared as, and the branch has already proven the value is a valid body through the bodyTag test and ArrayBuffer.isView. Casting to the declared type is also version- agnostic, where naming ArrayBufferView would break on older TypeScript. --- internal/client/generators/typescript/fetch_client.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/client/generators/typescript/fetch_client.go b/internal/client/generators/typescript/fetch_client.go index 363967aa..5a9e1a58 100644 --- a/internal/client/generators/typescript/fetch_client.go +++ b/internal/client/generators/typescript/fetch_client.go @@ -393,7 +393,14 @@ func (g *FetchClientGenerator) GenerateBaseClient(spec *client.APISpec, config c buf.WriteString(" // ArrayBuffer.isView covers every TypedArray and DataView, and is\n") buf.WriteString(" // realm-independent. Without this a Uint8Array would be stringified\n") buf.WriteString(" // index-by-index into {\\\"0\\\":1,\\\"1\\\":2,...} and an ArrayBuffer into \"{}\".\n") - buf.WriteString(" body = requestConfig.body as ArrayBuffer | ArrayBufferView;\n") + // Cast to BodyInit rather than to `ArrayBuffer | ArrayBufferView`. From + // TypeScript 5.7 the DOM lib parameterises the view (`ArrayBufferView`) + // and BodyInit accepts only `ArrayBufferView`, so the bare + // form now widens to include SharedArrayBuffer and stops being assignable + // — every generated client failed to typecheck on a current lib.dom. The + // branch has already proven the value is a valid body via the bodyTag test + // and ArrayBuffer.isView, and BodyInit is what the field is declared as. + buf.WriteString(" body = requestConfig.body as BodyInit;\n") buf.WriteString(" } else {\n") buf.WriteString(" // Only a JSON-serialisable body ever reaches this branch -- every\n") buf.WriteString(" // native BodyInit shape (FormData, Blob/File, URLSearchParams,\n") From 239ca52359dbbd71cb7202d663f9e29032412c9e Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Mon, 3 Aug 2026 10:55:39 -0500 Subject: [PATCH 05/11] fix(cli): make string slice flags split and accumulate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comments claimed comma-separated values and repeatable occurrences. The code delivered neither: the value was wrapped whole, so `--flag a,b` was a single element named "a,b", and each occurrence replaced the last, so `--flag a --flag b` kept only b. Both forms now work and compose. Empty segments are dropped so that a trailing comma does not become an argument that silently matches nothing — which is the failure this whole class of bug produces, an option that appears to have been applied and was not. --- cli/context.go | 32 ++++++++++++++++++++-- cli/slice_flag_test.go | 61 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 cli/slice_flag_test.go diff --git a/cli/context.go b/cli/context.go index 1e324a35..56b6249b 100644 --- a/cli/context.go +++ b/cli/context.go @@ -3,6 +3,7 @@ package cli import ( "context" "fmt" + "strings" "time" "github.com/xraph/forge" @@ -280,6 +281,17 @@ func parseFlagsForCommand(cmd Command, args []string) (map[string]*flagValue, [] return nil, nil, fmt.Errorf("validation failed for flag %s: %w", name, err) } + // A slice flag accumulates across repeats instead of replacing, which + // is what makes it a slice rather than a string that took the last + // word. + if flagDef.Type() == StringSliceFlagType { + if existing, ok := flagValues[flagDef.Name()]; ok && existing.IsSet() { + if added, ok := parsedValue.([]string); ok { + parsedValue = append(existing.StringSlice(), added...) + } + } + } + flagValues[flagDef.Name()] = &flagValue{ rawValue: parsedValue, isSet: true, @@ -310,12 +322,28 @@ func parseValue(value string, flagType FlagType) (any, error) { case BoolFlagType: return value == "true" || value == "1" || value == "yes", nil case StringSliceFlagType: - // Support comma-separated values + // Comma-separated within one occurrence, and repeatable across + // several; the two compose. Both forms were previously claimed by the + // comments here and delivered by neither — the value was wrapped whole + // and each occurrence replaced the last, so `--flag a --flag b` kept + // only b and `--flag a,b` was a single element named "a,b". + // + // Empty segments are dropped, so a trailing comma does not become a + // pattern that silently matches nothing. if value == "" { return []string{}, nil } - return []string{value}, nil // Single value, can be called multiple times + parts := strings.Split(value, ",") + values := make([]string, 0, len(parts)) + + for _, part := range parts { + if trimmed := strings.TrimSpace(part); trimmed != "" { + values = append(values, trimmed) + } + } + + return values, nil case DurationFlagType: d, err := time.ParseDuration(value) diff --git a/cli/slice_flag_test.go b/cli/slice_flag_test.go new file mode 100644 index 00000000..f5bbb292 --- /dev/null +++ b/cli/slice_flag_test.go @@ -0,0 +1,61 @@ +package cli + +import "testing" + +// TestStringSliceFlagParsing pins both forms the flag has always claimed to +// support and previously supported neither of: comma separation within one +// occurrence, and accumulation across several. +// +// The old behaviour wrapped the value whole and let each occurrence replace +// the last, so `--exclude /a --exclude /b` silently generated a client that +// still contained everything under /a. +func TestStringSliceFlagParsing(t *testing.T) { + cases := []struct { + name string + in string + want []string + }{ + {"single", "/api", []string{"/api"}}, + {"comma separated", "/api,/identity", []string{"/api", "/identity"}}, + {"spaces are trimmed", "/api, /identity", []string{"/api", "/identity"}}, + {"empty segments dropped", "/api,,", []string{"/api"}}, + {"empty", "", []string{}}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := parseValue(tc.in, StringSliceFlagType) + if err != nil { + t.Fatalf("parseValue: %v", err) + } + + values, ok := got.([]string) + if !ok { + t.Fatalf("parseValue returned %T, want []string", got) + } + + if len(values) != len(tc.want) { + t.Fatalf("parseValue(%q) = %v, want %v", tc.in, values, tc.want) + } + + for i := range tc.want { + if values[i] != tc.want[i] { + t.Errorf("parseValue(%q) = %v, want %v", tc.in, values, tc.want) + + break + } + } + }) + } +} + +// TestStringSliceFlagValueSplitsRawString covers the accessor's own fallback, +// which splits a raw string that never went through parseValue. +func TestStringSliceFlagValueSplitsRawString(t *testing.T) { + fv := &flagValue{rawValue: "/api,/identity", isSet: true} + + got := fv.StringSlice() + if len(got) != 2 || got[0] != "/api" || got[1] != "/identity" { + t.Errorf("StringSlice() = %v, want [/api /identity]", got) + } +} From 82cdebcc0ac7535791e7244ddef072bdddd568fd Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Mon, 3 Aug 2026 10:55:39 -0500 Subject: [PATCH 06/11] feat(client): filter generated endpoints by path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A specification is usually larger than the API any one consumer talks to. A service that mounts an auth engine, an admin dashboard and its own domain routes publishes all three from one document, and a client generated over the whole thing buries the endpoints a caller wants under the ones it must never touch. Patterns accept a path prefix, a glob, or a trailing "/**". Prefixes match on a segment boundary, so "/identity" covers "/identity/login" and not "/identity-provider". Plain path.Match alone would not do: its "*" never crosses a separator, so "/api/*" would miss "/api/v1/models", which is the pattern everyone writes first. Pruning unreachable component schemas is the half that makes this worth having. Schemas generate a type each, so filtering endpoints alone leaves a types file that is mostly unreachable from the client's own surface — the endpoints look filtered while the types plainly are not. Reachability follows $ref through properties, items, the polymorphic combinators, additionalProperties and discriminator mappings, and stops on revisit so a self-referential schema terminates. A filter matching nothing is an error rather than an empty client. A mistyped pattern otherwise yields a package that builds, publishes and calls nothing. --- internal/client/config.go | 7 + internal/client/filter.go | 286 +++++++++++++++++++++++++++++++++ internal/client/filter_test.go | 269 +++++++++++++++++++++++++++++++ internal/client/generator.go | 13 ++ 4 files changed, 575 insertions(+) create mode 100644 internal/client/filter.go create mode 100644 internal/client/filter_test.go diff --git a/internal/client/config.go b/internal/client/config.go index 5410e577..45150261 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -36,6 +36,13 @@ type GeneratorConfig struct { // Streaming contains streaming-specific configuration Streaming StreamingConfig + // PathFilter selects which endpoints the generated client covers. + // + // Honoured by GenerateFromFile, which owns the spec it parses. Callers of + // Generate hold their own *APISpec and should apply the filter themselves + // with spec.Apply — silently mutating an argument would be a surprise. + PathFilter PathFilter + // Module is the Go module path (for Go only) Module string diff --git a/internal/client/filter.go b/internal/client/filter.go new file mode 100644 index 00000000..1ce12cbd --- /dev/null +++ b/internal/client/filter.go @@ -0,0 +1,286 @@ +package client + +import ( + "path" + "sort" + "strings" +) + +// PathFilter selects which endpoints a generated client covers. +// +// It exists because a specification is usually larger than the API any one +// consumer talks to. A service that mounts an auth engine, an admin dashboard +// and its own domain routes publishes all three from one document, and a +// client generated over the whole thing buries the twenty endpoints a caller +// wants under the two hundred it must never touch. +// +// Filtering is a generation-time concern rather than a serving-time one: the +// server is right to publish everything it serves, and the client is right to +// bind only what it consumes. +type PathFilter struct { + // Include keeps only the endpoints matching at least one pattern. Empty + // means every endpoint is a candidate. + Include []string + + // Exclude drops endpoints matching any pattern, and is applied after + // Include so that a narrow exclusion can carve a hole in a broad include. + Exclude []string +} + +// FilterResult reports what a filter did, so a caller can say so rather than +// silently generating a smaller client than the operator expected. +type FilterResult struct { + // KeptEndpoints and DroppedEndpoints count operations, not paths: one path + // with a GET and a DELETE is two endpoints and they filter together. + KeptEndpoints int + DroppedEndpoints int + + // KeptSchemas and DroppedSchemas count component schemas after pruning. + KeptSchemas int + DroppedSchemas int + + // DroppedPaths lists the distinct paths removed, sorted, for reporting. + DroppedPaths []string +} + +// Empty reports whether the filter would do anything at all. +func (f PathFilter) Empty() bool { + return len(f.Include) == 0 && len(f.Exclude) == 0 +} + +// Apply filters the spec in place and prunes schemas no surviving endpoint can +// reach. +// +// Pruning matters as much as the endpoint filter. Component schemas generate a +// type each, so a spec whose auth engine contributes a hundred and forty of +// them yields a types file that is mostly unreachable from the client's own +// surface — the endpoints look filtered while the types plainly are not. +func (s *APISpec) Apply(f PathFilter) FilterResult { + result := FilterResult{} + + if f.Empty() { + result.KeptEndpoints = len(s.Endpoints) + result.KeptSchemas = len(s.Schemas) + + return result + } + + kept := make([]Endpoint, 0, len(s.Endpoints)) + dropped := make(map[string]struct{}) + + for _, endpoint := range s.Endpoints { + if f.allows(endpoint.Path) { + kept = append(kept, endpoint) + + continue + } + + dropped[endpoint.Path] = struct{}{} + result.DroppedEndpoints++ + } + + s.Endpoints = kept + result.KeptEndpoints = len(kept) + + for p := range dropped { + result.DroppedPaths = append(result.DroppedPaths, p) + } + + sort.Strings(result.DroppedPaths) + + before := len(s.Schemas) + s.pruneUnreachableSchemas() + result.KeptSchemas = len(s.Schemas) + result.DroppedSchemas = before - result.KeptSchemas + + return result +} + +// allows reports whether a path survives the filter. +func (f PathFilter) allows(p string) bool { + if len(f.Include) > 0 && !matchesAny(f.Include, p) { + return false + } + + return !matchesAny(f.Exclude, p) +} + +func matchesAny(patterns []string, p string) bool { + for _, pattern := range patterns { + if matchPath(pattern, p) { + return true + } + } + + return false +} + +// matchPath matches a path against one pattern. +// +// Two forms are accepted, because operators reach for both and guessing wrong +// is a silently empty client: +// +// - a path prefix: "/identity" matches "/identity" and "/identity/login" but +// not "/identity-provider", since the boundary is a path separator rather +// than a character count; +// - a glob: "/api/*/health" matches through one segment, and a trailing +// "/**" matches any depth. Plain path.Match is not enough on its own — its +// "*" never crosses a separator, so "/api/*" would miss "/api/v1/models", +// which is the pattern everyone writes first. +func matchPath(pattern, p string) bool { + if pattern == "" { + return false + } + + pattern = strings.TrimSuffix(pattern, "/") + if pattern == "" { + // "/" alone: the root prefix, which is every path. + return true + } + + if pattern == p { + return true + } + + // Recursive glob: "/api/**" is the prefix form written explicitly. + if base, ok := strings.CutSuffix(pattern, "/**"); ok { + return p == base || strings.HasPrefix(p, base+"/") + } + + // Prefix, on a segment boundary. + if strings.HasPrefix(p, pattern+"/") { + return true + } + + if ok, err := path.Match(pattern, p); err == nil && ok { + return true + } + + return false +} + +// pruneUnreachableSchemas drops component schemas that no remaining endpoint +// can reach, following $ref transitively through properties, items, the +// polymorphic combinators and additionalProperties. +func (s *APISpec) pruneUnreachableSchemas() { + if len(s.Schemas) == 0 { + return + } + + reachable := make(map[string]struct{}, len(s.Schemas)) + + var walk func(schema *Schema) + + walk = func(schema *Schema) { + if schema == nil { + return + } + + if name := refName(schema.Ref); name != "" { + if _, seen := reachable[name]; seen { + // Already expanded. Stopping here is also what keeps a + // self-referential schema — a tree node, a linked list — from + // recursing forever. + return + } + + reachable[name] = struct{}{} + walk(s.Schemas[name]) + } + + for _, prop := range schema.Properties { + walk(prop) + } + + walk(schema.Items) + + for _, sub := range schema.OneOf { + walk(sub) + } + + for _, sub := range schema.AnyOf { + walk(sub) + } + + for _, sub := range schema.AllOf { + walk(sub) + } + + if nested, ok := schema.AdditionalProperties.(*Schema); ok { + walk(nested) + } + + // A discriminator names schemas that no property references directly; + // dropping them would leave a union that cannot resolve its variants. + if schema.Discriminator != nil { + for _, ref := range schema.Discriminator.Mapping { + if name := refName(ref); name != "" { + if _, seen := reachable[name]; !seen { + reachable[name] = struct{}{} + walk(s.Schemas[name]) + } + } + } + } + } + + for i := range s.Endpoints { + endpoint := &s.Endpoints[i] + + for _, param := range endpoint.PathParams { + walk(param.Schema) + } + + for _, param := range endpoint.QueryParams { + walk(param.Schema) + } + + for _, param := range endpoint.HeaderParams { + walk(param.Schema) + } + + if endpoint.RequestBody != nil { + for _, media := range endpoint.RequestBody.Content { + walk(media.Schema) + } + } + + for _, resp := range endpoint.Responses { + walkResponse(resp, walk) + } + + walkResponse(endpoint.DefaultError, walk) + } + + for name := range s.Schemas { + if _, ok := reachable[name]; !ok { + delete(s.Schemas, name) + } + } +} + +func walkResponse(resp *Response, walk func(*Schema)) { + if resp == nil { + return + } + + for _, media := range resp.Content { + walk(media.Schema) + } + + for _, header := range resp.Headers { + walk(header.Schema) + } +} + +// refName extracts the component name from a local $ref, and returns "" for a +// remote or malformed one — which is not something to prune against. +func refName(ref string) string { + const prefix = "#/components/schemas/" + + if !strings.HasPrefix(ref, prefix) { + return "" + } + + return strings.TrimPrefix(ref, prefix) +} diff --git a/internal/client/filter_test.go b/internal/client/filter_test.go new file mode 100644 index 00000000..88aef646 --- /dev/null +++ b/internal/client/filter_test.go @@ -0,0 +1,269 @@ +package client_test + +import ( + "sort" + "testing" + "time" + + "github.com/xraph/forge/internal/client" +) + +func TestMatchPathForms(t *testing.T) { + cases := []struct { + pattern string + path string + want bool + }{ + // Prefix, on a segment boundary. + {"/identity", "/identity", true}, + {"/identity", "/identity/login", true}, + {"/identity", "/identity/v1/sessions/current", true}, + {"/identity", "/identity-provider", false}, + {"/identity", "/api/v1/identity", false}, + + // A trailing slash on the pattern changes nothing. + {"/identity/", "/identity/login", true}, + + // Glob within one segment. + {"/api/*/health", "/api/v1/health", true}, + {"/api/*/health", "/api/v1/deep/health", false}, + + // Recursive glob is the prefix form written out. + {"/api/**", "/api/v1/models", true}, + {"/api/**", "/api", true}, + {"/api/**", "/apiv1", false}, + + // The root matches everything. + {"/", "/anything/at/all", true}, + + {"", "/anything", false}, + } + + for _, tc := range cases { + spec := &client.APISpec{ + Endpoints: []client.Endpoint{{Path: tc.path, Method: "GET"}}, + } + + got := spec.Apply(client.PathFilter{Include: []string{tc.pattern}}).KeptEndpoints == 1 + if got != tc.want { + t.Errorf("pattern %q against %q = %v, want %v", tc.pattern, tc.path, got, tc.want) + } + } +} + +func TestFilterIncludeExcludePrecedence(t *testing.T) { + spec := &client.APISpec{ + Endpoints: []client.Endpoint{ + {Path: "/api/v1/models", Method: "GET"}, + {Path: "/api/v1/models/{id}", Method: "GET"}, + {Path: "/api/v1/internal/debug", Method: "GET"}, + {Path: "/identity/login", Method: "POST"}, + {Path: "/_health", Method: "GET"}, + }, + } + + // A narrow exclusion carving a hole in a broad include. + result := spec.Apply(client.PathFilter{ + Include: []string{"/api/v1"}, + Exclude: []string{"/api/v1/internal"}, + }) + + if result.KeptEndpoints != 2 { + t.Fatalf("kept %d endpoints, want 2", result.KeptEndpoints) + } + + if result.DroppedEndpoints != 3 { + t.Errorf("dropped %d endpoints, want 3", result.DroppedEndpoints) + } + + for _, ep := range spec.Endpoints { + if ep.Path == "/api/v1/internal/debug" { + t.Error("exclude must be applied after include") + } + } + + want := []string{"/_health", "/api/v1/internal/debug", "/identity/login"} + if len(result.DroppedPaths) != len(want) { + t.Fatalf("dropped paths = %v, want %v", result.DroppedPaths, want) + } + + sort.Strings(result.DroppedPaths) + + for i := range want { + if result.DroppedPaths[i] != want[i] { + t.Errorf("dropped paths = %v, want %v", result.DroppedPaths, want) + + break + } + } +} + +func TestFilterExcludeOnlyKeepsTheRest(t *testing.T) { + spec := &client.APISpec{ + Endpoints: []client.Endpoint{ + {Path: "/api/v1/models", Method: "GET"}, + {Path: "/identity/login", Method: "POST"}, + }, + } + + result := spec.Apply(client.PathFilter{Exclude: []string{"/identity"}}) + + if result.KeptEndpoints != 1 || spec.Endpoints[0].Path != "/api/v1/models" { + t.Fatalf("kept %v, want only /api/v1/models", spec.Endpoints) + } +} + +func TestFilterEmptyIsANoOp(t *testing.T) { + spec := &client.APISpec{ + Endpoints: []client.Endpoint{{Path: "/a", Method: "GET"}}, + Schemas: map[string]*client.Schema{"Unused": {Type: "object"}}, + } + + result := spec.Apply(client.PathFilter{}) + + if result.KeptEndpoints != 1 || result.KeptSchemas != 1 { + t.Fatalf("empty filter changed the spec: %+v", result) + } + + // Notably it must NOT prune. Generating over an unfiltered spec is the + // existing behaviour and some callers depend on schemas the endpoints + // never reference. + if _, ok := spec.Schemas["Unused"]; !ok { + t.Error("an empty filter must not prune schemas") + } +} + +// TestFilterPrunesUnreachableSchemas is the half that makes filtering worth +// having: endpoints look filtered while the types file plainly is not. +func TestFilterPrunesUnreachableSchemas(t *testing.T) { + spec := &client.APISpec{ + Endpoints: []client.Endpoint{ + { + Path: "/api/v1/models", + Method: "GET", + Responses: map[int]*client.Response{ + 200: {Content: map[string]*client.MediaType{ + "application/json": {Schema: &client.Schema{Ref: "#/components/schemas/ModelList"}}, + }}, + }, + }, + { + Path: "/identity/login", + Method: "POST", + Responses: map[int]*client.Response{ + 200: {Content: map[string]*client.MediaType{ + "application/json": {Schema: &client.Schema{Ref: "#/components/schemas/Session"}}, + }}, + }, + }, + }, + Schemas: map[string]*client.Schema{ + "ModelList": {Type: "object", Properties: map[string]*client.Schema{ + "items": {Type: "array", Items: &client.Schema{Ref: "#/components/schemas/Model"}}, + }}, + "Model": {Type: "object", Properties: map[string]*client.Schema{ + "bus": {Ref: "#/components/schemas/Bus"}, + }}, + "Bus": {Type: "object"}, + "Session": {Type: "object"}, + "Orphan": {Type: "object"}, + }, + } + + result := spec.Apply(client.PathFilter{Include: []string{"/api/v1"}}) + + for _, name := range []string{"ModelList", "Model", "Bus"} { + if _, ok := spec.Schemas[name]; !ok { + t.Errorf("%s is reachable from a kept endpoint and was pruned", name) + } + } + + for _, name := range []string{"Session", "Orphan"} { + if _, ok := spec.Schemas[name]; ok { + t.Errorf("%s is unreachable and should have been pruned", name) + } + } + + if result.KeptSchemas != 3 || result.DroppedSchemas != 2 { + t.Errorf("kept %d dropped %d schemas, want 3 and 2", result.KeptSchemas, result.DroppedSchemas) + } +} + +// A schema that references itself must not hang the walk. +func TestFilterHandlesRecursiveSchemas(t *testing.T) { + spec := &client.APISpec{ + Endpoints: []client.Endpoint{{ + Path: "/api/v1/tree", + Method: "GET", + Responses: map[int]*client.Response{ + 200: {Content: map[string]*client.MediaType{ + "application/json": {Schema: &client.Schema{Ref: "#/components/schemas/Node"}}, + }}, + }, + }}, + Schemas: map[string]*client.Schema{ + "Node": {Type: "object", Properties: map[string]*client.Schema{ + "children": {Type: "array", Items: &client.Schema{Ref: "#/components/schemas/Node"}}, + }}, + }, + } + + done := make(chan struct{}) + + go func() { + spec.Apply(client.PathFilter{Include: []string{"/api/v1"}}) + close(done) + }() + + select { + case <-done: + case <-timeoutAfterSeconds(5): + t.Fatal("recursive schema caused the reachability walk to hang") + } + + if _, ok := spec.Schemas["Node"]; !ok { + t.Error("Node was pruned despite being reachable") + } +} + +// A discriminator names variants nothing else references. +func TestFilterKeepsDiscriminatorVariants(t *testing.T) { + spec := &client.APISpec{ + Endpoints: []client.Endpoint{{ + Path: "/api/v1/events", + Method: "GET", + Responses: map[int]*client.Response{ + 200: {Content: map[string]*client.MediaType{ + "application/json": {Schema: &client.Schema{Ref: "#/components/schemas/Event"}}, + }}, + }, + }}, + Schemas: map[string]*client.Schema{ + "Event": { + Type: "object", + Discriminator: &client.Discriminator{ + PropertyName: "kind", + Mapping: map[string]string{ + "trip": "#/components/schemas/TripEvent", + }, + }, + }, + "TripEvent": {Type: "object"}, + "Orphan": {Type: "object"}, + }, + } + + spec.Apply(client.PathFilter{Include: []string{"/api/v1"}}) + + if _, ok := spec.Schemas["TripEvent"]; !ok { + t.Error("a discriminator variant was pruned, leaving a union that cannot resolve") + } + + if _, ok := spec.Schemas["Orphan"]; ok { + t.Error("Orphan should have been pruned") + } +} + +func timeoutAfterSeconds(n int) <-chan time.Time { + return time.After(time.Duration(n) * time.Second) +} diff --git a/internal/client/generator.go b/internal/client/generator.go index 78e46be4..45936f2c 100644 --- a/internal/client/generator.go +++ b/internal/client/generator.go @@ -85,6 +85,19 @@ func (g *Generator) GenerateFromFile(ctx context.Context, filePath string, confi return nil, fmt.Errorf("parse spec file: %w", err) } + if !config.PathFilter.Empty() { + result := spec.Apply(config.PathFilter) + + // A filter that matches nothing is a mistake in the pattern, not a + // request for an empty client. Failing here names the problem; the + // alternative is a package that builds, publishes and calls nothing. + if result.KeptEndpoints == 0 { + return nil, fmt.Errorf( + "path filter matched none of the %d endpoints in %s (include=%v exclude=%v)", + result.DroppedEndpoints, filePath, config.PathFilter.Include, config.PathFilter.Exclude) + } + } + // Generate client return g.Generate(ctx, spec, config) } From 61ade033db51cc8a599270aad4bbfe70e2c5caca Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Mon, 3 Aug 2026 10:55:39 -0500 Subject: [PATCH 07/11] feat(cli): --include and --exclude for client generation Both are repeatable and accept comma-separated lists. Flags win over .forge-client.yaml, which gains include and exclude keys so the split lives with the package it generates rather than in a shell history. The chosen patterns are echoed before generation. A client quietly missing half its endpoints looks identical to one whose server never had them, and the difference should not have to be discovered by calling something that is not there. --- cmd/forge/plugins/client.go | 31 ++++++++++++++++++++++++++++++ cmd/forge/plugins/client_config.go | 7 +++++++ 2 files changed, 38 insertions(+) diff --git a/cmd/forge/plugins/client.go b/cmd/forge/plugins/client.go index e83dc151..1ba10e29 100644 --- a/cmd/forge/plugins/client.go +++ b/cmd/forge/plugins/client.go @@ -57,6 +57,8 @@ func (p *ClientPlugin) Commands() []cli.Command { // own per-language default applies (camel for typescript, preserve // otherwise) so omitting this flag changes nothing for existing users. cli.WithFlag(cli.NewStringFlag("field-naming", "", "Client-side field naming strategy: camel, pascal, snake, or preserve (default: camel for typescript, preserve otherwise)", "")), + cli.WithFlag(cli.NewStringSliceFlag("include", "", "Only generate endpoints whose path matches a pattern (repeatable; prefix, glob or `/**`)", nil)), + cli.WithFlag(cli.NewStringSliceFlag("exclude", "", "Skip endpoints whose path matches a pattern; applied after --include (repeatable)", nil)), cli.WithFlag(cli.NewStringFlag("field-overrides", "", "Comma-separated field name overrides, e.g. 'User.user_id=userIdentifier,api_key=apiKey' (schema-scoped keys use \"Schema.wire_name\"; a bare \"wire_name\" applies globally)", "")), // Authentication and streaming (optional, defaults from config) @@ -137,6 +139,8 @@ func (p *ClientPlugin) generateClient(ctx cli.CommandContext) error { outputDir := ctx.String("output") packageName := ctx.String("package") baseURL := ctx.String("base-url") + includePaths := ctx.StringSlice("include") + excludePaths := ctx.StringSlice("exclude") module := ctx.String("module") // Use config defaults if flags not provided @@ -382,6 +386,32 @@ func (p *ClientPlugin) generateClient(ctx cli.CommandContext) error { enableHistory = true } + // Path filter: flags win, config fills in. Reported below rather than + // applied silently — a client that is quietly missing half its endpoints + // looks identical to one whose server never had them. + pathFilter := client.PathFilter{ + Include: includePaths, + Exclude: excludePaths, + } + + if len(pathFilter.Include) == 0 { + pathFilter.Include = clientConfig.Defaults.Include + } + + if len(pathFilter.Exclude) == 0 { + pathFilter.Exclude = clientConfig.Defaults.Exclude + } + + if !pathFilter.Empty() { + if len(pathFilter.Include) > 0 { + ctx.Info("Including paths: " + strings.Join(pathFilter.Include, ", ")) + } + + if len(pathFilter.Exclude) > 0 { + ctx.Info("Excluding paths: " + strings.Join(pathFilter.Exclude, ", ")) + } + } + // Create config genConfig := client.GeneratorConfig{ Language: language, @@ -395,6 +425,7 @@ func (p *ClientPlugin) generateClient(ctx cli.CommandContext) error { Version: "1.0.0", FieldNaming: fieldNaming, FieldOverrides: fieldOverrides, + PathFilter: pathFilter, Features: client.Features{ Reconnection: reconnection, Heartbeat: heartbeat, diff --git a/cmd/forge/plugins/client_config.go b/cmd/forge/plugins/client_config.go index 957e218b..3c318bc8 100644 --- a/cmd/forge/plugins/client_config.go +++ b/cmd/forge/plugins/client_config.go @@ -70,6 +70,13 @@ type GenerationDefaults struct { BaseURL string `yaml:"base_url,omitempty"` Module string `yaml:"module,omitempty"` + // Include keeps only endpoints whose path matches a pattern; Exclude drops + // matches and is applied second. Both accept a path prefix, a glob, or a + // trailing "/**". A specification usually describes more than any one + // consumer talks to, and these are how a client binds only its own surface. + Include []string `yaml:"include,omitempty"` + Exclude []string `yaml:"exclude,omitempty"` + // Feature flags Auth bool `yaml:"auth"` Streaming bool `yaml:"streaming"` From 5a718db136c1515b4a26cde248eb726a65a5b855 Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Mon, 3 Aug 2026 11:29:31 -0500 Subject: [PATCH 08/11] fix(client): resolve config from the working directory, emit type imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things stopped a generated package from being usable inside a workspace. The client config was resolved from the Forge project root rather than the working directory. LoadClientConfig already walks upward, so starting at the working directory finds a config beside the package being generated and one at the project root; starting at the root finds only the root's, which in a workspace is the one place the file usually is not. A package carrying its own .forge-client.yaml was silently generated with defaults. Types are now imported with `import type`. Under verbatimModuleSyntax — on by default in a strict project, and not something a generated package can ask its consumer to turn off — a type imported as a value is a compile error, because the emitter is forbidden from guessing which imports to elide. The generated client could not be typechecked inside such a project at all. --- cmd/forge/plugins/client.go | 22 +++++++++++++++---- .../client/generators/typescript/generator.go | 14 +++++++++--- internal/client/generators/typescript/rest.go | 4 +++- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/cmd/forge/plugins/client.go b/cmd/forge/plugins/client.go index 1ba10e29..1a9c8a2c 100644 --- a/cmd/forge/plugins/client.go +++ b/cmd/forge/plugins/client.go @@ -119,8 +119,15 @@ func (p *ClientPlugin) generateClient(ctx cli.CommandContext) error { err error ) - workDir, _ := os.Getwd() - if p.config != nil { + // Resolved from the working directory, not the project root. + // + // LoadClientConfig already walks upward, so starting here finds a config + // beside the package being generated *and* one at the project root. + // Starting at the root instead finds only the root's, which in a workspace + // is the one place the file usually is not — a package that carries its own + // .forge-client.yaml was silently generated with defaults. + workDir, err := os.Getwd() + if err != nil && p.config != nil { workDir = p.config.RootDir } @@ -567,8 +574,15 @@ func (p *ClientPlugin) listEndpoints(ctx cli.CommandContext) error { err error ) - workDir, _ := os.Getwd() - if p.config != nil { + // Resolved from the working directory, not the project root. + // + // LoadClientConfig already walks upward, so starting here finds a config + // beside the package being generated *and* one at the project root. + // Starting at the root instead finds only the root's, which in a workspace + // is the one place the file usually is not — a package that carries its own + // .forge-client.yaml was silently generated with defaults. + workDir, err := os.Getwd() + if err != nil && p.config != nil { workDir = p.config.RootDir } diff --git a/internal/client/generators/typescript/generator.go b/internal/client/generators/typescript/generator.go index de6bde64..86cb9365 100644 --- a/internal/client/generators/typescript/generator.go +++ b/internal/client/generators/typescript/generator.go @@ -1297,12 +1297,20 @@ func (g *Generator) schemaToTSType(schema *client.Schema, spec *client.APISpec, func (g *Generator) generateClient(spec *client.APISpec, config client.GeneratorConfig) string { var buf strings.Builder - buf.WriteString("import { HTTPClient, RequestConfig } from './fetch';\n") + // Types are imported with `import type`, values without. + // + // Under verbatimModuleSyntax — on by default in a strict project, and the + // setting a generated package cannot ask its consumer to turn off — a + // type imported as a value is a compile error, because the emitter is + // forbidden from guessing which imports to elide. The generated client + // previously could not be typechecked inside such a project at all. + buf.WriteString("import { HTTPClient } from './fetch';\n") + buf.WriteString("import type { RequestConfig } from './fetch';\n") if config.IncludeAuth { - buf.WriteString("import { ClientConfig, AuthConfig } from './types';\n") + buf.WriteString("import type { ClientConfig, AuthConfig } from './types';\n") } else { - buf.WriteString("import { ClientConfig } from './types';\n") + buf.WriteString("import type { ClientConfig } from './types';\n") } buf.WriteString("import { createError } from './errors';\n\n") diff --git a/internal/client/generators/typescript/rest.go b/internal/client/generators/typescript/rest.go index 23ec3ac0..4e6dd7f8 100644 --- a/internal/client/generators/typescript/rest.go +++ b/internal/client/generators/typescript/rest.go @@ -152,7 +152,9 @@ func (r *RESTGenerator) Generate(spec *client.APISpec, config client.GeneratorCo base = "Client" } - buf.WriteString("import { RequestConfig } from './fetch';\n") + // `import type`: RequestConfig is a type, and verbatimModuleSyntax refuses + // a type imported as a value. + buf.WriteString("import type { RequestConfig } from './fetch';\n") fmt.Fprintf(&buf, "import { %s } from './client';\n", base) buf.WriteString("import * as types from './types';\n\n") From 78172870af38d60f29dfdfa256fa0c2bc03e34b8 Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Mon, 3 Aug 2026 13:24:00 -0500 Subject: [PATCH 09/11] refactor(client): extract the method parameter ordering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Query and header parameters are emitted optional, so required ones are grouped first to avoid an optional-before-required signature. That ordering was expressed only inside the code that renders a signature, which left anything generating a *call* to these methods to reimplement it. Two implementations would drift, and the drift is silent: passing a limit where an id is expected still compiles when both are strings. Output is byte-identical — the generator suite passes unchanged. --- internal/client/generators/typescript/rest.go | 100 ++++++++++++------ 1 file changed, 67 insertions(+), 33 deletions(-) diff --git a/internal/client/generators/typescript/rest.go b/internal/client/generators/typescript/rest.go index 4e6dd7f8..6034dcf5 100644 --- a/internal/client/generators/typescript/rest.go +++ b/internal/client/generators/typescript/rest.go @@ -736,59 +736,93 @@ func (r *RESTGenerator) responseCodecRef(endpoint *client.Endpoint) (id string, // required body) are grouped first to avoid an optional-before-required // signature, which is a TypeScript error. func (r *RESTGenerator) generateParameters(endpoint client.Endpoint, spec *client.APISpec) string { - var required []string - var optional []string + params := r.methodParams(endpoint, spec) + parts := make([]string, 0, len(params)) + + for _, p := range params { + if p.Optional { + parts = append(parts, p.Name+"?: "+p.TSType) + + continue + } + + parts = append(parts, p.Name+": "+p.TSType) + } + + return strings.Join(parts, ", ") +} + +// MethodParam is one parameter of a generated method, in call order. +type MethodParam struct { + // Name is the TypeScript identifier. + Name string + + // TSType is the declared type, already carrying "| undefined" where the + // spec made the parameter optional. + TSType string + + // Optional marks the parameter as declared with "?". + Optional bool +} + +// methodParams returns a method's parameters in the order they are emitted. +// +// Extracted so that anything generating a *call* to these methods — the React +// Query hooks, in particular — derives the argument order from the same place +// the signature does. Two implementations of this ordering would drift, and +// the drift would be silent: passing a limit where an id is expected still +// compiles when both are strings. +// +// Query and header parameters are always emitted optional, so required +// parameters (path params and a required body) are grouped first to avoid an +// optional-before-required signature, which is a TypeScript error. +func (r *RESTGenerator) methodParams(endpoint client.Endpoint, spec *client.APISpec) []MethodParam { + var required []MethodParam + + var optional []MethodParam - // Path parameters are always required. for _, param := range endpoint.PathParams { - paramName := r.toTSParamName(param.Name) - tsType := r.schemaToTSType(param.Schema, spec) - required = append(required, fmt.Sprintf("%s: %s", paramName, tsType)) + required = append(required, MethodParam{ + Name: r.toTSParamName(param.Name), + TSType: r.schemaToTSType(param.Schema, spec), + }) } - // Request body: a required body joins the required group (placed before the - // optional query/header params); an optional body is appended last. - var optionalBody string + var optionalBody *MethodParam if r.hasBodyParam(&endpoint) { typeName := r.requestBodyParamType(&endpoint, spec) if endpoint.RequestBody.Required { - required = append(required, "body: "+typeName) + required = append(required, MethodParam{Name: "body", TSType: typeName}) } else { - optionalBody = "body?: " + typeName + optionalBody = &MethodParam{Name: "body", TSType: typeName, Optional: true} } } - // Query parameters (always emitted optional). - for _, param := range endpoint.QueryParams { - paramName := r.toTSParamName(param.Name) + appendOptional := func(params []client.Parameter) { + for _, param := range params { + tsType := r.schemaToTSType(param.Schema, spec) + if !param.Required { + tsType += " | undefined" + } - tsType := r.schemaToTSType(param.Schema, spec) - if !param.Required { - tsType += " | undefined" + optional = append(optional, MethodParam{ + Name: r.toTSParamName(param.Name), + TSType: tsType, + Optional: true, + }) } - - optional = append(optional, fmt.Sprintf("%s?: %s", paramName, tsType)) } - // Header parameters (always emitted optional). - for _, param := range endpoint.HeaderParams { - paramName := r.toTSParamName(param.Name) - - tsType := r.schemaToTSType(param.Schema, spec) - if !param.Required { - tsType += " | undefined" - } - - optional = append(optional, fmt.Sprintf("%s?: %s", paramName, tsType)) - } + appendOptional(endpoint.QueryParams) + appendOptional(endpoint.HeaderParams) - if optionalBody != "" { - optional = append(optional, optionalBody) + if optionalBody != nil { + optional = append(optional, *optionalBody) } - return strings.Join(append(required, optional...), ", ") + return append(required, optional...) } // generateReturnType generates the return type for an endpoint by unioning From 4181653836ed3948d881b7e3b1fb6f199bba617f Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Mon, 3 Aug 2026 13:24:00 -0500 Subject: [PATCH 10/11] feat(client): generate TanStack Query hooks over the REST client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A layer, not a second client. Each hook calls the method rest.go already produced, so the API is described once and a change to it lands in one place. Deriving the surface twice is how a hook and the method it wraps come to disagree about a parameter — which no compiler catches, because both sides were generated from a spec that never changed. Every parameter a method accepts is part of its cache key. A key that omits one serves one request's cached answer to a different request, and where an API is versioned along two time axes that failure is silent and plausible: a key carrying only the valid time returns what is true now to a caller that asked what was known then. There is a test for it. Reads become useQuery and writes become useMutation. A mutation is not keyed, because caching a write would serve a stale answer to a request whose whole purpose was to change something. The client is a parameter rather than a module singleton or a context this file invents: a generated file should not decide how an application provides its dependencies, and an explicit argument keeps the hooks usable from a test with no provider tree. react-query is a peer dependency so the hooks share the QueryClient the application already made — a second copy in the tree is a second cache no invalidation reaches — and it is only declared when hooks are actually generated. --- cmd/forge/plugins/client.go | 3 + cmd/forge/plugins/client_config.go | 3 + internal/client/config.go | 7 + .../client/generators/typescript/generator.go | 35 +- .../client/generators/typescript/query.go | 365 ++++++++++++++++++ .../typescript/query_internal_test.go | 183 +++++++++ 6 files changed, 594 insertions(+), 2 deletions(-) create mode 100644 internal/client/generators/typescript/query.go create mode 100644 internal/client/generators/typescript/query_internal_test.go diff --git a/cmd/forge/plugins/client.go b/cmd/forge/plugins/client.go index 1a9c8a2c..25d08bf5 100644 --- a/cmd/forge/plugins/client.go +++ b/cmd/forge/plugins/client.go @@ -57,6 +57,7 @@ func (p *ClientPlugin) Commands() []cli.Command { // own per-language default applies (camel for typescript, preserve // otherwise) so omitting this flag changes nothing for existing users. cli.WithFlag(cli.NewStringFlag("field-naming", "", "Client-side field naming strategy: camel, pascal, snake, or preserve (default: camel for typescript, preserve otherwise)", "")), + cli.WithFlag(cli.NewBoolFlag("react-query", "", "Generate TanStack Query hooks over the client", false)), cli.WithFlag(cli.NewStringSliceFlag("include", "", "Only generate endpoints whose path matches a pattern (repeatable; prefix, glob or `/**`)", nil)), cli.WithFlag(cli.NewStringSliceFlag("exclude", "", "Skip endpoints whose path matches a pattern; applied after --include (repeatable)", nil)), cli.WithFlag(cli.NewStringFlag("field-overrides", "", "Comma-separated field name overrides, e.g. 'User.user_id=userIdentifier,api_key=apiKey' (schema-scoped keys use \"Schema.wire_name\"; a bare \"wire_name\" applies globally)", "")), @@ -146,6 +147,7 @@ func (p *ClientPlugin) generateClient(ctx cli.CommandContext) error { outputDir := ctx.String("output") packageName := ctx.String("package") baseURL := ctx.String("base-url") + reactQuery := ctx.Bool("react-query") || clientConfig.Defaults.ReactQuery includePaths := ctx.StringSlice("include") excludePaths := ctx.StringSlice("exclude") module := ctx.String("module") @@ -433,6 +435,7 @@ func (p *ClientPlugin) generateClient(ctx cli.CommandContext) error { FieldNaming: fieldNaming, FieldOverrides: fieldOverrides, PathFilter: pathFilter, + ReactQuery: reactQuery, Features: client.Features{ Reconnection: reconnection, Heartbeat: heartbeat, diff --git a/cmd/forge/plugins/client_config.go b/cmd/forge/plugins/client_config.go index 3c318bc8..c8a0ee4c 100644 --- a/cmd/forge/plugins/client_config.go +++ b/cmd/forge/plugins/client_config.go @@ -70,6 +70,9 @@ type GenerationDefaults struct { BaseURL string `yaml:"base_url,omitempty"` Module string `yaml:"module,omitempty"` + // ReactQuery emits TanStack Query hooks over the generated client. + ReactQuery bool `yaml:"react_query"` + // Include keeps only endpoints whose path matches a pattern; Exclude drops // matches and is applied second. Both accept a path prefix, a glob, or a // trailing "/**". A specification usually describes more than any one diff --git a/internal/client/config.go b/internal/client/config.go index 45150261..ffd424d1 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -36,6 +36,13 @@ type GeneratorConfig struct { // Streaming contains streaming-specific configuration Streaming StreamingConfig + // ReactQuery emits TanStack Query hooks over the generated REST client. + // + // A layer rather than a second client: the hooks call the methods the REST + // generator already produced. Off by default — it adds a peer dependency + // on @tanstack/react-query, which a non-React consumer should not inherit. + ReactQuery bool + // PathFilter selects which endpoints the generated client covers. // // Honoured by GenerateFromFile, which owns the spec it parses. Callers of diff --git a/internal/client/generators/typescript/generator.go b/internal/client/generators/typescript/generator.go index 86cb9365..9fe4c582 100644 --- a/internal/client/generators/typescript/generator.go +++ b/internal/client/generators/typescript/generator.go @@ -289,6 +289,17 @@ func (g *Generator) Generate(ctx context.Context, specIface generators.APISpec, paginationCode := paginationGen.GeneratePaginationHelpers(spec, config) genClient.Files["src/pagination.ts"] = paginationCode } + + // Generate TanStack Query hooks if enabled + if config.ReactQuery && len(spec.Endpoints) > 0 { + queryGen := NewReactQueryGenerator() + + queryCode, queryWarnings := queryGen.Generate(spec, config) + if queryCode != "" { + genClient.Files["src/query.ts"] = queryCode + genClient.Warnings = append(genClient.Warnings, queryWarnings...) + } + } } // Generate types (always needed) @@ -489,7 +500,7 @@ func (g *Generator) generatePackageJSON(spec *client.APISpec, config client.Gene "lint": "eslint src --ext .ts", "format": "prettier --write \"src/**/*.ts\"" }, - "dependencies": %s, + "dependencies": %s,%s "devDependencies": { "@types/node": "^20.0.0", "@types/ws": "^8.5.0", @@ -510,7 +521,22 @@ func (g *Generator) generatePackageJSON(spec *client.APISpec, config client.Gene } } `, jsonString(packageName), jsonString(config.Version), - jsonString(packageSummary(spec.Info.Description)), depsJSON) + jsonString(packageSummary(spec.Info.Description)), depsJSON, peerDepsJSON(config)) +} + +// peerDepsJSON renders the peerDependencies block, or "" when there are none. +// +// TanStack Query is a peer rather than a dependency: the hooks must share the +// QueryClient the application already created, and a second copy of the +// library in the tree means a second cache that no invalidation reaches. +func peerDepsJSON(config client.GeneratorConfig) string { + if !config.ReactQuery { + return "" + } + + return "\n \"peerDependencies\": {\n" + + " \"@tanstack/react-query\": \">=5\"\n" + + " }," } // jsonString renders a Go string as a JSON string literal, quotes included. @@ -1399,6 +1425,11 @@ func (g *Generator) generateIndex(spec *client.APISpec, config client.GeneratorC if config.Pagination && len(spec.Endpoints) > 0 { buf.WriteString("export * from './pagination';\n") } + + // Export the React Query hooks + if config.ReactQuery && len(spec.Endpoints) > 0 { + buf.WriteString("export * from './query';\n") + } } else { buf.WriteString("\n") } diff --git a/internal/client/generators/typescript/query.go b/internal/client/generators/typescript/query.go new file mode 100644 index 00000000..270fc39b --- /dev/null +++ b/internal/client/generators/typescript/query.go @@ -0,0 +1,365 @@ +package typescript + +import ( + "fmt" + "sort" + "strings" + + "github.com/xraph/forge/internal/client" +) + +// ReactQueryGenerator emits TanStack Query hooks over the generated REST +// client. +// +// A layer, not a second client. The hooks call the methods rest.go already +// produced and add caching, request deduplication and invalidation on top; +// nothing here re-derives the API surface from the specification. Deriving it +// twice is how a hook and the method it is supposed to wrap end up disagreeing +// about a parameter, which no compiler catches because both sides were +// generated from a spec that never changed. +type ReactQueryGenerator struct { + rest *RESTGenerator +} + +func NewReactQueryGenerator() *ReactQueryGenerator { + return &ReactQueryGenerator{rest: NewRESTGenerator()} +} + +// queryEndpoint is one endpoint with the access path its method sits at. +type queryEndpoint struct { + // Path is the access path on the client, e.g. ["networkmodel", "list"]. + Path []string + + Endpoint *client.Endpoint +} + +// accessor renders the call path: `client.networkmodel.list`. +func (q queryEndpoint) accessor() string { + return "client." + strings.Join(q.Path, ".") +} + +// hookName renders `useNetworkmodelList`. +func (q queryEndpoint) hookName() string { + var b strings.Builder + + b.WriteString("use") + + for _, part := range q.Path { + b.WriteString(toPascal(part)) + } + + return b.String() +} + +// isQuery reports whether the endpoint reads rather than writes. +// +// GET and HEAD are cacheable and become useQuery; everything else becomes +// useMutation. A mutation is not keyed and not cached, which is the whole +// distinction — caching a POST would serve a stale answer to a request whose +// entire purpose was to change something. +func (q queryEndpoint) isQuery() bool { + method := strings.ToUpper(q.Endpoint.Method) + + return method == "GET" || method == "HEAD" +} + +// Generate produces query.ts. +func (g *ReactQueryGenerator) Generate(spec *client.APISpec, config client.GeneratorConfig) (string, []string) { + endpoints := g.collect(spec) + if len(endpoints) == 0 { + return "", nil + } + + var warnings []string + + var buf strings.Builder + + buf.WriteString(`/** + * TanStack Query hooks over the generated client. + * + * Generated. Every hook calls a method on the REST client rather than issuing + * its own request, so there is one description of the API and one place a + * change to it lands. + * + * The client is passed in rather than read from a module-level singleton or a + * context this file invents. A generated file should not decide how an + * application provides its dependencies, and an explicit argument keeps these + * usable from a test without a provider tree. + */ + +`) + + buf.WriteString("import {\n useMutation,\n useQuery,\n type UseMutationOptions,\n type UseQueryOptions,\n} from '@tanstack/react-query';\n") + buf.WriteString("import type { RESTClient } from './rest';\n") + buf.WriteString("import * as types from './types';\n\n") + + g.writeQueryKeys(&buf, endpoints, spec) + g.writeHooks(&buf, endpoints, spec, config, &warnings) + + return buf.String(), warnings +} + +// writeQueryKeys emits the key builders. +func (g *ReactQueryGenerator) writeQueryKeys( + buf *strings.Builder, + endpoints []queryEndpoint, + spec *client.APISpec, +) { + buf.WriteString(`/** + * Cache keys. + * + * Every parameter the method accepts is part of its key. That is not + * thoroughness for its own sake: a key that omits a parameter serves one + * request's cached answer to a different request. Where an API is versioned + * along more than one axis — a valid time and a knowledge time, say — a key + * carrying only the first will happily return what is true now to a caller + * that asked what was known then, and the answer looks entirely plausible. + * + * Keys are prefixed by their access path, so invalidating a whole group is + * ` + "`queryClient.invalidateQueries({ queryKey: ['networkmodel'] })`" + `. + */ +export const queryKeys = { +`) + + for _, ep := range endpoints { + if !ep.isQuery() { + continue + } + + params := g.rest.methodParams(*ep.Endpoint, spec) + names := make([]string, 0, len(params)) + + for _, p := range params { + names = append(names, p.Name) + } + + var sig strings.Builder + + for i, p := range params { + if i > 0 { + sig.WriteString(", ") + } + + sig.WriteString(p.Name) + + if p.Optional { + sig.WriteString("?: ") + } else { + sig.WriteString(": ") + } + + sig.WriteString(p.TSType) + } + + literals := make([]string, 0, len(ep.Path)) + for _, part := range ep.Path { + literals = append(literals, "'"+part+"'") + } + + payload := "" + if len(names) > 0 { + payload = ", { " + strings.Join(names, ", ") + " }" + } + + fmt.Fprintf(buf, " %s: (%s) =>\n [%s%s] as const,\n", + g.keyName(ep), sig.String(), strings.Join(literals, ", "), payload) + } + + buf.WriteString("} as const;\n\n") +} + +// keyName renders the key builder's property name: `networkmodelList`. +func (g *ReactQueryGenerator) keyName(ep queryEndpoint) string { + if len(ep.Path) == 0 { + return "root" + } + + name := ep.Path[0] + for _, part := range ep.Path[1:] { + name += toPascal(part) + } + + return toCamel(name) +} + +// writeHooks emits one hook per endpoint. +func (g *ReactQueryGenerator) writeHooks( + buf *strings.Builder, + endpoints []queryEndpoint, + spec *client.APISpec, + config client.GeneratorConfig, + warnings *[]string, +) { + for _, ep := range endpoints { + params := g.rest.methodParams(*ep.Endpoint, spec) + returnType, _ := g.rest.generateReturnType(*ep.Endpoint, spec) + + if returnType == "" { + returnType = "void" + *warnings = append(*warnings, fmt.Sprintf( + "endpoint %q: no return type could be derived; its hook resolves to void", + endpointLabel(ep.Endpoint))) + } + + returnType = qualifyTypes(returnType) + + args := make([]string, 0, len(params)) + for _, p := range params { + args = append(args, p.Name) + } + + if ep.Endpoint.Description != "" { + fmt.Fprintf(buf, "/** %s */\n", strings.ReplaceAll(ep.Endpoint.Description, "\n", " ")) + } + + if ep.isQuery() { + g.writeQueryHook(buf, ep, params, args, returnType) + + continue + } + + g.writeMutationHook(buf, ep, params, args, returnType, config) + } +} + +func (g *ReactQueryGenerator) writeQueryHook( + buf *strings.Builder, + ep queryEndpoint, + params []MethodParam, + args []string, + returnType string, +) { + fmt.Fprintf(buf, "export function %s(\n client: RESTClient,\n", ep.hookName()) + + for _, p := range params { + if p.Optional { + fmt.Fprintf(buf, " %s?: %s,\n", p.Name, p.TSType) + + continue + } + + fmt.Fprintf(buf, " %s: %s,\n", p.Name, p.TSType) + } + + fmt.Fprintf(buf, + " options?: Omit, 'queryKey' | 'queryFn'>,\n) {\n", + returnType) + + fmt.Fprintf(buf, " return useQuery({\n queryKey: queryKeys.%s(%s),\n", + g.keyName(ep), strings.Join(args, ", ")) + + // The signal is forwarded so an unmounted component's request is actually + // cancelled rather than merely ignored. + callArgs := append(append([]string{}, args...), "{ signal }") + fmt.Fprintf(buf, " queryFn: ({ signal }) => %s(%s),\n", + ep.accessor(), strings.Join(callArgs, ", ")) + + buf.WriteString(" ...options,\n });\n}\n\n") +} + +func (g *ReactQueryGenerator) writeMutationHook( + buf *strings.Builder, + ep queryEndpoint, + params []MethodParam, + args []string, + returnType string, + _ client.GeneratorConfig, +) { + varsType := "void" + if len(params) > 0 { + fields := make([]string, 0, len(params)) + + for _, p := range params { + marker := ": " + if p.Optional { + marker = "?: " + } + + fields = append(fields, p.Name+marker+p.TSType) + } + + varsType = "{ " + strings.Join(fields, "; ") + " }" + } + + fmt.Fprintf(buf, "export function %s(\n client: RESTClient,\n", ep.hookName()) + fmt.Fprintf(buf, + " options?: Omit, 'mutationFn'>,\n) {\n", + returnType, varsType) + + buf.WriteString(" return useMutation({\n") + + if len(params) == 0 { + fmt.Fprintf(buf, " mutationFn: () => %s(),\n", ep.accessor()) + } else { + destructure := strings.Join(args, ", ") + fmt.Fprintf(buf, " mutationFn: ({ %s }: %s) => %s(%s),\n", + destructure, varsType, ep.accessor(), destructure) + } + + buf.WriteString(" ...options,\n });\n}\n\n") +} + +// collect walks the endpoint tree the REST generator builds, so hook access +// paths match the methods that actually exist. +func (g *ReactQueryGenerator) collect(spec *client.APISpec) []queryEndpoint { + root := g.rest.buildEndpointTree(spec.Endpoints) + + var out []queryEndpoint + + var walk func(node *EndpointNode, path []string) + + walk = func(node *EndpointNode, path []string) { + names := make([]string, 0, len(node.Children)) + for name := range node.Children { + names = append(names, name) + } + + // Sorted: map order is random, and a generator whose output changes + // between runs cannot be reviewed in a diff. + sort.Strings(names) + + for _, name := range names { + child := node.Children[name] + childPath := append(append([]string{}, path...), name) + + if child.IsLeaf && child.Endpoint != nil { + out = append(out, queryEndpoint{Path: childPath, Endpoint: child.Endpoint}) + + continue + } + + walk(child, childPath) + } + } + + walk(root, nil) + + return out +} + +// qualifyTypes prefixes bare schema names with the types namespace. +// +// generateReturnType renders names as the REST file refers to them, and that +// file imports the namespace under the same alias — so a union like +// "types.A | void" arrives already qualified while a bare "void" or "string" +// must be left alone. +func qualifyTypes(t string) string { + parts := strings.Split(t, " | ") + for i, part := range parts { + trimmed := strings.TrimSpace(part) + switch trimmed { + case "void", "string", "Blob", "unknown", "any", "number", "boolean": + parts[i] = trimmed + default: + if strings.HasPrefix(trimmed, "types.") { + parts[i] = trimmed + + continue + } + + parts[i] = "types." + trimmed + } + } + + return strings.Join(parts, " | ") +} diff --git a/internal/client/generators/typescript/query_internal_test.go b/internal/client/generators/typescript/query_internal_test.go new file mode 100644 index 00000000..942a1c91 --- /dev/null +++ b/internal/client/generators/typescript/query_internal_test.go @@ -0,0 +1,183 @@ +package typescript + +import ( + "strings" + "testing" + + "github.com/xraph/forge/internal/client" +) + +func bitemporalSpec() *client.APISpec { + return &client.APISpec{ + Info: client.APIInfo{Title: "Grid", Version: "1.0.0"}, + Endpoints: []client.Endpoint{ + { + OperationID: "networkmodel.list", + Path: "/api/v1/models", + Method: "GET", + QueryParams: []client.Parameter{ + {Name: "limit", In: "query", Schema: &client.Schema{Type: "integer"}}, + {Name: "validAt", In: "query", Schema: &client.Schema{Type: "string"}}, + {Name: "knownAt", In: "query", Schema: &client.Schema{Type: "string"}}, + }, + Responses: map[int]*client.Response{ + 200: {Content: map[string]*client.MediaType{ + "application/json": {Schema: &client.Schema{Ref: "#/components/schemas/ModelList"}}, + }}, + }, + }, + { + OperationID: "networkmodel.create", + Path: "/api/v1/models", + Method: "POST", + RequestBody: &client.RequestBody{ + Required: true, + Content: map[string]*client.MediaType{ + "application/json": {Schema: &client.Schema{Ref: "#/components/schemas/NewModel"}}, + }, + }, + Responses: map[int]*client.Response{ + 201: {Content: map[string]*client.MediaType{ + "application/json": {Schema: &client.Schema{Ref: "#/components/schemas/NetworkModel"}}, + }}, + }, + }, + }, + Schemas: map[string]*client.Schema{ + "ModelList": {Type: "object"}, + "NewModel": {Type: "object"}, + "NetworkModel": {Type: "object"}, + }, + } +} + +// TestQueryKeysCarryEveryParameter is the property the whole layer exists for. +// +// A key missing a parameter serves one request's cached answer to a different +// request. With two independent time axes that failure is silent and +// plausible: a key on validAt alone returns what is true now to a caller that +// asked what was known then. +func TestQueryKeysCarryEveryParameter(t *testing.T) { + out, _ := NewReactQueryGenerator().Generate(bitemporalSpec(), client.GeneratorConfig{ + APIName: "Client", + ReactQuery: true, + }) + + if !strings.Contains(out, "networkmodelList: (limit?:") { + t.Fatalf("no key builder for networkmodel.list:\n%s", out) + } + + for _, param := range []string{"limit", "validAt", "knownAt"} { + if !strings.Contains(out, param+",") && !strings.Contains(out, param+" }") { + t.Errorf("query key omits %q, so a cached answer can serve a different request", param) + } + } + + if !strings.Contains(out, "['networkmodel', 'list', { limit, validAt, knownAt }] as const") { + t.Errorf("key payload is not the full parameter set:\n%s", out) + } +} + +// TestHooksCallTheGeneratedClient pins the layering: hooks must delegate, not +// issue their own requests. +func TestHooksCallTheGeneratedClient(t *testing.T) { + out, _ := NewReactQueryGenerator().Generate(bitemporalSpec(), client.GeneratorConfig{ + APIName: "Client", + ReactQuery: true, + }) + + if !strings.Contains(out, "client.networkmodel.list(limit, validAt, knownAt, { signal })") { + t.Errorf("hook does not delegate to the generated method:\n%s", out) + } + + // Re-deriving the request would mean a fetch or a URL in this file. + for _, forbidden := range []string{"fetch(", "new Request(", "`/api/v1"} { + if strings.Contains(out, forbidden) { + t.Errorf("hooks re-derive the API surface (%q); they must call the client", forbidden) + } + } +} + +// TestReadsAreQueriesWritesAreMutations covers the split. +func TestReadsAreQueriesWritesAreMutations(t *testing.T) { + out, _ := NewReactQueryGenerator().Generate(bitemporalSpec(), client.GeneratorConfig{ + APIName: "Client", + ReactQuery: true, + }) + + if !strings.Contains(out, "export function useNetworkmodelList(") || + !strings.Contains(out, "return useQuery({") { + t.Error("a GET should become a useQuery hook") + } + + if !strings.Contains(out, "export function useNetworkmodelCreate(") || + !strings.Contains(out, "return useMutation({") { + t.Error("a POST should become a useMutation hook") + } + + // A mutation must not be keyed: caching a write serves a stale answer to + // a request whose entire purpose was to change something. + // + // Bounded to the function body. Hooks are emitted in sorted order, so + // "create" precedes "list" and slicing to the end of the file would sweep + // in a legitimate query key from the next hook. + start := strings.Index(out, "export function useNetworkmodelCreate(") + if start < 0 { + t.Fatal("no mutation hook to inspect") + } + + end := strings.Index(out[start:], "\n}\n") + if end < 0 { + t.Fatal("mutation hook body is unterminated") + } + + if body := out[start : start+end]; strings.Contains(body, "queryKey:") { + t.Errorf("a mutation must not carry a query key:\n%s", body) + } +} + +// TestGeneratedOutputIsDeterministic guards the map iteration in collect(). +func TestGeneratedOutputIsDeterministic(t *testing.T) { + config := client.GeneratorConfig{APIName: "Client", ReactQuery: true} + + first, _ := NewReactQueryGenerator().Generate(bitemporalSpec(), config) + + for i := 0; i < 12; i++ { + next, _ := NewReactQueryGenerator().Generate(bitemporalSpec(), config) + if next != first { + t.Fatal("output varies between runs; a generator whose bytes move cannot be reviewed in a diff") + } + } +} + +// TestNoEndpointsProducesNoFile keeps an empty module out of the package. +func TestNoEndpointsProducesNoFile(t *testing.T) { + out, _ := NewReactQueryGenerator().Generate(&client.APISpec{}, client.GeneratorConfig{ + ReactQuery: true, + }) + + if out != "" { + t.Errorf("a spec with no endpoints should produce no query module, got:\n%s", out) + } +} + +// TestPeerDependencyOnlyWhenGenerated: a non-React consumer must not inherit +// a dependency on a UI library. +func TestPeerDependencyOnlyWhenGenerated(t *testing.T) { + g := &Generator{} + spec := &client.APISpec{Info: client.APIInfo{Title: "Grid", Version: "1.0.0"}} + + with := g.generatePackageJSON(spec, client.GeneratorConfig{ + PackageName: "@scope/c", Version: "1.0.0", ReactQuery: true, + }) + if !strings.Contains(with, "@tanstack/react-query") { + t.Error("hooks generated but no peer dependency declared") + } + + without := g.generatePackageJSON(spec, client.GeneratorConfig{ + PackageName: "@scope/c", Version: "1.0.0", + }) + if strings.Contains(without, "@tanstack/react-query") { + t.Error("a client without hooks must not depend on react-query") + } +} From 2cca6a1ebb4e671cfd52ab2d594dca9f93e4d371 Mon Sep 17 00:00:00 2001 From: Rex Raphael Date: Mon, 3 Aug 2026 13:35:01 -0500 Subject: [PATCH 11/11] fix(client): make generated output satisfy exactOptionalPropertyTypes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Optional properties were declared `?: T`, and the generated methods assign possibly-undefined values into them (`signal: options?.signal`). Under exactOptionalPropertyTypes those are different types, so a client generated for a strict project produced thirty errors the moment that project compiled it — in a workspace where the package is consumed from source, which is the normal monorepo arrangement, that is every build. Optional properties are now `?: T | undefined`, and the fetch call spreads its body rather than assigning `undefined` to a field RequestInit declares optional. The alternative is every consumer turning the check off, which is how a generated client quietly stops being typechecked at all. --- .../client/generators/typescript/codecs.go | 4 ++-- .../generators/typescript/fetch_client.go | 22 ++++++++++++++----- .../client/generators/typescript/generator.go | 2 +- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/internal/client/generators/typescript/codecs.go b/internal/client/generators/typescript/codecs.go index cdc20183..92c24885 100644 --- a/internal/client/generators/typescript/codecs.go +++ b/internal/client/generators/typescript/codecs.go @@ -1006,7 +1006,7 @@ func (g *CodecGenerator) Generate(spec *client.APISpec, config client.GeneratorC buf.WriteString("// and decode below walk this table to rename between the two.\n\n") buf.WriteString("export type Codec =\n") - buf.WriteString(" | { kind: 'object'; fields: Record; required?: string[]; values?: string }\n") + buf.WriteString(" | { kind: 'object'; fields: Record; required?: string[]; values?: string }\n") buf.WriteString(" | { kind: 'array'; items?: string }\n") buf.WriteString(" | { kind: 'record'; values?: string }\n") buf.WriteString(" | { kind: 'union'; discriminator?: { wire: string; map: Record }; members: string[] }\n") @@ -1168,7 +1168,7 @@ function walk(value: unknown, id: string | undefined, toTS: boolean): unknown { // Build the rename map in the requested direction. Decoding maps a // wire key to its ts name; encoding maps back. - const rename = new Map(); + const rename = new Map(); for (const [wire, field] of Object.entries(codec.fields)) { if (toTS) { rename.set(wire, { to: field.ts, codec: field.codec }); diff --git a/internal/client/generators/typescript/fetch_client.go b/internal/client/generators/typescript/fetch_client.go index 5a9e1a58..c080f9a9 100644 --- a/internal/client/generators/typescript/fetch_client.go +++ b/internal/client/generators/typescript/fetch_client.go @@ -72,13 +72,21 @@ func (g *FetchClientGenerator) GenerateBaseClient(spec *client.APISpec, config c buf.WriteString(" * config -- `return { ...config, headers: { ...config.headers, ... } };` --\n") buf.WriteString(" * rather than building a replacement object from scratch.\n") buf.WriteString(" */\n") + // Optional properties are declared `?: T | undefined` throughout. + // + // Under exactOptionalPropertyTypes — which a strict consumer may well have + // on — `foo?: T` and `foo: T | undefined` are different types, and an + // object literal assigning a possibly-undefined value to the former is an + // error. The generated methods do exactly that (`signal: options?.signal`), + // so a client generated without the widening simply cannot be compiled by + // the projects most likely to want it. buf.WriteString("export interface RequestConfig {\n") buf.WriteString(" method: string;\n") buf.WriteString(" url: string;\n") - buf.WriteString(" headers?: Record;\n") - buf.WriteString(" body?: any;\n") - buf.WriteString(" signal?: AbortSignal;\n") - buf.WriteString(" retry?: RetryConfig;\n") + buf.WriteString(" headers?: Record | undefined;\n") + buf.WriteString(" body?: any | undefined;\n") + buf.WriteString(" signal?: AbortSignal | undefined;\n") + buf.WriteString(" retry?: RetryConfig | undefined;\n") buf.WriteString(" // Set by the generated method when its declared return type has a\n") buf.WriteString(" // no-content 2xx response (i.e. includes `void`). Only then does an\n") buf.WriteString(" // empty response body mean \"there is legitimately nothing here\" —\n") @@ -466,7 +474,11 @@ func (g *FetchClientGenerator) GenerateBaseClient(spec *client.APISpec, config c buf.WriteString(" let response = await fetch(url, {\n") buf.WriteString(" method: requestConfig.method,\n") buf.WriteString(" headers,\n") - buf.WriteString(" body,\n") + // Spread rather than assigned. RequestInit declares `body?: BodyInit | null`, + // and under exactOptionalPropertyTypes an explicit `body: undefined` is not + // the same as an absent one — a GET with no body would fail to compile in + // any consumer with that setting on. + buf.WriteString(" ...(body === undefined ? {} : { body }),\n") buf.WriteString(" signal,\n") buf.WriteString(" });\n\n") diff --git a/internal/client/generators/typescript/generator.go b/internal/client/generators/typescript/generator.go index 9fe4c582..7e444eb8 100644 --- a/internal/client/generators/typescript/generator.go +++ b/internal/client/generators/typescript/generator.go @@ -1345,7 +1345,7 @@ func (g *Generator) generateClient(spec *client.APISpec, config client.Generator buf.WriteString(" protected httpClient: HTTPClient;\n") if config.IncludeAuth { - buf.WriteString(" private auth?: AuthConfig;\n\n") + buf.WriteString(" private auth?: AuthConfig | undefined;\n\n") } else { buf.WriteString("\n") }