diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 463ddcdc9..e2650d7f7 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,6 +6,9 @@ "version": "10.28.0" } }, + "containerEnv": { + "ASTRO_TELEMETRY_DISABLED": "1" + }, "customizations": { "codespaces": { "openFiles": [ diff --git a/.github/astro.instructions.md b/.github/astro.instructions.md index 2da9248b4..35ba19412 100644 --- a/.github/astro.instructions.md +++ b/.github/astro.instructions.md @@ -93,6 +93,7 @@ The site uses these Starlight plugins (configured in `astro.config.mjs`): Custom overrides live in `src/components/starlight/` and are registered in `astro.config.mjs` under `components:`: +- `Banner.astro` — dismissible announcement banner (content hashed for a stable dismiss key). Supports declarative auto-expiry via two optional **top-level** frontmatter fields: `bannerExpiresOn` (a `YYYY-MM-DD` sunset date that hides it for everyone; a past date drops it at build time) and `bannerAutoDismissAfterDays` (a per-reader window that auto-hides it N days after they first see it, tracked in `localStorage`). These are top-level (not nested under `banner`) because extra keys nested inside Starlight's built-in `banner` object are stripped before reaching the component. Pure decision logic lives in `src/utils/banner-expiry.ts` and is **imported directly** by the client script (single source of truth — the shipped behavior can't diverge from the unit tests). - `EditLink.astro` — adds translation link - `Footer.astro` — custom 4-column footer layout - `Head.astro` — git metadata, auto-language detection, accessibility @@ -122,7 +123,7 @@ Always follow existing component patterns: `.astro` files with frontmatter props Defined in `src/content.config.ts`: -- **docs** — uses Starlight's docs loader with extended schema fields: `renderBlocking`, `giscus`, `category`, `pageActions` +- **docs** — uses Starlight's docs loader with extended schema fields: `renderBlocking`, `giscus`, `category`, `pageActions`, and top-level banner auto-expiry fields (`bannerExpiresOn`, `bannerAutoDismissAfterDays`) - **i18n** — Starlight i18n loader for 15 locales - **packages** — auto-generated API reference JSON from `src/data/pkgs/` diff --git a/.github/forbidden-words.json b/.github/forbidden-words.json index 34a9c7c06..5d4fb086d 100644 --- a/.github/forbidden-words.json +++ b/.github/forbidden-words.json @@ -18,6 +18,11 @@ "replacement": "Aspire", "message": "Use \"Aspire\" instead of \"dotnet aspire\"." }, + { + "pattern": "(?<=polyglot )\\bapp hosts\\b", + "replacement": "AppHosts", + "message": "Use \"polyglot AppHosts\" instead of \"polyglot app hosts\"." + }, { "pattern": "\\bapp host\\b", "replacement": "AppHost", diff --git a/.github/workflows/update-integration-data.yml b/.github/workflows/update-integration-data.yml index 127d1513b..d15827624 100644 --- a/.github/workflows/update-integration-data.yml +++ b/.github/workflows/update-integration-data.yml @@ -211,6 +211,7 @@ jobs: echo "- Changed: \`${{ steps.update.outputs.changed || 'unknown' }}\`" echo "- Versions changed: \`${{ steps.update.outputs.versions_changed || 'n/a' }}\`" echo "- API regen ran: \`${{ steps.update.outputs.regen_ran || 'n/a' }}\`" + echo "- Semantic validation: \`${{ steps.update.outputs.semantic_validation || 'not run' }}\`" if [ -n "${{ steps.update.outputs.icon_warnings }}" ]; then echo "- Icon warnings: \`${{ steps.update.outputs.icon_warnings }}\`" fi diff --git a/src/frontend/config/sidebar/docs.topics.ts b/src/frontend/config/sidebar/docs.topics.ts index cf23dff1c..be4f35931 100644 --- a/src/frontend/config/sidebar/docs.topics.ts +++ b/src/frontend/config/sidebar/docs.topics.ts @@ -628,6 +628,27 @@ export const docsTopics: StarlightSidebarTopicsUserConfig = { }, slug: 'fundamentals/service-discovery', }, + { + label: 'Environment variables', + slug: 'fundamentals/environment-variables', + translations: { + da: 'Miljøvariabler', + de: 'Umgebungsvariablen', + en: 'Environment variables', + es: 'Variables de entorno', + fr: "Variables d'environnement", + hi: 'पर्यावरण चर', + id: 'Variabel lingkungan', + it: 'Variabili di ambiente', + ja: '環境変数', + ko: '환경 변수', + 'pt-BR': 'Variáveis de ambiente', + ru: 'Переменные среды', + tr: 'Ortam değişkenleri', + uk: 'Змінні середовища', + 'zh-CN': '环境变量', + }, + }, { label: 'Networking overview', slug: 'fundamentals/networking-overview', diff --git a/src/frontend/package.json b/src/frontend/package.json index b03719f64..fc47d7329 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -55,7 +55,8 @@ "update:ts-api": "tsx ./scripts/update-ts-api.ts", "update:github-stats": "tsx ./scripts/update-github-stats.ts", "update:samples": "tsx ./scripts/update-samples.ts", - "normalize:api-data": "tsx ./scripts/normalize-generated-api-data.ts" + "normalize:api-data": "tsx ./scripts/normalize-generated-api-data.ts", + "validate:api-data": "tsx ./scripts/validate-generated-api-data.ts" }, "dependencies": { "@astro-community/astro-embed-vimeo": "^0.3.12", @@ -81,7 +82,7 @@ "astro-tooltips": "^0.6.2", "hast-util-to-html": "^9.0.5", "mdast-util-to-hast": "^13.2.1", - "mermaid": "^11.16.0", + "mermaid": "^11.16.1", "remark-directive": "^4.0.0", "satori": "^0.28.1", "satteri": "^0.9.5", diff --git a/src/frontend/pnpm-lock.yaml b/src/frontend/pnpm-lock.yaml index 5d7d440d5..58fd7b944 100644 --- a/src/frontend/pnpm-lock.yaml +++ b/src/frontend/pnpm-lock.yaml @@ -12,7 +12,7 @@ overrides: '@cacheable/utils@2.5.1': 2.5.0 ajv@<6.14.0: 6.14.0 ajv@>=7.0.0-alpha.0 <8.18.0: 8.18.0 - brace-expansion@<5.0.8: '>=5.0.8' + brace-expansion@<5.0.9: '>=5.0.9' cache-manager@7.2.10: 7.2.9 cacheable-request@13.0.20: 13.0.19 cacheable@2.5.1: 2.5.0 @@ -26,11 +26,13 @@ overrides: flat-cache@6.1.24: 6.1.23 flatted@<3.4.2: '>=3.4.2' h3@<1.15.9: '>=1.15.9' + js-yaml@>=4.0.0 <4.3.1: 4.3.1 keyv@6.0.0: 5.6.0 lodash-es@>=4.0.0 <=4.17.22: '>=4.17.23' lodash@>=4.0.0 <=4.17.22: '>=4.17.23' minimatch@<10.2.3: '>=10.2.3' - postcss@<8.5.10: '>=8.5.10' + nanoid@<3.3.17: 3.3.17 + postcss@<8.5.23: '>=8.5.23' rollup@>=4.0.0 <4.59.0: '>=4.59.0' seroval: '>=1.4.1' simple-git@<3.36.0: '>=3.36.0' @@ -103,7 +105,7 @@ importers: version: 0.44.1(astro@7.1.3(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.3)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0)) astro-mermaid: specifier: ^2.1.0 - version: 2.1.0(astro@7.1.3(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.3)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0))(mermaid@11.16.0) + version: 2.1.0(astro@7.1.3(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.3)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0))(mermaid@11.16.1) astro-tooltips: specifier: ^0.6.2 version: 0.6.2 @@ -114,8 +116,8 @@ importers: specifier: ^13.2.1 version: 13.2.1 mermaid: - specifier: ^11.16.0 - version: 11.16.0 + specifier: ^11.16.1 + version: 11.16.1 remark-directive: specifier: ^4.0.0 version: 4.0.0 @@ -822,9 +824,6 @@ packages: '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} - '@iconify/utils@3.1.3': - resolution: {integrity: sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==} - '@iconify/utils@3.1.4': resolution: {integrity: sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==} @@ -1040,6 +1039,13 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@napi-rs/wasm-runtime@1.2.2': + resolution: {integrity: sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3 + '@nodable/entities@3.0.0': resolution: {integrity: sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==} @@ -1359,30 +1365,58 @@ packages: resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==} engines: {node: '>=20'} + '@shikijs/core@4.4.2': + resolution: {integrity: sha512-StyzbAyxg2/tBGf78gwbBkGyeQ73lf8UiJArFaQhTQIDqQOCKPCQFanvrs4/Yv3Yfyc+ONInJM6K+FMIf+P+kA==} + engines: {node: '>=20'} + '@shikijs/engine-javascript@4.3.1': resolution: {integrity: sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==} engines: {node: '>=20'} + '@shikijs/engine-javascript@4.4.2': + resolution: {integrity: sha512-MnIkeqWdVPUWsxlx8gKLVCJFTsqrQJgpTPBPpQwaFeJ56lOnJxj5aN2LUFnfxUEcvOQuNocmbaMnVrCEln6rkw==} + engines: {node: '>=20'} + '@shikijs/engine-oniguruma@4.3.1': resolution: {integrity: sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==} engines: {node: '>=20'} + '@shikijs/engine-oniguruma@4.4.2': + resolution: {integrity: sha512-GLhowz1+jixjz+wiZ3wMnOn1jTxiFCGl2PkXufivbnwPHKuyw1AYqu5/hbWhZZ2oAb0NP05WUJhYeigY14drnw==} + engines: {node: '>=20'} + '@shikijs/langs@4.3.1': resolution: {integrity: sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==} engines: {node: '>=20'} + '@shikijs/langs@4.4.2': + resolution: {integrity: sha512-8DfeusD+Zdv/eYIDdXyJTUnSMHt+aAWjAOCXV20HNGAHRlInXpG8wh421v6B91WOm9TFwRLN+b/LG5F2NAIojg==} + engines: {node: '>=20'} + '@shikijs/primitive@4.3.1': resolution: {integrity: sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==} engines: {node: '>=20'} + '@shikijs/primitive@4.4.2': + resolution: {integrity: sha512-l6fQQKsOMlz72n38fztmSgZ76MO6KSWuw8o+GJ+FhmqrpC9pIOJNQNXGgbb5yX2AwpzlEHwsaLPnk/8o4Fm+rA==} + engines: {node: '>=20'} + '@shikijs/themes@4.3.1': resolution: {integrity: sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==} engines: {node: '>=20'} + '@shikijs/themes@4.4.2': + resolution: {integrity: sha512-H0CFoL07ddDC2Dd6EdrPYNkRhUR6YCkJlnuYFceYYUJJA5TIm2b5B33qqiDYryBExgbKMndFJPb2u1gTuqO37g==} + engines: {node: '>=20'} + '@shikijs/types@4.3.1': resolution: {integrity: sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==} engines: {node: '>=20'} + '@shikijs/types@4.4.2': + resolution: {integrity: sha512-PFYitV4vpDr/iPCIhnHp+Q4ftic5N5VeNJ3KQ1O8gn3h2ar8qgwMAXF7tq4m1CWaMS60fV4VqF6vfnWH4F7vqQ==} + engines: {node: '>=20'} + '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -1466,8 +1500,8 @@ packages: '@types/d3-format@3.0.4': resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} - '@types/d3-geo@3.1.0': - resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + '@types/d3-geo@3.1.1': + resolution: {integrity: sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==} '@types/d3-hierarchy@3.1.7': resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} @@ -1484,8 +1518,8 @@ packages: '@types/d3-quadtree@3.0.6': resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} - '@types/d3-random@3.0.3': - resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + '@types/d3-random@3.0.4': + resolution: {integrity: sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==} '@types/d3-scale-chromatic@3.1.0': resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} @@ -1672,26 +1706,6 @@ packages: '@typescript-eslint/visitor-keys@8.65.0': resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - - - - - - - - - - - - - - - - - - - '@ungap/structured-clone@1.3.2': resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==} @@ -1980,8 +1994,8 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - brace-expansion@5.0.8: - resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -2357,8 +2371,8 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.4.12: - resolution: {integrity: sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==} + dompurify@3.4.13: + resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==} domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -2397,8 +2411,8 @@ packages: es-module-lexer@2.3.1: resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} - es-toolkit@1.49.0: - resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==} + es-toolkit@1.50.0: + resolution: {integrity: sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==} esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} @@ -2842,8 +2856,8 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} hasBin: true json-buffer@3.0.1: @@ -2882,78 +2896,78 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [android] - lightningcss-darwin-arm64@1.32.0: - resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.32.0: - resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] - lightningcss-freebsd-x64@1.32.0: - resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - lightningcss-linux-arm64-gnu@1.32.0: - resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] libc: [glibc] - lightningcss-linux-arm64-musl@1.32.0: - resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] libc: [musl] - lightningcss-linux-x64-gnu@1.32.0: - resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] libc: [glibc] - lightningcss-linux-x64-musl@1.32.0: - resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] libc: [musl] - lightningcss-win32-arm64-msvc@1.32.0: - resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] - lightningcss-win32-x64-msvc@1.32.0: - resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] - lightningcss@1.32.0: - resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} linebreak@1.1.0: @@ -3065,8 +3079,8 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - mermaid@11.16.0: - resolution: {integrity: sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==} + mermaid@11.16.1: + resolution: {integrity: sha512-TQsq6u22fAn3rek5VOubrhKPo1g5hwC3FXUN9hiyupTckcYiGuuKGkNQrKYwGJkXUxZdojwRG46gsSCFZMDp4g==} micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -3201,8 +3215,8 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} - nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + nanoid@3.3.17: + resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -3297,6 +3311,9 @@ packages: package-manager-detector@1.7.0: resolution: {integrity: sha512-xg1eHpwYL/D/HEdWw2goFZP6vV0FH7W+PZ5rFkGjdIDLtxq7EkzBUeT3m+lndYCt8wKbmofUu1MUdMCXkCk9ZQ==} + package-manager-detector@1.8.0: + resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} + pagefind@1.5.2: resolution: {integrity: sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==} hasBin: true @@ -3400,8 +3417,8 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.20: - resolution: {integrity: sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==} + postcss@8.5.25: + resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -3626,6 +3643,10 @@ packages: resolution: {integrity: sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==} engines: {node: '>=20'} + shiki@4.4.2: + resolution: {integrity: sha512-P8F/dFhRevaw2uSdeIYlq/5SXZNY85DPtmXQ947gD1Zj2JqO5AkNvVVBar0Me9JkFx3uzVud/qOtP5ek9NEQGA==} + engines: {node: '>=20'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -3795,6 +3816,10 @@ packages: resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} engines: {node: '>=18'} + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -4219,8 +4244,8 @@ snapshots: '@antfu/install-pkg@1.1.0': dependencies: - package-manager-detector: 1.7.0 - tinyexec: 1.2.4 + package-manager-detector: 1.8.0 + tinyexec: 1.3.0 '@astro-community/astro-embed-baseline-status@0.2.2': dependencies: @@ -4340,7 +4365,7 @@ snapshots: dependencies: '@types/hast': 3.0.5 '@types/mdast': 4.0.4 - js-yaml: 4.3.0 + js-yaml: 4.3.1 picomatch: 4.0.5 retext-smartypants: 6.2.0 shiki: 4.3.1 @@ -4432,7 +4457,7 @@ snapshots: hast-util-to-string: 3.0.1 hastscript: 9.0.1 i18next: 26.3.6(typescript@6.0.3) - js-yaml: 4.3.0 + js-yaml: 4.3.1 klona: 2.0.6 magic-string: 0.30.21 mdast-util-directive: 3.1.0 @@ -4754,8 +4779,8 @@ snapshots: hast-util-to-html: 9.0.5 hast-util-to-text: 4.0.2 hastscript: 9.0.1 - postcss: 8.5.20 - postcss-nested: 6.2.0(postcss@8.5.20) + postcss: 8.5.25 + postcss-nested: 6.2.0(postcss@8.5.25) unist-util-visit: 5.1.0 unist-util-visit-parents: 6.0.2 @@ -4774,7 +4799,7 @@ snapshots: '@expressive-code/plugin-shiki@0.44.1': dependencies: '@expressive-code/core': 0.44.1 - shiki: 4.3.1 + shiki: 4.4.2 '@expressive-code/plugin-text-markers@0.44.1': dependencies: @@ -4810,12 +4835,6 @@ snapshots: '@iconify/types@2.0.0': {} - '@iconify/utils@3.1.3': - dependencies: - '@antfu/install-pkg': 1.1.0 - '@iconify/types': 2.0.0 - import-meta-resolve: 4.2.0 - '@iconify/utils@3.1.4': dependencies: '@antfu/install-pkg': 1.1.0 @@ -5028,6 +5047,13 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true + '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + '@nodable/entities@3.0.0': {} '@nodelib/fs.scandir@2.1.5': @@ -5191,7 +5217,7 @@ snapshots: dependencies: '@emnapi/core': 1.11.1 '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true '@rolldown/binding-win32-arm64-msvc@1.1.5': @@ -5216,36 +5242,74 @@ snapshots: '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 + '@shikijs/core@4.4.2': + dependencies: + '@shikijs/primitive': 4.4.2 + '@shikijs/types': 4.4.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + hast-util-to-html: 9.0.5 + '@shikijs/engine-javascript@4.3.1': dependencies: '@shikijs/types': 4.3.1 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.6 + '@shikijs/engine-javascript@4.4.2': + dependencies: + '@shikijs/types': 4.4.2 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.6 + '@shikijs/engine-oniguruma@4.3.1': dependencies: '@shikijs/types': 4.3.1 '@shikijs/vscode-textmate': 10.0.2 + '@shikijs/engine-oniguruma@4.4.2': + dependencies: + '@shikijs/types': 4.4.2 + '@shikijs/vscode-textmate': 10.0.2 + '@shikijs/langs@4.3.1': dependencies: '@shikijs/types': 4.3.1 + '@shikijs/langs@4.4.2': + dependencies: + '@shikijs/types': 4.4.2 + '@shikijs/primitive@4.3.1': dependencies: '@shikijs/types': 4.3.1 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 + '@shikijs/primitive@4.4.2': + dependencies: + '@shikijs/types': 4.4.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + '@shikijs/themes@4.3.1': dependencies: '@shikijs/types': 4.3.1 + '@shikijs/themes@4.4.2': + dependencies: + '@shikijs/types': 4.4.2 + '@shikijs/types@4.3.1': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 + '@shikijs/types@4.4.2': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + '@shikijs/vscode-textmate@10.0.2': {} '@shuding/opentype.js@1.4.0-beta.0': @@ -5325,7 +5389,7 @@ snapshots: '@types/d3-format@3.0.4': {} - '@types/d3-geo@3.1.0': + '@types/d3-geo@3.1.1': dependencies: '@types/geojson': 7946.0.16 @@ -5341,7 +5405,7 @@ snapshots: '@types/d3-quadtree@3.0.6': {} - '@types/d3-random@3.0.3': {} + '@types/d3-random@3.0.4': {} '@types/d3-scale-chromatic@3.1.0': {} @@ -5386,13 +5450,13 @@ snapshots: '@types/d3-fetch': 3.0.7 '@types/d3-force': 3.0.10 '@types/d3-format': 3.0.4 - '@types/d3-geo': 3.1.0 + '@types/d3-geo': 3.1.1 '@types/d3-hierarchy': 3.1.7 '@types/d3-interpolate': 3.0.4 '@types/d3-path': 3.1.1 '@types/d3-polygon': 3.0.2 '@types/d3-quadtree': 3.0.6 - '@types/d3-random': 3.0.3 + '@types/d3-random': 3.0.4 '@types/d3-scale': 4.0.9 '@types/d3-scale-chromatic': 3.1.0 '@types/d3-selection': 3.0.11 @@ -5602,26 +5666,6 @@ snapshots: dependencies: '@typescript-eslint/types': 8.65.0 eslint-visitor-keys: 5.0.1 - - - - - - - - - - - - - - - - - - - - '@ungap/structured-clone@1.3.2': {} @@ -5933,12 +5977,12 @@ snapshots: rehype-expressive-code: 0.44.1 url-extras: 0.1.0 - astro-mermaid@2.1.0(astro@7.1.3(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.3)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0))(mermaid@11.16.0): + astro-mermaid@2.1.0(astro@7.1.3(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.3)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0))(mermaid@11.16.1): dependencies: astro: 7.1.3(@astrojs/markdown-remark@7.2.1)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.13.3)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0) import-meta-resolve: 4.2.0 mdast-util-to-string: 4.0.0 - mermaid: 11.16.0 + mermaid: 11.16.1 unist-util-visit: 5.1.0 astro-tooltips@0.6.2: @@ -5981,7 +6025,7 @@ snapshots: github-slugger: 2.0.0 html-escaper: 3.0.3 http-cache-semantics: 4.2.0 - js-yaml: 4.3.0 + js-yaml: 4.3.1 jsonc-parser: 3.3.1 magic-string: 0.30.21 magicast: 0.5.3 @@ -6068,7 +6112,7 @@ snapshots: boolbase@1.0.0: {} - brace-expansion@5.0.8: + brace-expansion@5.0.9: dependencies: balanced-match: 4.0.4 @@ -6434,7 +6478,7 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.4.12: + dompurify@3.4.13: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -6462,7 +6506,7 @@ snapshots: es-module-lexer@2.3.1: {} - es-toolkit@1.49.0: {} + es-toolkit@1.50.0: {} esast-util-from-estree@2.0.0: dependencies: @@ -7094,7 +7138,7 @@ snapshots: jiti@2.7.0: {} - js-yaml@4.3.0: + js-yaml@4.3.1: dependencies: argparse: 2.0.1 @@ -7127,54 +7171,54 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lightningcss-android-arm64@1.32.0: + lightningcss-android-arm64@1.33.0: optional: true - lightningcss-darwin-arm64@1.32.0: + lightningcss-darwin-arm64@1.33.0: optional: true - lightningcss-darwin-x64@1.32.0: + lightningcss-darwin-x64@1.33.0: optional: true - lightningcss-freebsd-x64@1.32.0: + lightningcss-freebsd-x64@1.33.0: optional: true - lightningcss-linux-arm-gnueabihf@1.32.0: + lightningcss-linux-arm-gnueabihf@1.33.0: optional: true - lightningcss-linux-arm64-gnu@1.32.0: + lightningcss-linux-arm64-gnu@1.33.0: optional: true - lightningcss-linux-arm64-musl@1.32.0: + lightningcss-linux-arm64-musl@1.33.0: optional: true - lightningcss-linux-x64-gnu@1.32.0: + lightningcss-linux-x64-gnu@1.33.0: optional: true - lightningcss-linux-x64-musl@1.32.0: + lightningcss-linux-x64-musl@1.33.0: optional: true - lightningcss-win32-arm64-msvc@1.32.0: + lightningcss-win32-arm64-msvc@1.33.0: optional: true - lightningcss-win32-x64-msvc@1.32.0: + lightningcss-win32-x64-msvc@1.33.0: optional: true - lightningcss@1.32.0: + lightningcss@1.33.0: dependencies: detect-libc: 2.1.2 optionalDependencies: - lightningcss-android-arm64: 1.32.0 - lightningcss-darwin-arm64: 1.32.0 - lightningcss-darwin-x64: 1.32.0 - lightningcss-freebsd-x64: 1.32.0 - lightningcss-linux-arm-gnueabihf: 1.32.0 - lightningcss-linux-arm64-gnu: 1.32.0 - lightningcss-linux-arm64-musl: 1.32.0 - lightningcss-linux-x64-gnu: 1.32.0 - lightningcss-linux-x64-musl: 1.32.0 - lightningcss-win32-arm64-msvc: 1.32.0 - lightningcss-win32-x64-msvc: 1.32.0 + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 linebreak@1.1.0: dependencies: @@ -7410,10 +7454,10 @@ snapshots: merge2@1.4.1: {} - mermaid@11.16.0: + mermaid@11.16.1: dependencies: '@braintree/sanitize-url': 7.1.2 - '@iconify/utils': 3.1.3 + '@iconify/utils': 3.1.4 '@mermaid-js/parser': 1.2.0 '@types/d3': 7.4.3 '@upsetjs/venn.js': 2.0.0 @@ -7424,8 +7468,8 @@ snapshots: d3-sankey: 0.12.3 dagre-d3-es: 7.0.14 dayjs: 1.11.21 - dompurify: 3.4.12 - es-toolkit: 1.49.0 + dompurify: 3.4.13 + es-toolkit: 1.50.0 katex: 0.16.47 khroma: 2.1.0 marked: 16.4.2 @@ -7715,7 +7759,7 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.8 + brace-expansion: 5.0.9 minipass@7.1.3: {} @@ -7732,7 +7776,7 @@ snapshots: muggle-string@0.4.1: {} - nanoid@3.3.16: {} + nanoid@3.3.17: {} natural-compare@1.4.0: {} @@ -7842,6 +7886,8 @@ snapshots: package-manager-detector@1.7.0: {} + package-manager-detector@1.8.0: {} + pagefind@1.5.2: optionalDependencies: '@pagefind/darwin-arm64': 1.5.2 @@ -7938,9 +7984,9 @@ snapshots: path-data-parser: 0.1.0 points-on-curve: 0.2.0 - postcss-nested@6.2.0(postcss@8.5.20): + postcss-nested@6.2.0(postcss@8.5.25): dependencies: - postcss: 8.5.20 + postcss: 8.5.25 postcss-selector-parser: 6.1.4 postcss-selector-parser@6.1.4: @@ -7950,9 +7996,9 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.5.20: + postcss@8.5.25: dependencies: - nanoid: 3.3.16 + nanoid: 3.3.17 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -8305,6 +8351,17 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 + shiki@4.4.2: + dependencies: + '@shikijs/core': 4.4.2 + '@shikijs/engine-javascript': 4.4.2 + '@shikijs/engine-oniguruma': 4.4.2 + '@shikijs/langs': 4.4.2 + '@shikijs/themes': 4.4.2 + '@shikijs/types': 4.4.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -8509,6 +8566,8 @@ snapshots: tinyexec@1.2.4: {} + tinyexec@1.3.0: {} + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.5) @@ -8573,7 +8632,6 @@ snapshots: typescript@6.0.3: {} - ufo@1.6.4: {} ultrahtml@1.7.0: {} @@ -8770,9 +8828,9 @@ snapshots: vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0): dependencies: - lightningcss: 1.32.0 + lightningcss: 1.33.0 picomatch: 4.0.5 - postcss: 8.5.20 + postcss: 8.5.25 rolldown: 1.1.5 tinyglobby: 0.2.17 optionalDependencies: diff --git a/src/frontend/pnpm-workspace.yaml b/src/frontend/pnpm-workspace.yaml index a317ab7ce..b6e055dd1 100644 --- a/src/frontend/pnpm-workspace.yaml +++ b/src/frontend/pnpm-workspace.yaml @@ -31,7 +31,7 @@ overrides: '@cacheable/utils@2.5.1': 2.5.0 ajv@<6.14.0: 6.14.0 ajv@>=7.0.0-alpha.0 <8.18.0: 8.18.0 - brace-expansion@<5.0.8: '>=5.0.8' + brace-expansion@<5.0.9: '>=5.0.9' cache-manager@7.2.10: 7.2.9 cacheable-request@13.0.20: 13.0.19 cacheable@2.5.1: 2.5.0 @@ -45,11 +45,13 @@ overrides: flat-cache@6.1.24: 6.1.23 flatted@<3.4.2: '>=3.4.2' h3@<1.15.9: '>=1.15.9' + js-yaml@>=4.0.0 <4.3.1: 4.3.1 keyv@6.0.0: 5.6.0 lodash-es@>=4.0.0 <=4.17.22: '>=4.17.23' lodash@>=4.0.0 <=4.17.22: '>=4.17.23' minimatch@<10.2.3: '>=10.2.3' - postcss@<8.5.10: '>=8.5.10' + nanoid@<3.3.17: 3.3.17 + postcss@<8.5.23: '>=8.5.23' rollup@>=4.0.0 <4.59.0: '>=4.59.0' seroval: '>=1.4.1' simple-git@<3.36.0: '>=3.36.0' diff --git a/src/frontend/scripts/aspire-terminology.ts b/src/frontend/scripts/aspire-terminology.ts index a313c6447..f749fc593 100644 --- a/src/frontend/scripts/aspire-terminology.ts +++ b/src/frontend/scripts/aspire-terminology.ts @@ -53,6 +53,10 @@ const terminologyRules: readonly TerminologyRule[] = [ replacement: 'Aspire', article: 'an', }, + { + term: String.raw`(?<=polyglot${horizontalWhitespace})app${horizontalWhitespace}hosts`, + replacement: 'AppHosts', + }, { term: String.raw`app${horizontalWhitespace}host`, replacement: 'AppHost', diff --git a/src/frontend/scripts/generate-twoslash-types.ts b/src/frontend/scripts/generate-twoslash-types.ts index 3fa817e29..d50fd0733 100644 --- a/src/frontend/scripts/generate-twoslash-types.ts +++ b/src/frontend/scripts/generate-twoslash-types.ts @@ -8,15 +8,21 @@ * after regenerating). */ -import { readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; +import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; import { dirname, resolve } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); -const MODULES_DIR = resolve(__dirname, '..', 'src', 'data', 'ts-modules'); -const PKGS_DIR = resolve(__dirname, '..', 'src', 'data', 'pkgs'); -const OUTPUT_DIR = resolve(__dirname, '..', 'src', 'data', 'twoslash'); -const OUTPUT_FILE = resolve(OUTPUT_DIR, 'aspire.d.ts'); +const MODULES_DIR = process.env.ASPIRE_API_TS_MODULES_DIR + ? resolve(process.env.ASPIRE_API_TS_MODULES_DIR) + : resolve(__dirname, '..', 'src', 'data', 'ts-modules'); +const PKGS_DIR = process.env.ASPIRE_API_PKGS_DIR + ? resolve(process.env.ASPIRE_API_PKGS_DIR) + : resolve(__dirname, '..', 'src', 'data', 'pkgs'); +const OUTPUT_FILE = process.env.ASPIRE_API_TWOSLASH_FILE + ? resolve(process.env.ASPIRE_API_TWOSLASH_FILE) + : resolve(__dirname, '..', 'src', 'data', 'twoslash', 'aspire.d.ts'); +const OUTPUT_DIR = dirname(OUTPUT_FILE); interface Parameter { name: string; @@ -43,6 +49,7 @@ interface FunctionEntry { interface DtoField { name: string; type: string; + isOptional?: boolean; } interface DtoType { @@ -65,6 +72,7 @@ interface HandleType { kind: 'handle'; exposeProperties?: boolean; implementedInterfaces?: string[]; + baseTypeHierarchy?: string[]; capabilities?: FunctionEntry[]; } @@ -78,6 +86,7 @@ interface ModuleJson { interface PkgTypeEntry { name: string; + fullName: string; kind: string; baseType?: string; } @@ -98,6 +107,10 @@ function lastDotted(id: string): string { return parts[parts.length - 1]; } +function withoutAssemblyPrefix(id: string): string { + return id.includes('/') ? id.slice(id.lastIndexOf('/') + 1) : id; +} + function cleanType(raw: string | undefined): string { if (!raw) return 'unknown'; let s = raw.trim(); @@ -211,6 +224,15 @@ const PARAM_TYPE_OVERRIDES: Record> = { withEnvironment: { value: 'string | IResourceWithConnectionString | IValueProvider', }, + // The polyglot `withParameter` dispatcher accepts the full Bicep parameter + // value union (string / string[] / parameter / connection-string resource / + // bicep output / reference expression / endpoint), but the dumped JSON only + // encodes the trailing `EndpointReference` overload. Mirror the real + // `[AspireUnion]` surface so docs can pass strings and parameter references. + withParameter: { + value: + 'string | string[] | ParameterResource | IResourceWithConnectionString | BicepOutputReference | ReferenceExpression | EndpointReference', + }, // Accept parameter-resource references in addition to literal strings, matching // the real SDK's overload for "publish as an existing Azure resource". publishAsExisting: { @@ -273,6 +295,7 @@ function optionsOverloadSplit(fnName: string, params: Parameter[]): number { } if (firstOpt < 0) return -1; const tail = params.slice(firstOpt); + if (tail.length === 1 && tail[0].name === 'options') return -1; const minTail = /^(add|with|publish)[A-Z0-9]/.test(fnName) ? 1 : 2; if (tail.length < minTail) return -1; // `with*` methods that take a callback (e.g. `withPgAdmin(configureContainer?)`) @@ -315,25 +338,26 @@ const modules: ModuleJson[] = files.map( console.log(`📚 Loaded ${modules.length} module JSON files`); -// Load class-inheritance metadata from the richer pkgs/*.json dumps. The -// ts-modules JSON captures implemented interfaces but not class-level `extends`, -// so resource types like ViteAppResource lose inherited methods such as -// publishAsDockerFile. Map each class short-name to its base class short-name. -const classBaseByName = new Map(); -try { +// Load class-inheritance metadata from the richer pkgs/*.json dumps. Older +// ts-modules snapshots omitted BaseTypeHierarchy, so this remains the fallback. +// Key by full type name: short names are not unique across integration packages. +const classBaseByFullName = new Map(); +if (existsSync(PKGS_DIR)) { const pkgFiles = readdirSync(PKGS_DIR).filter((f) => f.endsWith('.json')); for (const f of pkgFiles) { const pkg = JSON.parse(readFileSync(resolve(PKGS_DIR, f), 'utf8')) as PkgJson; for (const t of pkg.types ?? []) { if (t.kind !== 'class' || !t.baseType) continue; - const base = lastDotted(t.baseType).split('<')[0]; - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(base)) continue; - if (!classBaseByName.has(t.name)) classBaseByName.set(t.name, base); + const existing = classBaseByFullName.get(t.fullName); + if (existing && existing !== t.baseType) { + throw new Error( + `Conflicting base types for ${t.fullName}: ${existing} and ${t.baseType}` + ); + } + classBaseByFullName.set(t.fullName, t.baseType); } } - console.log(` + ${classBaseByName.size} class-inheritance links from pkgs/`); -} catch { - // pkgs directory is optional — older snapshots may not include it. + console.log(` + ${classBaseByFullName.size} class-inheritance links from pkgs/`); } // ---------- collect ---------- @@ -345,11 +369,6 @@ const handleTypes: HandleType[] = []; const handleByName = new Map(); const dtoByName = new Map(); const enumByName = new Map(); -// Track which package each handle originates from, so we can decide whether -// to inject a ContainerResource base (integration packages ship container-backed -// resources whose .NET class extends ContainerResource, but the JSON dump only -// lists implemented interfaces — no class inheritance). -const handlePackage = new Map(); // target short name -> methods const methodsByTarget = new Map(); @@ -374,7 +393,9 @@ export declare function refExpr(strings: TemplateStringsArray, ...values: unknow ]; const POST_SNAPSHOT_DECLARATIONS = [ - `/** + { + name: 'InputType', + declaration: `/** * Enum Aspire.Hosting.ApplicationModel.InputType */ export type InputType = "Text" | "Number" | "Choice" | "SecretText"; @@ -384,28 +405,35 @@ export declare const InputType: { readonly Choice: "Choice"; readonly SecretText: "SecretText"; };`, - `export interface ParameterCustomInputOptions { + }, + { + name: 'ParameterCustomInputOptions', + declaration: `export interface ParameterCustomInputOptions { inputType?: InputType; label?: string; placeholder?: string; options?: Record; }`, - `export interface BeforePublishEvent extends IDistributedApplicationEvent { + }, + { + name: 'BeforePublishEvent', + declaration: `export interface BeforePublishEvent extends IDistributedApplicationEvent { model: PropertyAccessor; services: PropertyAccessor; }`, - `export interface AfterPublishEvent extends IDistributedApplicationEvent { + }, + { + name: 'AfterPublishEvent', + declaration: `export interface AfterPublishEvent extends IDistributedApplicationEvent { model: PropertyAccessor; services: PropertyAccessor; }`, + }, ]; -const POST_SNAPSHOT_DECLARATION_TYPE_NAMES = new Set([ - 'BeforePublishEvent', - 'AfterPublishEvent', - 'InputType', - 'ParameterCustomInputOptions', -]); +const POST_SNAPSHOT_DECLARATION_TYPE_NAMES = new Set( + POST_SNAPSHOT_DECLARATIONS.map(({ name }) => name) +); const POST_SNAPSHOT_AUGMENTATIONS = [ `export interface IDistributedApplicationBuilder { @@ -499,7 +527,6 @@ for (const mod of modules) { if (!handleByName.has(h.name)) { handleByName.set(h.name, h); handleTypes.push(h); - handlePackage.set(h.name, mod.package.name); } } @@ -624,12 +651,8 @@ const BUILT_IN = new Set([ ]); const declaredTypes = new Set([ - ...dtoTypes - .filter((d) => !POST_SNAPSHOT_DECLARATION_TYPE_NAMES.has(d.name)) - .map((d) => d.name), - ...enumTypes - .filter((e) => !POST_SNAPSHOT_DECLARATION_TYPE_NAMES.has(e.name)) - .map((e) => e.name), + ...dtoTypes.map((d) => d.name), + ...enumTypes.map((e) => e.name), ...handleTypes.map((h) => h.name), ...methodsByTarget.keys(), ...POST_SNAPSHOT_DECLARATION_TYPE_NAMES, @@ -663,12 +686,17 @@ parts.push(` set(key: string, value: unknown): Promise;`); parts.push(`};`); parts.push(``); parts.push(`// ---- enums ----`); -for (const declaration of POST_SNAPSHOT_DECLARATIONS) { +const generatedDeclarationTypeNames = new Set([ + ...dtoTypes.map((dto) => dto.name), + ...enumTypes.map((enumType) => enumType.name), + ...handleTypes.map((handle) => handle.name), +]); +for (const { name, declaration } of POST_SNAPSHOT_DECLARATIONS) { + if (generatedDeclarationTypeNames.has(name)) continue; parts.push(declaration); parts.push(''); } for (const en of enumTypes) { - if (POST_SNAPSHOT_DECLARATION_TYPE_NAMES.has(en.name)) continue; parts.push(jsdoc([`Enum ${en.fullName}`])); const members = en.members.map((m) => JSON.stringify(m)).join(' | ') || 'string'; parts.push(`export type ${en.name} = ${members};`); @@ -685,40 +713,20 @@ for (const en of enumTypes) { parts.push(`// ---- DTOs ----`); for (const dto of dtoTypes) { - if (POST_SNAPSHOT_DECLARATION_TYPE_NAMES.has(dto.name)) continue; parts.push(jsdoc([`DTO ${dto.fullName}`])); parts.push(`export interface ${dto.name} {`); for (const f of dto.fields) { const t = cleanType(f.type); extractTypeIdentifiers(t, referencedTypes); scanExprForGenerics(t); - parts.push(` ${camelCase(f.name)}: ${t};`); + const optional = f.isOptional ? '?' : ''; + parts.push(` ${camelCase(f.name)}${optional}: ${t};`); } parts.push(`}`); parts.push(''); } parts.push(`// ---- handle types ----`); -// Integration packages whose resources are NOT container-backed. Resources -// from these packages implement the same IComputeResource/IResourceWithArgs/ -// IResourceWithEndpoints trio that container-backed resources do, but their -// .NET class does not derive from ContainerResource — so we must not inject -// it into the TS extends clause. -const NON_CONTAINER_PACKAGES = new Set([ - 'Aspire.Hosting', // core primitives (ProjectResource, ExecutableResource, ...) - 'Aspire.Hosting.JavaScript', - 'Aspire.Hosting.Python', - 'Aspire.Hosting.DevTunnels', - 'Aspire.Hosting.Maui', -]); -// Specific handles to exclude even if their package is container-friendly. -// AzureFunctionsProjectResource is a project handle that lives in the Functions -// package alongside the Storage emulator (which IS container-backed). -const NON_CONTAINER_HANDLES = new Set([ - 'AzureFunctionsProjectResource', - 'AzurePromptAgentResource', -]); - const EXTRA_HANDLE_MEMBERS: Record = { AzureResourceInfrastructure: [ ' /** Gets the provisionable Azure resources produced by the infrastructure callback. */', @@ -730,17 +738,22 @@ const EXTRA_HANDLE_MEMBERS: Record = { ], }; -function isContainerBacked(h: HandleType): boolean { - if (h.name === 'ContainerResource') return false; - if (NON_CONTAINER_HANDLES.has(h.name)) return false; - const pkg = handlePackage.get(h.name); - if (pkg && NON_CONTAINER_PACKAGES.has(pkg)) return false; - const ifaces = new Set((h.implementedInterfaces ?? []).map((i) => lastDotted(i).split('<')[0])); - return ( - ifaces.has('IComputeResource') && - ifaces.has('IResourceWithArgs') && - ifaces.has('IResourceWithEndpoints') - ); +function getBaseTypeHierarchy(h: HandleType): string[] { + if ((h.baseTypeHierarchy?.length ?? 0) > 0) { + return h.baseTypeHierarchy!; + } + + const hierarchy: string[] = []; + const seen = new Set([h.fullName]); + let ancestor = classBaseByFullName.get(h.fullName); + while (ancestor) { + const identity = withoutAssemblyPrefix(ancestor); + if (seen.has(identity)) break; + seen.add(identity); + hierarchy.push(identity); + ancestor = classBaseByFullName.get(identity); + } + return hierarchy; } for (const h of handleTypes) { @@ -749,23 +762,16 @@ for (const h of handleTypes) { .map((i) => i.split('<')[0]) .filter((i) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(i) && i !== h.name); const uniqueParents = [...new Set(parents)]; - if (isContainerBacked(h) && !uniqueParents.includes('ContainerResource')) { - // Put ContainerResource first so chains like `.withImageTag(...).withLifetime(...)` - // resolve to inherited members before the marker interfaces contribute noise. - uniqueParents.unshift('ContainerResource'); - } - // Chase the class-inheritance chain so methods defined on a parent class - // (e.g. publishAsDockerFile on ExecutableResource) are visible on subclasses - // (e.g. ViteAppResource). The ts-modules dump only lists implemented - // interfaces, so without this step subclasses lose inherited members. - let ancestor = classBaseByName.get(h.name); - const seen = new Set([h.name]); - while (ancestor && !seen.has(ancestor)) { - seen.add(ancestor); - if (handleByName.has(ancestor) && !uniqueParents.includes(ancestor)) { - uniqueParents.unshift(ancestor); + + for (const ancestor of getBaseTypeHierarchy(h)) { + const ancestorName = cleanType(ancestor).split('<')[0]; + if ( + ancestorName !== h.name && + handleByName.has(ancestorName) && + !uniqueParents.includes(ancestorName) + ) { + uniqueParents.unshift(ancestorName); } - ancestor = classBaseByName.get(ancestor); } const implementsClause = uniqueParents.length > 0 ? ` extends ${uniqueParents.join(', ')}` : ''; for (const i of uniqueParents) referencedTypes.add(i); diff --git a/src/frontend/scripts/normalize-generated-api-data.ts b/src/frontend/scripts/normalize-generated-api-data.ts index b7afcc8ed..10332bdbf 100644 --- a/src/frontend/scripts/normalize-generated-api-data.ts +++ b/src/frontend/scripts/normalize-generated-api-data.ts @@ -48,9 +48,10 @@ export const TS_MODULES_DIR = path.join(DATA_DIR, 'ts-modules'); // left intact, hence the kind gating below. `description`/`returns`/`remarks` // are always prose (and appear as string values only in the TS API + member // summaries; the C# doc arrays open with `[` and are skipped, their inner text -// nodes handled by the `text` rule). +// nodes handled by the `text` rule). `Reason` is the prose payload of +// `AspireExportIgnoreAttribute`. const nodeLine = - /^[ \t]*"kind"[ \t]*:[ \t]*"([^"]*)"|^([ \t]*")(text|description|returns|remarks)("[ \t]*:[ \t]*")((?:[^"\\]|\\.)*)(")/gm; + /^[ \t]*"kind"[ \t]*:[ \t]*"([^"]*)"|^([ \t]*")(text|description|returns|remarks|Reason)("[ \t]*:[ \t]*")((?:[^"\\]|\\.)*)(")/gm; /** * Rewrite deprecated Aspire terminology in the prose fields of a generated API @@ -135,10 +136,20 @@ function main(): void { const explicit = args.includes('--pkgs') || args.includes('--ts-modules'); const targets: Array<{ label: string; dir: string }> = []; if (!explicit || args.includes('--pkgs')) { - targets.push({ label: 'pkgs', dir: PKGS_DIR }); + targets.push({ + label: 'pkgs', + dir: process.env.ASPIRE_API_PKGS_DIR + ? path.resolve(process.env.ASPIRE_API_PKGS_DIR) + : PKGS_DIR, + }); } if (!explicit || args.includes('--ts-modules')) { - targets.push({ label: 'ts-modules', dir: TS_MODULES_DIR }); + targets.push({ + label: 'ts-modules', + dir: process.env.ASPIRE_API_TS_MODULES_DIR + ? path.resolve(process.env.ASPIRE_API_TS_MODULES_DIR) + : TS_MODULES_DIR, + }); } let total = 0; diff --git a/src/frontend/scripts/update-integration-data.ps1 b/src/frontend/scripts/update-integration-data.ps1 index 86b40042e..fa196d361 100644 --- a/src/frontend/scripts/update-integration-data.ps1 +++ b/src/frontend/scripts/update-integration-data.ps1 @@ -19,8 +19,10 @@ a. generate-package-json.ps1 -> src/data/pkgs/*.json (C# API) b. pnpm update:ts-api -> src/data/ts-modules/*.json (TS API) + chains twoslash aspire.d.ts bundle - 4. Scope check — the working tree must only contain allowed data files. - 5. Emits a summary + PR title/body. In CI, writes step outputs to + 4. Semantic validation — cross-checks generated identities, provenance, + DTO optionality, inheritance, options shapes, and attribute payloads. + 5. Scope check — the working tree must only contain allowed data files. + 6. Emits a summary + PR title/body. In CI, writes step outputs to $GITHUB_OUTPUT and the PR body to a file; locally prints a summary. Exit codes: @@ -28,9 +30,8 @@ 1 a required phase failed (update:all, TS API regen, out-of-scope diff). The caller must NOT open a PR on a non-zero exit. - Per-package failures inside generate-package-json.ps1 are tolerated (they - are common for meta-packages without a public API surface); their counts are - reported in the PR body but do not fail the run. + Packages without a public API surface are reported as explicit skips. Any + generation failure or semantic validation error fails the run. .PARAMETER SkipRegen Skip the API-reference regeneration phase even when versions changed. Useful @@ -79,6 +80,9 @@ $AllowedPaths = @( ) $IsCI = [bool]$env:GITHUB_ACTIONS +$script:ApiStageRoot = $null +$script:PreserveApiStage = $false +$script:PreviousApiEnvironment = @{} function Write-Section([string]$Text) { Write-Host "" @@ -124,6 +128,128 @@ function Test-PathAllowed { return $false } +function Restore-ApiEnvironment { + foreach ($name in @( + 'ASPIRE_API_PKGS_DIR', + 'ASPIRE_API_TS_MODULES_DIR', + 'ASPIRE_API_TWOSLASH_FILE' + )) { + $previousValue = $script:PreviousApiEnvironment[$name] + if ($null -eq $previousValue) { + Remove-Item "Env:$name" -ErrorAction SilentlyContinue + } + else { + Set-Item "Env:$name" $previousValue + } + } +} + +function Remove-ApiStage { + Restore-ApiEnvironment + if ($script:ApiStageRoot -and (Test-Path $script:ApiStageRoot)) { + if ($script:PreserveApiStage) { + Write-Warning "Preserving API generation recovery data at $($script:ApiStageRoot)." + } + else { + Remove-Item -Path $script:ApiStageRoot -Recurse -Force -ErrorAction SilentlyContinue + } + } + $script:ApiStageRoot = $null + $script:PreserveApiStage = $false +} + +function Stop-ApiRegeneration { + param([Parameter(Mandatory)][string]$Message) + + Remove-ApiStage + Write-Host $Message -ForegroundColor Red + exit 1 +} + +function Publish-GeneratedApiData { + param( + [Parameter(Mandatory)][string]$PackageSource, + [Parameter(Mandatory)][string]$ModuleSource, + [Parameter(Mandatory)][string]$TwoslashSource + ) + + $moves = @( + [PSCustomObject]@{ + Source = $PackageSource + Destination = Join-Path $DataDir 'pkgs' + Backup = Join-Path $script:ApiStageRoot 'backup-pkgs' + }, + [PSCustomObject]@{ + Source = $ModuleSource + Destination = Join-Path $DataDir 'ts-modules' + Backup = Join-Path $script:ApiStageRoot 'backup-ts-modules' + }, + [PSCustomObject]@{ + Source = $TwoslashSource + Destination = Join-Path $DataDir 'twoslash' 'aspire.d.ts' + Backup = Join-Path $script:ApiStageRoot 'backup-aspire.d.ts' + } + ) + $completed = [System.Collections.Generic.List[object]]::new() + + try { + foreach ($move in $moves) { + if (-not (Test-Path $move.Source)) { + throw "Staged API artifact is missing: $($move.Source)" + } + if (-not (Test-Path $move.Destination)) { + throw "Published API artifact is missing: $($move.Destination)" + } + if (Test-Path $move.Backup) { + throw "API recovery path already exists: $($move.Backup)" + } + } + + foreach ($move in $moves) { + Move-Item -LiteralPath $move.Destination -Destination $move.Backup + $completed.Add($move) + Move-Item -LiteralPath $move.Source -Destination $move.Destination + } + } + catch { + $publishError = $_ + $rollbackErrors = [System.Collections.Generic.List[string]]::new() + for ($index = $completed.Count - 1; $index -ge 0; $index--) { + $move = $completed[$index] + try { + if (Test-Path $move.Destination) { + if (Test-Path $move.Source) { + throw "Staged path already exists: $($move.Source)" + } + Move-Item -LiteralPath $move.Destination -Destination $move.Source + } + } + catch { + $rollbackErrors.Add("$($move.Destination) -> $($move.Source): $_") + } + + try { + if (Test-Path $move.Backup) { + if (Test-Path $move.Destination) { + throw "Destination is still occupied: $($move.Destination)" + } + Move-Item -LiteralPath $move.Backup -Destination $move.Destination + } + } + catch { + $rollbackErrors.Add("$($move.Backup) -> $($move.Destination): $_") + } + } + + if ($rollbackErrors.Count -gt 0) { + $script:PreserveApiStage = $true + throw "Publishing failed: $publishError Rollback was incomplete: $($rollbackErrors -join '; '). Recovery data remains under $($script:ApiStageRoot)." + } + + throw "Publishing failed: $publishError The original generated data was restored." + } +} + # ── Phase 1: data update ──────────────────────────────────────────────────── Write-Section 'Phase 1 — pnpm update:all' Push-Location $FrontendDir @@ -166,6 +292,7 @@ if (-not $anyChanges) { Set-Output 'changed' 'false' Set-Output 'versions_changed' 'false' Set-Output 'regen_ran' 'false' + Set-Output 'semantic_validation' 'not run' exit 0 } @@ -229,23 +356,59 @@ else { # ── Phase 3: conditional API-reference regeneration ───────────────────────── $regenRan = $false $pkgSummary = '' +$pkgSkippedPackages = '' $tsApiSummary = '' +$tsSkippedPackages = '' $twoslashSummary = '' +$semanticSummary = '' if ($versionsChanged -and -not $SkipRegen) { Write-Section 'Phase 3 — API reference regeneration' - # 3a. C# API JSON. Per-package failures are tolerated (meta-packages). + $script:ApiStageRoot = Join-Path $DataDir ".api-generation-$([Guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $script:ApiStageRoot -Force | Out-Null + $pkgStageDir = Join-Path $script:ApiStageRoot 'pkgs' + $tsStageDir = Join-Path $script:ApiStageRoot 'ts-modules' + $twoslashStageFile = Join-Path $script:ApiStageRoot 'twoslash' 'aspire.d.ts' + foreach ($name in @( + 'ASPIRE_API_PKGS_DIR', + 'ASPIRE_API_TS_MODULES_DIR', + 'ASPIRE_API_TWOSLASH_FILE' + )) { + $script:PreviousApiEnvironment[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') + } + $env:ASPIRE_API_PKGS_DIR = $pkgStageDir + $env:ASPIRE_API_TS_MODULES_DIR = $tsStageDir + $env:ASPIRE_API_TWOSLASH_FILE = $twoslashStageFile + + # 3a. C# API JSON. Write-Host "→ generate-package-json.ps1 (C# API → pkgs/)" -ForegroundColor Cyan - $pkgArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $PkgGenScript) + $pkgArgs = @( + '-NoProfile', '-ExecutionPolicy', 'Bypass', + '-File', $PkgGenScript, + '-OutputDir', $pkgStageDir + ) if ($Framework) { $pkgArgs += @('-Framework', $Framework) } $pkgLog = & pwsh @pkgArgs 2>&1 | Tee-Object -Variable pkgTeed | Out-String if ($LASTEXITCODE -ne 0) { - Write-Error "generate-package-json.ps1 failed hard (exit $LASTEXITCODE). Aborting; no PR will be opened." - exit 1 + Stop-ApiRegeneration "generate-package-json.ps1 failed (exit $LASTEXITCODE).`n$pkgLog`nAborting; no PR will be opened." } $pkgDone = ($pkgLog -split "`n" | Where-Object { $_ -match 'Done!\s+Success:' } | Select-Object -First 1) - $pkgSummary = if ($pkgDone) { ($pkgDone -replace '.*Done!\s+', '').Trim() } else { 'summary unavailable' } + if (-not $pkgDone -or + $pkgDone -notmatch 'Done!\s+Success:\s+(?\d+)\s+\|\s+Failed:\s+(?\d+)\s+\|\s+Skipped:\s+(?\d+)') { + Stop-ApiRegeneration "C# API generation did not emit a valid completion summary. Aborting; no PR will be opened." + } + $pkgSummary = "Success: $($Matches.Succeeded) | Failed: $($Matches.Failed) | Skipped: $($Matches.Skipped)" + if ([int]$Matches.Failed -gt 0) { + Stop-ApiRegeneration "C# API generation reported $($Matches.Failed) package failure(s). Aborting; no PR will be opened." + } + $pkgSkippedLine = ($pkgLog -split "`n" | + Where-Object { $_ -match '^\s*Skipped packages:' } | + Select-Object -First 1) + $pkgSkippedPackages = if ($pkgSkippedLine) { + ($pkgSkippedLine -replace '^\s*Skipped packages:\s*', '').Trim() + } + else { '' } Write-Host " C# API: $pkgSummary" # 3a-normalize. Enforce Aspire terminology in the freshly generated C# API @@ -262,8 +425,7 @@ if ($versionsChanged -and -not $SkipRegen) { Pop-Location } if ($pkgNormExit -ne 0) { - Write-Error "normalize:api-data (pkgs) failed (exit $pkgNormExit).`n$pkgNormLog`nAborting; no PR will be opened." - exit 1 + Stop-ApiRegeneration "normalize:api-data (pkgs) failed (exit $pkgNormExit).`n$pkgNormLog`nAborting; no PR will be opened." } # 3b. TS API JSON (+ chained twoslash bundle). Requires the Aspire CLI; the @@ -281,12 +443,11 @@ if ($versionsChanged -and -not $SkipRegen) { if ($tsExit -ne 0) { # Distinguish phase-2 vs phase-3 failure using the script's log markers. if ($tsLog -match 'Twoslash type generation failed') { - Write-Error "Twoslash bundle generation failed. Aborting; no PR will be opened." + Stop-ApiRegeneration "Twoslash bundle generation failed.`n$tsLog`nAborting; no PR will be opened." } else { - Write-Error "TypeScript API generation failed. Aborting; no PR will be opened." + Stop-ApiRegeneration "TypeScript API generation failed.`n$tsLog`nAborting; no PR will be opened." } - exit 1 } $tsApiDone = ($tsLog -split "`n" | @@ -294,24 +455,56 @@ if ($versionsChanged -and -not $SkipRegen) { Select-Object -First 1) if (-not $tsApiDone -or $tsApiDone -notmatch 'Complete:\s+(?\d+)\s+succeeded,\s+(?\d+)\s+failed,\s+(?\d+)\s+skipped') { - Write-Error "TypeScript API generation did not emit a valid completion summary. Aborting; no PR will be opened." - exit 1 + Stop-ApiRegeneration "TypeScript API generation did not emit a valid completion summary. Aborting; no PR will be opened." } $tsApiSummary = "$($Matches.Succeeded) succeeded, $($Matches.Failed) failed, $($Matches.Skipped) skipped" if ([int]$Matches.Failed -gt 0) { - Write-Error "TypeScript API generation reported $($Matches.Failed) package failure(s). Aborting; no PR will be opened." - exit 1 + Stop-ApiRegeneration "TypeScript API generation reported $($Matches.Failed) package failure(s). Aborting; no PR will be opened." } + $tsSkippedLine = ($tsLog -split "`n" | + Where-Object { $_ -match '^\s*Skipped packages:' } | + Select-Object -First 1) + $tsSkippedPackages = if ($tsSkippedLine) { + ($tsSkippedLine -replace '^\s*Skipped packages:\s*', '').Trim() + } + else { '' } $twoslashSummary = 'succeeded' + + # 3c. Cross-artifact semantic validation. This runs only after every + # generator has completed and must pass before the workflow can publish. + Write-Host "→ pnpm validate:api-data (semantic regression gate)" -ForegroundColor Cyan + Push-Location $FrontendDir + try { + $validationLog = & pnpm run validate:api-data 2>&1 | Tee-Object -Variable validationTeed | Out-String + $validationExit = $LASTEXITCODE + } + finally { + Pop-Location + } + if ($validationExit -ne 0) { + Stop-ApiRegeneration "Generated API semantic validation failed.`n$validationLog`nAborting; no PR will be opened." + } + $semanticSummary = 'passed' + + try { + Publish-GeneratedApiData ` + -PackageSource $pkgStageDir ` + -ModuleSource $tsStageDir ` + -TwoslashSource $twoslashStageFile + } + catch { + Stop-ApiRegeneration "Publishing validated API data failed: $_" + } + Remove-ApiStage $regenRan = $true } elseif ($versionsChanged -and $SkipRegen) { Write-Warning "Versions changed but -SkipRegen was set; regeneration skipped (local/dev use only)." } -# ── Phase 4: scope check ──────────────────────────────────────────────────── -Write-Section 'Phase 4 — scope check' +# ── Phase 5: scope check ──────────────────────────────────────────────────── +Write-Section 'Phase 5 — scope check' $allStatus = @(Invoke-Git @('status', '--porcelain') | Where-Object { $_ -and $_.Trim().Length -gt 0 }) $outOfScope = [System.Collections.Generic.List[string]]::new() foreach ($line in $allStatus) { @@ -396,6 +589,13 @@ if ($regenRan) { [void]$sb.AppendLine("- C# API JSON (``generate-package-json.ps1`` → ``pkgs/``): $pkgSummary") [void]$sb.AppendLine("- TS API JSON (``update:ts-api`` → ``ts-modules/``): $tsApiSummary") [void]$sb.AppendLine("- Twoslash bundle (``twoslash/aspire.d.ts``): $twoslashSummary") + [void]$sb.AppendLine("- Semantic generated-data validation: $semanticSummary") + if ($pkgSkippedPackages) { + [void]$sb.AppendLine("- C# packages skipped because they have no public API: ``$pkgSkippedPackages``") + } + if ($tsSkippedPackages) { + [void]$sb.AppendLine("- TypeScript packages skipped because they export no ATS functions: ``$tsSkippedPackages``") + } } else { [void]$sb.AppendLine("_No integration package versions changed in this run — API reference regeneration was skipped._") @@ -429,6 +629,7 @@ Set-Content -Path $prBodyFile -Value $prBody -Encoding utf8 Set-Output 'changed' 'true' Set-Output 'versions_changed' ($versionsChanged.ToString().ToLowerInvariant()) Set-Output 'regen_ran' ($regenRan.ToString().ToLowerInvariant()) +Set-Output 'semantic_validation' $(if ($regenRan) { $semanticSummary } else { 'not run' }) Set-Output 'pr_title' $prTitle Set-Output 'pr_body_file' $prBodyFile Set-Output 'icon_warnings' $iconWarnings diff --git a/src/frontend/scripts/update-ts-api.ts b/src/frontend/scripts/update-ts-api.ts index 4b539b183..0bbb9917f 100644 --- a/src/frontend/scripts/update-ts-api.ts +++ b/src/frontend/scripts/update-ts-api.ts @@ -18,7 +18,7 @@ * tsx ./scripts/update-ts-api.ts /path/aspire # from repo clone */ -import { execSync, execFileSync } from 'child_process'; +import { execFileSync } from 'child_process'; import { existsSync } from 'fs'; import { dirname, resolve } from 'path'; import { fileURLToPath } from 'url'; @@ -60,7 +60,7 @@ function main(): void { process.exit(1); } - let psArgs: string; + const psArgs = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', SCRIPT_PATH]; if (aspireRepoPath) { const resolvedPath = resolve(aspireRepoPath); if (!existsSync(resolvedPath)) { @@ -68,17 +68,20 @@ function main(): void { process.exit(1); } console.log(`🔄 Generating TypeScript API reference data from ${resolvedPath}...`); - psArgs = `-AspireRepoPath "${resolvedPath}"`; + psArgs.push('-AspireRepoPath', resolvedPath); } else { console.log('🔄 Generating TypeScript API reference data from installed Aspire CLI...'); - psArgs = ''; + } + + const outputDir = process.env.ASPIRE_API_TS_MODULES_DIR + ? resolve(process.env.ASPIRE_API_TS_MODULES_DIR) + : TS_MODULES_DIR; + if (process.env.ASPIRE_API_TS_MODULES_DIR) { + psArgs.push('-OutputDir', outputDir); } try { - execSync( - `pwsh -NoProfile -ExecutionPolicy Bypass -File "${SCRIPT_PATH}" ${psArgs}`.trim(), - { stdio: 'inherit', cwd: resolve(__dirname, '..') } - ); + execFileSync('pwsh', psArgs, { stdio: 'inherit', cwd: resolve(__dirname, '..') }); console.log('✅ TypeScript API reference data updated.'); } catch (error: unknown) { console.error('❌ Generation failed:', getErrorMessage(error)); @@ -91,7 +94,7 @@ function main(): void { // JSDoc/XML docs may carry. Reuses the single source of truth in // aspire-terminology.ts. console.log('🔄 Normalizing Aspire terminology in ts-modules JSON...'); - const { changes: tsModuleChanges } = normalizeApiDir(TS_MODULES_DIR); + const { changes: tsModuleChanges } = normalizeApiDir(outputDir); console.log(`✅ Normalized ${tsModuleChanges} occurrence(s) in ts-modules JSON.`); // Refresh the twoslash .d.ts bundle so docs hover tooltips stay in sync diff --git a/src/frontend/scripts/validate-generated-api-data.ts b/src/frontend/scripts/validate-generated-api-data.ts new file mode 100644 index 000000000..5ed77b7b6 --- /dev/null +++ b/src/frontend/scripts/validate-generated-api-data.ts @@ -0,0 +1,677 @@ +import { spawnSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +interface CatalogEntry { + title: string; + version: string; +} + +interface PackageMetadata { + name: string; + version: string; + sourceRepository?: string; + sourceCommit?: string; +} + +interface ApiAttribute { + name: string; + constructorArguments?: unknown[]; + arguments?: Record; + namedArguments?: Record; +} + +interface ApiParameter { + name: string; + type?: string; + modifier?: string; + attributes?: ApiAttribute[]; +} + +interface ApiMember { + name: string; + kind?: string; + signature?: string; + genericParameters?: unknown[]; + attributes?: ApiAttribute[]; + parameters?: ApiParameter[]; +} + +interface ApiType { + name: string; + fullName: string; + kind: string; + baseType?: string; + attributes?: ApiAttribute[]; + members?: ApiMember[]; +} + +interface PackageJson { + package: PackageMetadata; + types?: ApiType[]; +} + +interface DtoField { + name: string; + type: string; + isOptional?: boolean; +} + +interface DtoType { + name: string; + fields?: DtoField[]; +} + +interface HandleType { + name: string; + fullName: string; + implementedInterfaces?: string[]; + baseTypeHierarchy?: string[]; +} + +interface TsModuleJson { + package: PackageMetadata; + functions?: unknown[]; + dtoTypes?: DtoType[]; + handleTypes?: HandleType[]; +} + +export interface GeneratedFile { + fileName: string; + data: T; + baseline?: T; +} + +export interface ValidationInput { + catalog: CatalogEntry[]; + packages: GeneratedFile[]; + modules: GeneratedFile[]; + declarations: string; +} + +export interface ValidationResult { + errors: string[]; + checks: string[]; +} + +interface ParsedInterface { + parents: string[]; + properties: Map; +} + +interface AttributePayloadShape { + constructorArgumentCount: number; + namedArgumentNames: string[]; +} + +const relevantAttribute = /(?:^|\.)Aspire(?:Export(?:Ignore)?|Dto|Union)Attribute$/; + +function identity(metadata: PackageMetadata): string { + return `${metadata.name}@${metadata.version}`; +} + +function expectedFileName(metadata: PackageMetadata): string { + return `${metadata.name}.${metadata.version}.json`; +} + +function shortTypeName(typeId: string): string { + let normalized = typeId.trim(); + normalized = normalized.replace( + /[A-Za-z_][A-Za-z0-9_]*(?:[./][A-Za-z_][A-Za-z0-9_]*)+/g, + (value) => { + const withoutAssembly = value.includes('/') + ? value.slice(value.lastIndexOf('/') + 1) + : value; + const parts = withoutAssembly.split('.'); + return parts[parts.length - 1]; + } + ); + normalized = normalized.replace(/\]\]+/g, ''); + const withoutAssembly = normalized.includes('/') + ? normalized.slice(normalized.lastIndexOf('/') + 1) + : normalized; + const withoutGeneric = withoutAssembly.split('<')[0]; + const parts = withoutGeneric.split('.'); + return parts[parts.length - 1]; +} + +function camelCase(name: string): string { + return name.length === 0 ? name : name[0].toLowerCase() + name.slice(1); +} + +function normalizeTypeScriptType(typeName: string): string { + return typeName + .trim() + .replace(/[A-Za-z_][A-Za-z0-9_]*(?:[./][A-Za-z_][A-Za-z0-9_]*)+/g, (value) => { + const withoutAssembly = value.includes('/') + ? value.slice(value.lastIndexOf('/') + 1) + : value; + const parts = withoutAssembly.split('.'); + return parts[parts.length - 1]; + }) + .replace(/\s+/g, ''); +} + +function normalizeClrType(typeName: string): string { + const slashIndex = typeName.indexOf('/'); + return (slashIndex >= 0 ? typeName.slice(slashIndex + 1) : typeName) + .replace(/\s+/g, '') + .replace(/`\d+\[\[/g, '<') + .replace(/\],\[/g, ',') + .replace(/\]\]/g, '>'); +} + +function addUnique( + files: GeneratedFile[], + metadata: (data: T) => PackageMetadata, + label: string, + errors: string[] +): Map> { + const byIdentity = new Map>(); + for (const file of files) { + const packageMetadata = metadata(file.data); + const key = identity(packageMetadata); + if (byIdentity.has(key)) { + errors.push(`${label} contains duplicate package identity ${key}.`); + continue; + } + byIdentity.set(key, file); + if (file.fileName !== expectedFileName(packageMetadata)) { + errors.push( + `${label} file ${file.fileName} does not match its package identity; expected ${expectedFileName(packageMetadata)}.` + ); + } + } + return byIdentity; +} + +function isPackageOutputExpected(name: string): boolean { + return ( + !name.startsWith('Aspire.Hosting.CodeGeneration.') && + name !== 'Aspire.Hosting.Integration.Analyzers' + ); +} + +function attributesForOwner( + result: Map, + owner: string, + attributes: ApiAttribute[] | undefined +): void { + const indexes = new Map(); + for (const attribute of attributes ?? []) { + if (!relevantAttribute.test(attribute.name)) continue; + const index = indexes.get(attribute.name) ?? 0; + indexes.set(attribute.name, index + 1); + result.set(`${owner}|${attribute.name}|${index}`, { + constructorArgumentCount: attribute.constructorArguments?.length ?? 0, + namedArgumentNames: [ + ...new Set([ + ...Object.keys(attribute.arguments ?? {}), + ...Object.keys(attribute.namedArguments ?? {}), + ]), + ].sort(), + }); + } +} + +function memberOwnerKey(typeOwner: string, member: ApiMember): string { + const genericArity = member.genericParameters?.length ?? 0; + const parameterTypes = (member.parameters ?? []) + .map((parameter) => + `${parameter.modifier ? `${parameter.modifier} ` : ''}${parameter.type ?? '?'}` + ) + .join(','); + return `${typeOwner}/member:${member.kind ?? 'member'}:${member.name}${genericArity > 0 ? `\`${genericArity}` : ''}(${parameterTypes})`; +} + +function collectAttributePayloads(pkg: PackageJson): Map { + const result = new Map(); + for (const type of pkg.types ?? []) { + const typeOwner = `type:${type.fullName}`; + attributesForOwner(result, typeOwner, type.attributes); + for (const member of type.members ?? []) { + const memberOwner = memberOwnerKey(typeOwner, member); + attributesForOwner(result, memberOwner, member.attributes); + for (const [index, parameter] of (member.parameters ?? []).entries()) { + attributesForOwner( + result, + `${memberOwner}/parameter:${index}:${parameter.name}`, + parameter.attributes + ); + } + } + } + return result; +} + +function hasExportedApi(pkg: PackageJson): boolean { + const visit = (attributes: ApiAttribute[] | undefined): boolean => + (attributes ?? []).some((attribute) => /(?:^|\.)AspireExportAttribute$/.test(attribute.name)); + + return (pkg.types ?? []).some( + (type) => + visit(type.attributes) || + (type.members ?? []).some( + (member) => + visit(member.attributes) || + (member.parameters ?? []).some((parameter) => visit(parameter.attributes)) + ) + ); +} + +function splitTopLevel(value: string): string[] { + const result: string[] = []; + let depth = 0; + let start = 0; + for (let index = 0; index < value.length; index++) { + if (value[index] === '<') depth++; + if (value[index] === '>') depth--; + if (value[index] === ',' && depth === 0) { + result.push(value.slice(start, index).trim()); + start = index + 1; + } + } + const tail = value.slice(start).trim(); + if (tail) result.push(tail); + return result; +} + +function parseInterfaces(declarations: string): Map { + const header = /^export interface ([A-Za-z_][A-Za-z0-9_]*)/gm; + const matches = [...declarations.matchAll(header)]; + const result = new Map(); + + for (const [index, match] of matches.entries()) { + const lineStart = match.index ?? 0; + const lineEnd = declarations.indexOf('\n', lineStart); + const headerLine = declarations.slice( + lineStart, + lineEnd === -1 ? declarations.length : lineEnd + ); + let cursor = match[0].length; + if (headerLine[cursor] === '<') { + let depth = 0; + do { + if (headerLine[cursor] === '<') depth++; + if (headerLine[cursor] === '>') depth--; + cursor++; + } while (cursor < headerLine.length && depth > 0); + } + const remainder = headerLine.slice(cursor).trim(); + const extendsText = remainder.startsWith('extends ') + ? remainder.slice('extends '.length, remainder.lastIndexOf('{')).trim() + : ''; + const bodyStart = lineEnd === -1 ? declarations.length : lineEnd + 1; + const bodyEnd = matches[index + 1]?.index ?? declarations.length; + const previous = result.get(match[1]); + const parents = new Set(previous?.parents ?? []); + for (const parent of splitTopLevel(extendsText).map(shortTypeName)) { + parents.add(parent); + } + const properties = new Map(previous?.properties ?? []); + const body = declarations.slice(bodyStart, bodyEnd); + for (const property of body.matchAll( + /^\s{2}([A-Za-z_][A-Za-z0-9_]*)(\?)?:\s*(.+);$/gm + )) { + properties.set(property[1], { + optional: property[2] === '?', + type: property[3].trim(), + }); + } + + result.set(match[1], { + parents: [...parents], + properties, + }); + } + return result; +} + +function setDifference(left: Set, right: Set): string[] { + return [...left].filter((value) => !right.has(value)).sort(); +} + +function resolveBaseHierarchy( + module: TsModuleJson, + handle: HandleType, + packageByIdentity: Map> +): string[] { + if ((handle.baseTypeHierarchy?.length ?? 0) > 0) { + return handle.baseTypeHierarchy!; + } + + const pkg = packageByIdentity.get(identity(module.package))?.data; + const baseByFullName = new Map( + (pkg?.types ?? []) + .filter((type) => type.kind === 'class' && type.baseType) + .map((type) => [type.fullName, type.baseType!] as const) + ); + const hierarchy: string[] = []; + const seen = new Set([handle.fullName]); + let ancestor = baseByFullName.get(handle.fullName); + while (ancestor && !seen.has(ancestor)) { + hierarchy.push(ancestor); + seen.add(ancestor); + ancestor = baseByFullName.get(ancestor); + } + return hierarchy; +} + +export function validateGeneratedApiData(input: ValidationInput): ValidationResult { + const errors: string[] = []; + const catalogByName = new Map(); + for (const entry of input.catalog) { + if (catalogByName.has(entry.title)) { + errors.push(`Integration catalog contains duplicate package ${entry.title}.`); + } else { + catalogByName.set(entry.title, entry); + } + } + + const packageByIdentity = addUnique(input.packages, (pkg) => pkg.package, 'pkgs', errors); + const moduleByIdentity = addUnique(input.modules, (module) => module.package, 'ts-modules', errors); + + for (const entry of input.catalog) { + if (!isPackageOutputExpected(entry.title)) continue; + const key = `${entry.title}@${entry.version}`; + if (!packageByIdentity.has(key)) { + errors.push(`Missing C# API output for catalog package ${key}.`); + } + } + + for (const file of input.packages) { + const metadata = file.data.package; + const catalogEntry = catalogByName.get(metadata.name); + if (!catalogEntry) { + errors.push(`C# API output ${identity(metadata)} is not present in the integration catalog.`); + } else if (catalogEntry.version !== metadata.version) { + errors.push( + `Stale C# API output ${identity(metadata)}; catalog version is ${catalogEntry.version}.` + ); + } + if (!metadata.sourceRepository) { + errors.push(`C# API output ${identity(metadata)} is missing its source repository.`); + } + + if (file.baseline && identity(file.baseline.package) === identity(metadata)) { + const before = collectAttributePayloads(file.baseline); + const after = collectAttributePayloads(file.data); + for (const [attributeKey, payload] of before) { + const generatedPayload = after.get(attributeKey); + const lostConstructorArguments = + !generatedPayload || + generatedPayload.constructorArgumentCount < payload.constructorArgumentCount; + const lostNamedArguments = + !generatedPayload || + payload.namedArgumentNames.some( + (name) => !generatedPayload.namedArgumentNames.includes(name) + ); + if (lostConstructorArguments || lostNamedArguments) { + errors.push( + `Attribute payload changed or disappeared for ${identity(metadata)} at ${attributeKey}.` + ); + } + } + } + } + + for (const file of input.modules) { + const metadata = file.data.package; + const catalogEntry = catalogByName.get(metadata.name); + if (!catalogEntry) { + errors.push(`TypeScript API output ${identity(metadata)} is not present in the integration catalog.`); + } else if (catalogEntry.version !== metadata.version) { + errors.push( + `Stale TypeScript API output ${identity(metadata)}; catalog version is ${catalogEntry.version}.` + ); + } + if ((file.data.functions?.length ?? 0) === 0) { + errors.push(`TypeScript API output ${identity(metadata)} contains no exported functions.`); + } + + const matchingPackage = packageByIdentity.get(identity(metadata))?.data; + if (!matchingPackage) { + errors.push(`TypeScript API output ${identity(metadata)} has no exact C# API output.`); + continue; + } + if (metadata.sourceRepository !== matchingPackage.package.sourceRepository) { + errors.push( + `Source repository mismatch for ${identity(metadata)}: C# has ${matchingPackage.package.sourceRepository ?? '(missing)'}, TypeScript has ${metadata.sourceRepository ?? '(missing)'}.` + ); + } + if (metadata.sourceCommit !== matchingPackage.package.sourceCommit) { + errors.push( + `Source commit mismatch for ${identity(metadata)}: C# has ${matchingPackage.package.sourceCommit ?? '(missing)'}, TypeScript has ${metadata.sourceCommit ?? '(missing)'}.` + ); + } + } + + for (const file of input.packages) { + if (hasExportedApi(file.data) && !moduleByIdentity.has(identity(file.data.package))) { + errors.push(`Missing TypeScript API output for exported package ${identity(file.data.package)}.`); + } + } + + const parsedInterfaces = parseInterfaces(input.declarations); + const selectedDtos = new Map(); + const selectedHandles = new Map(); + for (const file of [...input.modules].sort((left, right) => + left.fileName.localeCompare(right.fileName) + )) { + for (const dto of file.data.dtoTypes ?? []) { + if (!selectedDtos.has(dto.name)) selectedDtos.set(dto.name, dto); + } + for (const handle of file.data.handleTypes ?? []) { + if (!selectedHandles.has(handle.name)) { + selectedHandles.set(handle.name, { handle, module: file.data }); + } + } + } + + for (const dto of selectedDtos.values()) { + const declaration = parsedInterfaces.get(dto.name); + if (!declaration) { + errors.push(`Twoslash DTO ${dto.name} is missing its declaration.`); + continue; + } + for (const field of dto.fields ?? []) { + const propertyName = camelCase(field.name); + const property = declaration.properties.get(propertyName); + if (!field.isOptional) { + errors.push( + `TypeScript DTO ${dto.name}.${propertyName} is required, but the SDK emits every DTO field as optional.` + ); + } else if (!property) { + errors.push(`Twoslash DTO ${dto.name} is missing property ${propertyName}.`); + } else if (!property.optional) { + errors.push( + `Twoslash DTO ${dto.name}.${propertyName} optionality does not match ts-modules metadata.` + ); + } + if ( + property && + normalizeTypeScriptType(property.type) !== normalizeTypeScriptType(field.type) + ) { + errors.push( + `Twoslash DTO ${dto.name}.${propertyName} type ${property.type} does not match ts-modules metadata ${field.type}.` + ); + } + } + } + + const knownHandles = new Set(selectedHandles.keys()); + for (const { handle, module } of selectedHandles.values()) { + const declaration = parsedInterfaces.get(handle.name); + if (!declaration) { + errors.push(`Twoslash handle ${handle.name} is missing its declaration.`); + continue; + } + const matchingPackage = packageByIdentity.get(identity(module.package))?.data; + const packageType = (matchingPackage?.types ?? []).find( + (type) => type.fullName === handle.fullName + ); + const generatedDirectBase = handle.baseTypeHierarchy?.[0]; + if (packageType?.baseType) { + if (!generatedDirectBase) { + errors.push( + `TypeScript handle ${handle.name} is missing base hierarchy metadata for C# base type ${packageType.baseType}.` + ); + } else if ( + normalizeClrType(generatedDirectBase) !== normalizeClrType(packageType.baseType) + ) { + errors.push( + `TypeScript handle ${handle.name} base type ${generatedDirectBase} does not match C# metadata ${packageType.baseType}.` + ); + } + } + const expectedParents = new Set( + (handle.implementedInterfaces ?? []) + .map(shortTypeName) + .filter( + (name) => + /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && + name !== handle.name + ) + ); + for (const ancestor of resolveBaseHierarchy(module, handle, packageByIdentity)) { + const name = shortTypeName(ancestor); + if (name !== handle.name && knownHandles.has(name)) expectedParents.add(name); + } + const actualParents = new Set(declaration.parents); + const missing = setDifference(expectedParents, actualParents); + const unexpected = setDifference(actualParents, expectedParents); + if (missing.length > 0 || unexpected.length > 0) { + errors.push( + `Twoslash handle ${handle.name} has incorrect inheritance (missing: ${missing.join(', ') || 'none'}; unexpected: ${unexpected.join(', ') || 'none'}).` + ); + } + } + + if (/options\?:\s*\{\s*options\?:/s.test(input.declarations)) { + errors.push('Twoslash declarations contain a nested single-DTO options wrapper.'); + } + + return { + errors, + checks: [ + `${catalogByName.size} catalog package identities reconciled`, + `${moduleByIdentity.size} TypeScript modules matched to C# provenance`, + `${selectedDtos.size} DTO shapes checked`, + `${selectedHandles.size} handle inheritance chains checked`, + 'attribute payload regressions checked against HEAD', + ], + }; +} + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const frontendDir = path.resolve(scriptDir, '..'); +const repoRoot = path.resolve(frontendDir, '..', '..'); +const dataDir = path.join(frontendDir, 'src', 'data'); +const canonicalPackageDir = path.join(dataDir, 'pkgs'); + +export interface GitCommandResult { + status: number | null; + stdout: string; + stderr: string; + error?: Error; +} + +export type GitCommandRunner = (arguments_: string[]) => GitCommandResult; + +function runGit(arguments_: string[]): GitCommandResult { + const result = spawnSync('git', arguments_, { + encoding: 'utf8', + maxBuffer: 128 * 1024 * 1024, + }); + return { + status: result.status, + stdout: result.stdout ?? '', + stderr: result.stderr ?? '', + error: result.error, + }; +} + +function assertGitSucceeded( + command: string, + relativePath: string, + result: GitCommandResult +): void { + if (result.status === 0) return; + + const detail = result.error?.message ?? result.stderr.trim() ?? ''; + throw new Error( + `Unable to read HEAD baseline for ${relativePath}: git ${command} failed${detail ? `: ${detail}` : '.'}` + ); +} + +export function loadJsonFromHead( + root: string, + relativePath: string, + git: GitCommandRunner = runGit +): T | undefined { + const treeArguments = ['-C', root, 'ls-tree', '--name-only', 'HEAD', '--', relativePath]; + const treeResult = git(treeArguments); + assertGitSucceeded('ls-tree', relativePath, treeResult); + if (treeResult.stdout.trim().length === 0) { + return undefined; + } + + const showResult = git(['-C', root, 'show', `HEAD:${relativePath}`]); + assertGitSucceeded('show', relativePath, showResult); + return JSON.parse(showResult.stdout) as T; +} + +function loadJsonFiles(directory: string, baselineDirectory?: string): GeneratedFile[] { + return fs + .readdirSync(directory) + .filter((fileName) => fileName.endsWith('.json')) + .sort() + .map((fileName) => { + const data = JSON.parse(fs.readFileSync(path.join(directory, fileName), 'utf8')) as T; + let baseline: T | undefined; + if (baselineDirectory) { + const relativePath = path + .relative(repoRoot, path.join(baselineDirectory, fileName)) + .replaceAll(path.sep, '/'); + baseline = loadJsonFromHead(repoRoot, relativePath); + } + return { fileName, data, baseline }; + }); +} + +function main(): void { + const packageDir = process.env.ASPIRE_API_PKGS_DIR + ? path.resolve(process.env.ASPIRE_API_PKGS_DIR) + : canonicalPackageDir; + const moduleDir = process.env.ASPIRE_API_TS_MODULES_DIR + ? path.resolve(process.env.ASPIRE_API_TS_MODULES_DIR) + : path.join(dataDir, 'ts-modules'); + const declarationsFile = process.env.ASPIRE_API_TWOSLASH_FILE + ? path.resolve(process.env.ASPIRE_API_TWOSLASH_FILE) + : path.join(dataDir, 'twoslash', 'aspire.d.ts'); + const result = validateGeneratedApiData({ + catalog: JSON.parse( + fs.readFileSync(path.join(dataDir, 'aspire-integrations.json'), 'utf8') + ) as CatalogEntry[], + packages: loadJsonFiles(packageDir, canonicalPackageDir), + modules: loadJsonFiles(moduleDir), + declarations: fs.readFileSync(declarationsFile, 'utf8'), + }); + + if (result.errors.length > 0) { + console.error(`Generated API validation failed with ${result.errors.length} error(s):`); + for (const error of result.errors) console.error(` - ${error}`); + process.exitCode = 1; + return; + } + + console.log('Generated API semantic validation passed:'); + for (const check of result.checks) console.log(` - ${check}`); +} + +const isMainModule = process.argv[1] + ? path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) + : false; + +if (isMainModule) main(); diff --git a/src/frontend/src/components/api-reference/MemberCard.astro b/src/frontend/src/components/api-reference/MemberCard.astro index ab100ec76..1ca934d3d 100644 --- a/src/frontend/src/components/api-reference/MemberCard.astro +++ b/src/frontend/src/components/api-reference/MemberCard.astro @@ -31,6 +31,8 @@ interface Props { parentType?: ParentTypeInfo; sourceBaseUrl?: string; diagnosticSlugs?: Set; + anchorAliases?: string[]; + exactAnchor?: string; } const { @@ -40,6 +42,8 @@ const { parentType, sourceBaseUrl, diagnosticSlugs, + anchorAliases = [], + exactAnchor, } = Astro.props; const base = import.meta.env.BASE_URL.replace(/\/$/, ''); @@ -95,7 +99,12 @@ const returnLink = member.returnType const rawDisplayName = memberDisplayName(member); const displayName = member.name === '.ctor' ? rawDisplayName.replace('.ctor', 'Constructor') : rawDisplayName; -const anchorSlug = memberSlug(member); +const anchorSlug = exactAnchor ?? memberSlug(member); +const aliases = anchorAliases.filter((alias) => alias !== anchorSlug); +const primaryAnchorSlug = aliases[0] ?? anchorSlug; +const hiddenAnchorSlugs = primaryAnchorSlug === anchorSlug + ? [] + : [anchorSlug, ...aliases.slice(1)]; const isCtor = member.name === '.ctor'; const returnShort = member.returnType ? shortTypeName(member.returnType) : member.returnType; const returnColorClass = returnShort ? `mc-param-type-${typeColorIndex(returnShort)}` : ''; @@ -152,7 +161,10 @@ if (formattedSigRaw && (member.kind === 'property' || member.kind === 'indexer') } --- -
+
+ {hiddenAnchorSlugs.map((slug) => ( +