diff --git a/.github/shared/package-lock.json b/.github/shared/package-lock.json new file mode 100644 index 000000000000..4de254eb96ac --- /dev/null +++ b/.github/shared/package-lock.json @@ -0,0 +1,2932 @@ +{ + "name": "@azure-tools/specs-shared", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@azure-tools/specs-shared", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^15.1.3", + "debug": "^4.4.3", + "js-yaml": "^4.1.0", + "marked": "^18.0.0", + "simple-git": "^3.36.0", + "zod": "^4.3.5" + }, + "bin": { + "spec-model": "cmd/spec-model.js" + }, + "devDependencies": { + "@eslint/js": "^10.0.0", + "@tsconfig/node20": "^20.1.4", + "@types/debug": "^4.1.12", + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.0.0", + "@types/semver": "^7.7.1", + "@vitest/coverage-v8": "^4.1.0", + "cross-env": "^10.1.0", + "eslint": "^10.0.0", + "globals": "^17.0.0", + "prettier": "3.8.3", + "prettier-plugin-organize-imports": "^4.2.0", + "semver": "^7.7.1", + "tinybench": "^6.0.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.0", + "vitest": "^4.1.0" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "15.3.5", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-15.3.5.tgz", + "integrity": "sha512-orNOYXw3hYXxxisXMldjzjBzqqTLBPbwOtHg7ovBPvfBHDue1qM9YJENZ3W2BQuS+7z4ThogMbEzEsov57Itkg==", + "license": "MIT", + "dependencies": { + "js-yaml": "^4.1.1" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@types/json-schema": "^7.0.15" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@simple-git/args-pathspec": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz", + "integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==", + "license": "MIT" + }, + "node_modules/@simple-git/argv-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz", + "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==", + "license": "MIT", + "dependencies": { + "@simple-git/args-pathspec": "^1.0.3" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node20": { + "version": "20.1.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.9.tgz", + "integrity": "sha512-IjlTv1RsvnPtUcjTqtVsZExKVq+KQx4g5pCP5tI7rAs6Xesl2qFwSz/tPDBC4JajkL/MlezBu3gPUwqRHl+RIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.42", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.42.tgz", + "integrity": "sha512-5L7SUaFC1RyDraj2yRhyBzHTobyXHmohD100CChNtyPyleoq37Mqab5Gn8XEKI04dfN/oqPdpHk38MgcQWHbZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", + "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/type-utils": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.61.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz", + "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz", + "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.61.0", + "@typescript-eslint/types": "^8.61.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz", + "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz", + "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz", + "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz", + "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz", + "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.61.0", + "@typescript-eslint/tsconfig-utils": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz", + "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz", + "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz", + "integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.8", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.8", + "vitest": "4.1.8" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.3.tgz", + "integrity": "sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.1.tgz", + "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "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" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/marked": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", + "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-organize-imports": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.3.0.tgz", + "integrity": "sha512-FxFz0qFhyBsGdIsb697f/EkvHzi5SZOhWAjxcx2dLt+Q532bAlhswcXGYB1yzjZ69kW8UoadFBw7TyNwlq96Iw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": ">=2.0", + "typescript": ">=2.9", + "vue-tsc": "^2.1.0 || 3" + }, + "peerDependenciesMeta": { + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/semver": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.3.tgz", + "integrity": "sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-git": { + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz", + "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==", + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "@simple-git/args-pathspec": "^1.0.3", + "@simple-git/argv-parser": "^1.1.0", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinybench": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.0.2.tgz", + "integrity": "sha512-FlHoQpcFvCzeXK5kVPvV7IVgW/hs/B36QWTz876iSdeJguBDfdTSRQmYmaHX+fQNt4hp+gEFB2XXw+8hT4/y8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.61.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz", + "integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.61.0", + "@typescript-eslint/parser": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/.github/shared/package.json b/.github/shared/package.json new file mode 100644 index 000000000000..df75018baf14 --- /dev/null +++ b/.github/shared/package.json @@ -0,0 +1,76 @@ +{ + "name": "@azure-tools/specs-shared", + "private": true, + "type": "module", + "exports": { + "./array": "./src/array.js", + "./breaking-change": "./src/breaking-change.js", + "./changed-files": "./src/changed-files.js", + "./console": "./src/console.js", + "./eslint-base-config": "./eslint.base.config.js", + "./exec": "./src/exec.js", + "./git": "./src/git.js", + "./github": "./src/github.js", + "./logger": "./src/logger.js", + "./math": "./src/math.js", + "./path": "./src/path.js", + "./readme": "./src/readme.js", + "./sdk-types": "./src/sdk-types.js", + "./set": "./src/set.js", + "./simple-git": "./src/simple-git.js", + "./sleep": "./src/sleep.js", + "./sort": "./src/sort.js", + "./spec-model-error": "./src/spec-model-error.js", + "./spec-model": "./src/spec-model.js", + "./swagger": "./src/swagger.js", + "./tag": "./src/tag.js", + "./time": "./src/time.js", + "./test/examples": "./test/examples.js" + }, + "bin": { + "spec-model": "./cmd/spec-model.js" + }, + "_comments": { + "dependencies": "Runtime dependencies must be kept to an absolute minimum for performance, ideally with no transitive dependencies", + "dependencies2": "All runtime and dev dependencies in this file, must be a subset of ../package.json" + }, + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^15.1.3", + "debug": "^4.4.3", + "js-yaml": "^4.1.0", + "marked": "^18.0.0", + "simple-git": "^3.36.0", + "zod": "^4.3.5" + }, + "devDependencies": { + "@eslint/js": "^10.0.0", + "@tsconfig/node20": "^20.1.4", + "@types/debug": "^4.1.12", + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.0.0", + "@types/semver": "^7.7.1", + "@vitest/coverage-v8": "^4.1.0", + "cross-env": "^10.1.0", + "eslint": "^10.0.0", + "globals": "^17.0.0", + "prettier": "3.8.3", + "prettier-plugin-organize-imports": "^4.2.0", + "semver": "^7.7.1", + "tinybench": "^6.0.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.0", + "vitest": "^4.1.0" + }, + "scripts": { + "check": "npm run test:ci && npm run lint && npm run format:check", + "lint": "npm run lint:eslint && npm run lint:tsc", + "lint:eslint": "cross-env DEBUG=eslint:eslint,eslint:linter eslint", + "lint:tsc": "tsc --build --verbose", + "format": "prettier . --ignore-path ../.prettierignore --write", + "format:check": "prettier . --ignore-path ../.prettierignore --check", + "format:check:ci": "prettier . --ignore-path ../.prettierignore --check --log-level debug", + "perf": "node perf/perf.js", + "test": "vitest", + "test:ci": "vitest run --coverage --reporter=verbose" + } +} diff --git a/.github/shared/src/cache.js b/.github/shared/src/cache.js new file mode 100644 index 000000000000..444a7ce2f932 --- /dev/null +++ b/.github/shared/src/cache.js @@ -0,0 +1,61 @@ +/** + * Caches values in memory with a single key of any type. + * + * @template K, V + */ +export class KeyedCache { + /** @type {Map} */ + #map = new Map(); + + /** + * Returns cached value, initializing if necessary + * + * @param {K} key + * @param {() => V} factory + * @returns {V} cached value + * + * @example + * const result = cache.getOrCreate(42, async () => await doWork(42)); + */ + getOrCreate(key, factory) { + let value = this.#map.get(key); + + if (value === undefined) { + value = factory(); + this.#map.set(key, value); + } + + return value; + } +} + +/** + * Caches values in memory with an ordered pair of keys of any types. + * + * @template K1, K2, V + */ +export class KeyedPairCache { + // Two-layer nested cache + /** @type {KeyedCache>} */ + #cache1 = new KeyedCache(); + + /** + * Returns cached value, initializing if necessary. + * Keys are ordered, so (key1, key2) != (key2, key1). + * + * @param {K1} key1 + * @param {K2} key2 + * @param {() => V} factory + * @returns {V} cached value + * + * @example + * const result = cache.getOrCreate(42, 7, async () => await doWork(42, 7)); + */ + getOrCreate(key1, key2, factory) { + // key1 => cache for the next layer + const cache2 = this.#cache1.getOrCreate(key1, () => new KeyedCache()); + + // key2 => final value + return cache2.getOrCreate(key2, factory); + } +} diff --git a/.github/shared/src/changed-files.js b/.github/shared/src/changed-files.js new file mode 100644 index 000000000000..43b26ddbd877 --- /dev/null +++ b/.github/shared/src/changed-files.js @@ -0,0 +1,315 @@ +import debug from "debug"; +import { simpleGit } from "simple-git"; +import { KeyedCache } from "./cache.js"; +import { includesSegment } from "./path.js"; + +// cSpell:ignore unshift + +// Enable simple-git debug logging to improve console output +debug.enable("simple-git"); + +// Cache results of the `example` filter, using the un-resolved path for maximum perf +// The `example` filter is a hot path in spec-model for large specs like "network". +/** @type {KeyedCache} */ +const exampleCache = new KeyedCache(); + +/** + * Get a list of changed files in a git repository + * + * @param {Object} [options] + * @param {string} [options.baseCommitish] Default: "HEAD^". + * @param {string} [options.cwd] Current working directory. Default: process.cwd(). + * @param {string[]} [options.gitOptions] Additional git options to pass to git diff command. Example: ["--no-renames"]. Default: [] + * @param {string} [options.headCommitish] Default: "HEAD". + * @param {import('./logger.js').ILogger} [options.logger] + * @param {string[]} [options.paths] Limits the diff to the named paths. If not set, includes all paths in repo. Default: [] + * @returns {Promise} List of changed files, using posix paths, relative to repo root. Example: ["specification/foo/Microsoft.Foo/main.tsp"]. + */ +export async function getChangedFiles(options = {}) { + const { + baseCommitish = "HEAD^", + cwd, + gitOptions = [], + headCommitish = "HEAD", + logger, + paths = [], + } = options; + + if (paths.length > 0) { + // Use "--" to separate paths from revisions + paths.unshift("--"); + } + + // TODO: If we need to filter based on status, instead of passing an argument to `--diff-filter, + // consider using "--name-status" instead of "--name-only", and return an array of objects like + // { name: "/foo/baz.js", status: Status.Renamed, previousName: "/foo/bar.js"}. + // Then add filter functions to filter based on status. This is more flexible and lets consumers + // filter based on status with a single call to `git diff`. + const result = await simpleGit(cwd).diff([ + "--name-only", + ...gitOptions, + baseCommitish, + headCommitish, + ...paths, + ]); + + const files = result + .trim() + .split("\n") + // ignore empty lines (e.g. when no files are changed) + .filter((s) => s.length > 0); + logger?.info("Changed Files:"); + for (const file of files) { + logger?.info(` ${file}`); + } + logger?.info(""); + + return files; +} + +/** + * Get a list of changed files in a git repository with statuses for additions, + * modifications, deletions, and renames. Warning: rename behavior can vary + * based on the git client's configuration of diff.renames. + * + * @param {Object} [options] + * @param {string} [options.baseCommitish] Default: "HEAD^". + * @param {string} [options.cwd] Current working directory. Default: process.cwd(). + * @param {string[]} [options.gitOptions] Additional git options to pass to git diff command. Example: ["--no-renames"]. Default: [] + * @param {string} [options.headCommitish] Default: "HEAD". + * @param {import('./logger.js').ILogger} [options.logger] + * @param {string[]} [options.paths] Limits the diff to the named paths. If not set, includes all paths in repo. Default: [] + * @returns {Promise<{additions: string[], modifications: string[], deletions: string[], renames: {from: string, to: string}[], total: number}>} + */ +export async function getChangedFilesStatuses(options = {}) { + const { + baseCommitish = "HEAD^", + cwd, + gitOptions = [], + headCommitish = "HEAD", + logger, + paths = [], + } = options; + + if (paths.length > 0) { + // Use "--" to separate paths from revisions + paths.unshift("--"); + } + + const result = await simpleGit(cwd).diff([ + "--name-status", + ...gitOptions, + baseCommitish, + headCommitish, + ...paths, + ]); + + const categorizedFiles = { + additions: /** @type {string[]} */ ([]), + modifications: /** @type {string[]} */ ([]), + deletions: /** @type {string[]} */ ([]), + renames: /** @type {{from: string, to: string}[]} */ ([]), + total: 0, + }; + + if (result.trim()) { + const lines = result.trim().split("\n"); + + for (const line of lines) { + const parts = line.split("\t"); + const status = parts[0]; + + switch (status[0]) { + case "A": + categorizedFiles.additions.push(parts[1]); + break; + case "M": + categorizedFiles.modifications.push(parts[1]); + break; + case "D": + categorizedFiles.deletions.push(parts[1]); + break; + case "R": + categorizedFiles.renames.push({ + from: parts[1], + to: parts[2], + }); + break; + case "C": + categorizedFiles.additions.push(parts[2]); + break; + default: + categorizedFiles.modifications.push(parts[1]); + } + } + + categorizedFiles.total = + categorizedFiles.additions.length + + categorizedFiles.modifications.length + + categorizedFiles.deletions.length + + categorizedFiles.renames.length; + } + + // Log all changed files by categories + if (logger) { + logger.info("Categorized Changed Files:"); + + if (categorizedFiles.additions.length > 0) { + logger.info(` Additions (${categorizedFiles.additions.length}):`); + for (const file of categorizedFiles.additions) { + logger.info(` + ${file}`); + } + } + + if (categorizedFiles.modifications.length > 0) { + logger.info(` Modifications (${categorizedFiles.modifications.length}):`); + for (const file of categorizedFiles.modifications) { + logger.info(` M ${file}`); + } + } + + if (categorizedFiles.deletions.length > 0) { + logger.info(` Deletions (${categorizedFiles.deletions.length}):`); + for (const file of categorizedFiles.deletions) { + logger.info(` - ${file}`); + } + } + + if (categorizedFiles.renames.length > 0) { + logger.info(` Renames (${categorizedFiles.renames.length}):`); + for (const rename of categorizedFiles.renames) { + logger.info(` R ${rename.from} -> ${rename.to}`); + } + } + + logger.info(` Total: ${categorizedFiles.total} files`); + logger.info(""); + } + + return categorizedFiles; +} + +// Functions suitable for passing to string[].filter(), ordered roughly in order of increasing specificity +// Functions accept both relative and absolute paths, since paths are resolve()'d before searching (when needed) + +/** + * @param {string} [file] + * @returns {boolean} + */ +export function json(file) { + // Extension "json" with any case is a valid JSON file + return typeof file === "string" && file.toLowerCase().endsWith(".json"); +} + +/** + * @param {string} [file] + * @returns {boolean} + */ +export function markdown(file) { + // Extension ".md" with any case is a valid markdown file + return typeof file === "string" && file.toLowerCase().endsWith(".md"); +} + +/** + * @param {string} [file] + * @returns {boolean} + */ +export function readme(file) { + // Filename "readme.md" with any case is a valid README file + return typeof file === "string" && file.toLowerCase().endsWith("readme.md"); +} + +/** + * @param {string} [file] + * @returns {boolean} + */ +export function dataPlane(file) { + // Folder name "data-plane" should match case for consistency across specs + return typeof file === "string" && includesSegment(file, "data-plane"); +} + +/** + * @param {string} [file] + * @returns {boolean} + */ +export function resourceManager(file) { + // Folder name "resource-manager" should match case for consistency across specs + return typeof file === "string" && includesSegment(file, "resource-manager"); +} + +/** + * @param {string} [file] + * @returns {boolean} + */ +export function preview(file) { + // Folder name "preview" should match case for consistency across specs + return typeof file === "string" && includesSegment(file, "preview"); +} + +/** + * @param {string} [file] + * @returns {boolean} + */ +export function stable(file) { + // Folder name "stable" should match case for consistency across specs + return typeof file === "string" && includesSegment(file, "stable"); +} + +/** + * @param {string} [file] + * @returns {boolean} + */ +export function example(file) { + return ( + typeof file === "string" && + // Intentionally use un-resolved path as key for perf, since we are OK + // caching the same result for different representations of the same path. + exampleCache.getOrCreate( + file, + // Folder name "examples" should match case for consistency across specs + () => json(file) && includesSegment(file, "examples"), + ) + ); +} + +/** + * @param {string} file + * @returns {boolean} + */ +export function typespec(file) { + return ( + typeof file === "string" && + (file.toLowerCase().endsWith(".tsp") || file.toLowerCase().endsWith("tspconfig.yaml")) + ); +} + +/** + * @param {string} [file] + * @returns {boolean} + */ +export function quickstartTemplate(file) { + return typeof file === "string" && json(file) && file.includes("/quickstart-templates/"); +} + +/** + * @param {string} [file] + * @returns {boolean} + */ +export function swagger(file) { + return ( + typeof file === "string" && + json(file) && + (dataPlane(file) || resourceManager(file)) && + !example(file) && + !quickstartTemplate(file) && + !scenario(file) + ); +} + +/** + * @param {string} [file] + * @returns {boolean} + */ +export function scenario(file) { + return typeof file === "string" && json(file) && includesSegment(file, "scenarios"); +} diff --git a/.github/shared/src/exec.js b/.github/shared/src/exec.js new file mode 100644 index 000000000000..aba68ea0cf25 --- /dev/null +++ b/.github/shared/src/exec.js @@ -0,0 +1,134 @@ +import child_process from "child_process"; +import { dirname, join } from "path"; +import { promisify } from "util"; +const execFileImpl = promisify(child_process.execFile); + +/** + * @typedef {Object} ExecOptions + * @property {string} [cwd] Current working directory. Default: process.cwd(). + * @property {import('./logger.js').ILogger} [logger] + * @property {number} [maxBuffer] Max bytes allowed on stdout or stderr. Default: 16 * 1024 * 1024. + */ + +/** + * @typedef {Object} NpmPrefixOptions + * @property {string} [prefix] Prefix to pass to npm via "--prefix". + */ + +/** + * @typedef {ExecOptions & NpmPrefixOptions} ExecNpmOptions + */ + +/** + * @typedef {Object} ExecResult + * @property {string} stdout + * @property {string} stderr + */ + +/** + * @typedef {Error & { stdout?: string, stderr?: string, code?: number }} ExecError + */ + +/** + * Checks whether an unknown error object is an ExecError. + * @param {unknown} error + * @returns {error is ExecError} + */ +export function isExecError(error) { + if (!(error instanceof Error)) return false; + + const e = /** @type {ExecError} */ (error); + return typeof e.stdout === "string" || typeof e.stderr === "string"; +} + +/** + * Wraps `child_process.execFile()`, adding logging and a larger default maxBuffer. + * + * @param {string} file + * @param {string[]} [args] + * @param {ExecOptions} [options] + * @returns {Promise} + * @throws {ExecError} + */ +export async function execFile(file, args, options = {}) { + const { + cwd, + logger, + // Node default is 1024 * 1024, which is too small for some git commands returning many entities or large file content. + // To support "git show", should be larger than the largest swagger file in the repo (2.5 MB as of 2/28/2025). + maxBuffer = 16 * 1024 * 1024, + } = options; + + logger?.info(`execFile("${file}", ${JSON.stringify(args)})`); + + try { + // execFile(file, args) is more secure than exec(cmd), since the latter is vulnerable to shell injection + const result = await execFileImpl(file, args, { + cwd, + maxBuffer, + }); + + logger?.debug(`stdout: '${result.stdout}'`); + logger?.debug(`stderr: '${result.stderr}'`); + + return result; + } catch (error) { + /* v8 ignore next */ + logger?.debug(`error: '${JSON.stringify(error)}'`); + + throw error; + } +} + +/** + * Calls `execFile()` with appropriate arguments to run `npm` on all platforms + * + * @param {string[]} args + * @param {ExecNpmOptions} [options] + * @returns {Promise} + * @throws {ExecError} + */ +export async function execNpm(args, options = {}) { + const { prefix } = options; + + // Exclude platform-specific code from coverage + /* v8 ignore start */ + const { file, defaultArgs } = + process.platform === "win32" + ? { + // Only way I could find to run "npm" on Windows, without using the shell (e.g. "cmd /c npm ...") + // + // "node.exe", ["--", "npm-cli.js", ...args] + // + // The "--" MUST come BEFORE "npm-cli.js", to ensure args are sent to the script unchanged. + // If the "--" comes after "npm-cli.js", the args sent to the script will be ["--", ...args], + // which is NOT equivalent, and can break if args itself contains another "--". + + // example: "C:\Program Files\nodejs\node.exe" + file: process.execPath, + + // example: "C:\Program Files\nodejs\node_modules\npm\bin\npm-cli.js" + defaultArgs: [ + "--", + join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"), + ], + } + : { file: "npm", defaultArgs: [] }; + /* v8 ignore stop */ + + const prefixArgs = prefix ? ["--prefix", prefix] : []; + + return await execFile(file, [...defaultArgs, ...prefixArgs, ...args], options); +} + +/** + * Calls `execNpm()` with arguments ["exec", "--no", "--"] prepended. + * + * @param {string[]} args + * @param {ExecNpmOptions} [options] + * @returns {Promise} + * @throws {ExecError} + */ +export async function execNpmExec(args, options = {}) { + return await execNpm(["exec", "--no", "--", ...args], options); +} diff --git a/.github/shared/src/github.js b/.github/shared/src/github.js new file mode 100644 index 000000000000..501a512810da --- /dev/null +++ b/.github/shared/src/github.js @@ -0,0 +1,102 @@ +/* v8 ignore start */ + +export const PER_PAGE_MAX = 100; + +/** + * https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks#check-statuses-and-conclusions + * + * @readonly + * @enum {"completed" | "expected" | "failure" | "in_progress" | "pending" | "queued" | "requested" | "startup_failure" | "waiting" } + */ +export const CheckStatus = Object.freeze({ + /** + * @description The check run completed and has a conclusion. + */ + COMPLETED: "completed", + /** + * @description The check run is waiting for a status to be reported. + */ + EXPECTED: "expected", + /** + * @description The check run failed. + */ + FAILURE: "failure", + /** + * @description The check run is in progress. + */ + IN_PROGRESS: "in_progress", + /** + * @description The check run is at the front of the queue but the group-based concurrency limit has been reached. + */ + PENDING: "pending", + /** + * @description The check run has been queued. + */ + QUEUED: "queued", + /** + * @description The check run has been created but has not been queued. + */ + REQUESTED: "requested", + /** + * @description The check suite failed during startup. This status is not applicable to check runs. + */ + STARTUP_FAILURE: "startup_failure", + /** + * @description The check run is waiting for a deployment protection rule to be satisfied. + */ + WAITING: "waiting", +}); + +/** + * https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks#check-statuses-and-conclusions + * + * @readonly + * @enum {"action_required" | "cancelled" | "failure" | "neutral" | "skipped" | "stale" | "success" | "timed_out" } + */ +export const CheckConclusion = Object.freeze({ + /** + * @description The check run provided required actions upon its completion. For more information, see Using the REST API to interact with checks. + */ + ACTION_REQUIRED: "action_required", + /** + * @description The check run was cancelled before it completed. + */ + CANCELLED: "cancelled", + /** + * @description The check run failed. + */ + FAILURE: "failure", + /** + * @description The check run completed with a neutral result. This is treated as a success for dependent checks in GitHub Actions. + */ + NEUTRAL: "neutral", + /** + * @description The check run was skipped. This is treated as a success for dependent checks in GitHub Actions. + */ + SKIPPED: "skipped", + /** + * @description The check run was marked stale by GitHub because it took too long. + */ + STALE: "stale", + /** + * @description The check run completed successfully. + */ + SUCCESS: "success", + /** + * @description The check run timed out. + */ + TIMED_OUT: "timed_out", +}); + +/** + * https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#create-a-commit-status--parameters + * + * @readonly + * @enum {"error" | "failure" | "pending" | "success"} + */ +export const CommitStatusState = Object.freeze({ + ERROR: "error", + FAILURE: "failure", + PENDING: "pending", + SUCCESS: "success", +}); diff --git a/.github/shared/src/logger.js b/.github/shared/src/logger.js new file mode 100644 index 000000000000..dc43a06d9782 --- /dev/null +++ b/.github/shared/src/logger.js @@ -0,0 +1,64 @@ +/** + * @typedef {Object} ILogger + * @property {(message:string) => void} debug + * @property {(message:string) => void} error + * @property {(message:string) => void} info + * @property {(message:string) => void} warning + * @property {() => boolean} isDebug + */ + +/** + * @implements {ILogger} + */ +export class ConsoleLogger { + /** @type {boolean} */ + #isDebug; + + /** + * @param {boolean} [isDebug] - If true, debug logs will be printed. Default: false. + */ + constructor(isDebug = false) { + this.#isDebug = isDebug; + } + + /** + * @param {string} message + */ + debug(message) { + if (this.isDebug()) { + console.debug(message); + } + } + + /** + * @param {string} message + */ + error(message) { + console.error(message); + } + + /** + * @param {string} message + */ + info(message) { + console.log(message); + } + + /** + * @returns {boolean} + */ + isDebug() { + return this.#isDebug; + } + + /** + * @param {string} message + */ + warning(message) { + console.warn(message); + } +} + +// Singleton loggers +export const defaultLogger = new ConsoleLogger(); +export const debugLogger = new ConsoleLogger(/*isDebug*/ true); diff --git a/.github/shared/src/path.js b/.github/shared/src/path.js new file mode 100644 index 000000000000..929116c931b6 --- /dev/null +++ b/.github/shared/src/path.js @@ -0,0 +1,104 @@ +import { basename, dirname, resolve } from "path"; + +import { KeyedCache, KeyedPairCache } from "./cache.js"; + +/** @type {KeyedCache} */ +const resolveCache = new KeyedCache(); + +/** @type {KeyedPairCache} */ +const resolvePairCache = new KeyedPairCache(); + +/** + * + * @param {string} path Absolute or relative path + * @param {string} segment File or folder + * @returns {boolean} True if resolved path contains segment + * + * @example + * includesSegment("stable/2025-01-01/examples/foo.json", "examples") + * // -> true + */ +export function includesSegment(path, segment) { + return untilLastSegment(path, segment) !== ""; +} + +/** + * Wraps `path.resolve(path)` with a cache to improve performance + * + * @param {string} path + * @returns {string} + */ +export function resolveCached(path) { + return resolveCache.getOrCreate(path, () => resolve(path)); +} + +/** + * Wraps `path.resolve(from, to)` with a cache to improve performance + +* @param {string} from + * @param {string} to + * @returns {string} + */ +export function resolvePairCached(from, to) { + return resolvePairCache.getOrCreate(from, to, () => resolve(from, to)); +} + +/** + * @param {string} path Absolute or relative path + * @param {string} segment File or folder + * @returns {string} Portion of resolved path up to (and including) the last occurrence of segment + * + * @example + * untilLastSegment("stable/2025-01-01/examples/foo.json", "examples") + * // -> "{cwd}/stable/2025-01-01/examples" + */ +export function untilLastSegment(path, segment) { + // Shares code with `untilLastSegmentWithParent()`, but not worth refactoring yet + + let current = resolveCached(path); + + while (true) { + const parent = dirname(current); + + if (basename(current) === segment) { + // Found the target folder. Return it. + return current; + } else if (parent === current) { + // Reached the filesystem root (folder not found). Return empty string. + return ""; + } else { + // Keep walking upward + current = parent; + } + } +} + +/** + * @param {string} path Absolute or relative path + * @param {string} segment File or folder + * @returns {string} Portion of resolved path up to (and including) the last segment with the specified parent + * + * @example + * untilLastSegmentWithParent("specification/foo/data-plane/stable/2025-01-01/foo.json", "specification") + * // -> "{cwd}/specification/foo" + */ +export function untilLastSegmentWithParent(path, segment) { + // Shares code with `untilLastSegment()`, but not worth refactoring yet + + let current = resolveCached(path); + + while (true) { + const parent = dirname(current); + + if (basename(parent) === segment) { + // Found the target parent. Return current; + return current; + } else if (parent === current) { + // Reached the filesystem root (folder not found). Return empty string. + return ""; + } else { + // Keep walking upward + current = parent; + } + } +} diff --git a/.github/shared/src/simple-git.js b/.github/shared/src/simple-git.js new file mode 100644 index 000000000000..c8241c839e35 --- /dev/null +++ b/.github/shared/src/simple-git.js @@ -0,0 +1,13 @@ +import { resolve } from "path"; +import { simpleGit } from "simple-git"; + +/** + * + * @param {string} inputPath + * @returns {Promise} + */ +export async function getRootFolder(inputPath) { + // expecting users to handle the case where inputPath is not a git repo + const gitRoot = await simpleGit(inputPath).revparse("--show-toplevel"); + return resolve(gitRoot.trim()); +} diff --git a/.github/skills/create-api-review-pr/SKILL.md b/.github/skills/create-api-review-pr/SKILL.md new file mode 100644 index 000000000000..1d71b4d6ab90 --- /dev/null +++ b/.github/skills/create-api-review-pr/SKILL.md @@ -0,0 +1,91 @@ +--- +name: create-api-review-pr +description: Create a GitHub PR for API review by comparing a baseline API surface against a target tag or branch. Use this when the user wants to create an API review PR, compare API changes between versions, or review API surface differences for a package. +--- + +# Create API Review PR + +Creates a dedicated API review PR that shows the diff between a baseline release and a target tag or branch's API surface using `scripts/api_md_workflow/create_api_review_pr.js`. + +## Unsupported Requests + +If the user asks to create an API review PR for a new package, explain that new packages do not use API review PRs and stop. Do not gather script inputs or run `create_api_review_pr.js` for new packages. + +## Prerequisites + +1. The user must have `gh` CLI installed and authenticated (`gh auth login`). +2. The working tree must be clean (no uncommitted changes). +3. The latest Node.js LTS must be installed. +4. `azpysdk` must be installed (`pip install -e ./eng/tools/azure-sdk-tools`). + +## Information to Gather + +Ask the user for the following using `vscode_askQuestions`: + +### 1. Package Name (required) +The Azure SDK package name (e.g. `azure-storage-blob`, `azure-ai-projects`, `azure-servicebus`, `azure-planetarycomputer`). + +### 2. Baseline (required) +The release tag to use as the baseline for comparison. Tags follow the format `_` (e.g. `azure-storage-blob_12.29.0`). + +- If the user provides a package name and version separately, construct the tag as `_`. + +### 3. Target (optional) +The branch or PR to generate the "current" API surface from. Can be: +- A package release tag (e.g. `azure-storage-blob_12.30.0`) — used directly as a tag ref +- A branch name (e.g. `main`, `feature-branch`) — fetched from `origin` +- An `owner:branch` reference (e.g. `someone:their-branch`) — fetched from the fork +- If omitted, defaults to `origin/main` + +## Validation Steps + +Before running the script: + +1. **Validate the package exists**: Confirm a directory matching `sdk/*/` exists with a `pyproject.toml` or `setup.py`. +2. **Validate the baseline tag**: Run `git tag -l ""` to confirm the tag exists. If the user provided a version like `12.29.0`, construct the full tag as `_` and validate that. +3. **Validate the target tag when applicable**: If the user provided a target version or tag, construct or validate the full tag as `_` and run `git tag -l ""`. +4. **Confirm the working tree is clean**: Run `git status --porcelain` and warn if there are uncommitted changes. + +## Execution + +This is a long-running operation. The script may take several minutes because it generates API surfaces for both the baseline and target, creates or reuses review branches, pushes branches, and then opens the draft PR. Do not treat quiet terminal periods during `apistub` generation as failure unless the command exits, prints an error, or waits for input. + +If `create_api_review_pr.js` fails while running this skill, do not patch the script, modify package files, retry with workaround edits, or try to manually complete branch/PR creation. Stop the workflow, report the failure clearly, include the relevant error details, and suggest practical next steps. + +If the script reports that there are no API differences, relay that message to the user and stop. Do not create branches or a PR manually. + +Run the following command from the repository root: + +```bash +node scripts/api_md_workflow/create_api_review_pr.js --package-name --base [--target ] +``` + +### Examples + +**Standard review (comparing a release tag to a PR branch):** +```bash +node scripts/api_md_workflow/create_api_review_pr.js --package-name azure-storage-blob --base azure-storage-blob_12.29.0 --target someone:feature-branch +``` + +**Release-to-release review (comparing two package tags):** +```bash +node scripts/api_md_workflow/create_api_review_pr.js --package-name azure-ai-projects --base azure-ai-projects_2.1.0 --target azure-ai-projects_2.2.0 +``` + +**Review against main (no target specified):** +```bash +node scripts/api_md_workflow/create_api_review_pr.js --package-name azure-cosmos --base azure-cosmos_4.14.0 +``` + +## Post-Execution + +The script will: +1. Generate `api.md` for both baseline and target +2. Push `apireview/base__` and `apireview/review__` branches +3. Open a draft PR (or print a compare URL if `gh pr create` fails) + +During execution, report progress at major phases: baseline generation, target generation, branch creation or reuse, branch push, and PR creation. If the terminal is quiet, check whether the process is still running before assuming it is hung. + +When the target is a tag, the PR body labels it as `Target tag`. Branch and fork targets are labeled as `Working branch`. + +Report the PR URL to the user when complete. diff --git a/.github/skills/generate-api-markdown/SKILL.md b/.github/skills/generate-api-markdown/SKILL.md index f3f96e32c839..dbf28fcbb551 100644 --- a/.github/skills/generate-api-markdown/SKILL.md +++ b/.github/skills/generate-api-markdown/SKILL.md @@ -7,7 +7,7 @@ description: Generate an API markdown file and token file using ApiView. Use thi ## Prerequisites -1. Activate your virtual environment with a Python version that is strictly less than the version limit specified in `eng/tools/azure-sdk-tools/azpysdk/apistub.py`. +1. Activate your virtual environment. 2. Install the required dependencies: ```bash cd @@ -19,5 +19,6 @@ description: Generate an API markdown file and token file using ApiView. Use thi 1. Navigate to the desired package directory 2. Run the command: ```bash - azpysdk apistub --md . + azpysdk apistub --md --extract-metadata --install-deps --dest-dir . . + ``` 3. The command outputs the location of the generated markdown file. Provide this file to the user for review. \ No newline at end of file diff --git a/.github/workflows/api-consistency.yml b/.github/workflows/api-consistency.yml new file mode 100644 index 000000000000..7bdf8a93d9ad --- /dev/null +++ b/.github/workflows/api-consistency.yml @@ -0,0 +1,66 @@ +name: API.md Consistency + +on: + pull_request: + types: + # default + - opened + - synchronize + - reopened + # re-run if base branch is changed, since previous merge commit may generate incorrect diff + - edited + # re-run if PR changes to/from draft + - converted_to_draft + - ready_for_review + paths: + - "sdk/**" + +permissions: + contents: read + +jobs: + consistency: + if: ${{ !github.event.pull_request.draft }} + runs-on: ubuntu-latest + outputs: + changed_count: ${{ steps.consistency.outputs.changed_count || '0' }} + mismatch_count: ${{ steps.consistency.outputs.mismatch_count || '0' }} + missing_count: ${{ steps.consistency.outputs.missing_count || '0' }} + issue_count: ${{ steps.consistency.outputs.issue_count || '0' }} + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 2 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Install azpysdk + shell: bash + run: | + python -m pip install --upgrade pip + python -m pip install -r eng/apiview_reqs.txt --index-url=https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/ + python -m pip install ./eng/tools/azure-sdk-tools + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: "24" + + - name: Run API.md consistency checks + id: consistency + uses: actions/github-script@v8 + env: + API_MD_BASE_REF: ${{ github.event.pull_request.base.ref }} + API_MD_CHANGED_FILE: .artifacts/changed_package_dirs.txt + API_MD_PACKAGES_FILE: .artifacts/affected_package_dirs.txt + API_MD_MISMATCHES_FILE: .artifacts/mismatched_api_files.txt + API_MD_MISSING_FILE: .artifacts/missing_api_files.txt + with: + script: | + const { default: apiMdConsistency } = + await import('${{ github.workspace }}/.github/workflows/src/api-md-consistency/api-md-consistency.js'); + return await apiMdConsistency({ github, context, core }); diff --git a/.github/workflows/api-md-workflow-tests.yml b/.github/workflows/api-md-workflow-tests.yml new file mode 100644 index 000000000000..8a10b49e7df0 --- /dev/null +++ b/.github/workflows/api-md-workflow-tests.yml @@ -0,0 +1,26 @@ +name: API.md Workflow Unit Tests + +on: + workflow_dispatch: + pull_request: + branches: [ main ] + paths: + - "scripts/api_md_workflow/**" + - ".github/workflows/api-md-workflow-tests.yml" + +permissions: + contents: read + +jobs: + unit-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: "24" + + - name: Run API.md workflow unit tests + run: node --test scripts/api_md_workflow/*.test.js \ No newline at end of file diff --git a/.github/workflows/src/api-md-consistency/api-md-consistency.js b/.github/workflows/src/api-md-consistency/api-md-consistency.js new file mode 100644 index 000000000000..7c397e67a86b --- /dev/null +++ b/.github/workflows/src/api-md-consistency/api-md-consistency.js @@ -0,0 +1,133 @@ +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const SHARED_SRC_ROOT = path.resolve(__dirname, "..", "..", "..", "shared", "src"); +const sharedModuleCache = new Map(); + +async function loadSharedModule(fileName) { + if (sharedModuleCache.has(fileName)) { + return sharedModuleCache.get(fileName); + } + + const filePath = path.join(SHARED_SRC_ROOT, fileName); + const modulePromise = import(pathToFileURL(filePath).href); + sharedModuleCache.set(fileName, modulePromise); + return modulePromise; +} + +async function runNode(scriptRelativePath, workspace, core) { + const { execFile, isExecError } = await loadSharedModule("exec.js"); + + try { + const result = await execFile("node", [scriptRelativePath], { + cwd: workspace, + logger: core, + }); + + if (result.stdout) { + core.info(result.stdout.trimEnd()); + } + if (result.stderr) { + core.info(result.stderr.trimEnd()); + } + } catch (error) { + if (isExecError(error)) { + if (error.stdout) { + core.info(error.stdout.trimEnd()); + } + if (error.stderr) { + core.info(error.stderr.trimEnd()); + } + } + + const status = isExecError(error) && Number.isInteger(error.code) ? error.code : 1; + throw new Error(`Command failed (${status}): node ${scriptRelativePath}`); + } +} + +function readLines(fileRelativePath, workspace) { + const fullPath = path.join(workspace, fileRelativePath); + if (!fs.existsSync(fullPath)) { + return []; + } + + return fs + .readFileSync(fullPath, "utf-8") + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => Boolean(line)); +} + +function formatIssueSection(title, apiFiles) { + if (!apiFiles.length) { + return ""; + } + + const lines = [title]; + for (const apiFile of apiFiles) { + const packageDir = apiFile.replace(/\/(api\.md|api\.metadata\.yml)$/, ""); + const packageName = path.basename(packageDir); + lines.push(`- ${packageDir}`); + lines.push(` API file: ${apiFile}`); + lines.push(` Regenerate: azpysdk apistub --md --extract-metadata ${packageName} --dest-dir .`); + } + lines.push(""); + return lines.join("\n"); +} + +module.exports = async function apiMdConsistency({ core }) { + const workspace = process.env.GITHUB_WORKSPACE || process.cwd(); + + await runNode("scripts/api_md_workflow/find_affected.js", workspace, core); + + const affected = readLines(process.env.API_MD_PACKAGES_FILE, workspace); + const changedCount = affected.length; + core.setOutput("changed_count", String(changedCount)); + + if (changedCount === 0) { + core.setOutput("mismatch_count", "0"); + core.setOutput("missing_count", "0"); + core.setOutput("issue_count", "0"); + return { + changedCount, + mismatchCount: 0, + missingCount: 0, + issueCount: 0, + }; + } + + await runNode("scripts/api_md_workflow/regenerate.js", workspace, core); + await runNode("scripts/api_md_workflow/find_mismatches.js", workspace, core); + + const mismatches = readLines(process.env.API_MD_MISMATCHES_FILE, workspace); + const missing = readLines(process.env.API_MD_MISSING_FILE, workspace); + + const mismatchCount = mismatches.length; + const missingCount = missing.length; + const issueCount = mismatchCount + missingCount; + + core.setOutput("mismatch_count", String(mismatchCount)); + core.setOutput("missing_count", String(missingCount)); + core.setOutput("issue_count", String(issueCount)); + + if (issueCount > 0) { + const messageParts = [ + "Generated api.md or api.metadata.yml does not match the committed files, or required API files are missing, for one or more affected packages.", + "api.metadata.yml must be committed alongside api.md, and selected metadata fields are part of pass/fail gating.", + "", + formatIssueSection("Mismatched packages:", mismatches), + formatIssueSection("Missing required API files:", missing), + "To regenerate api.md locally, run the command shown for each package from the repository root.", + ].filter((part) => part !== ""); + + core.setFailed(messageParts.join("\n")); + } + + return { + changedCount, + mismatchCount, + missingCount, + issueCount, + }; +}; diff --git a/doc/eng_sys_checks.md b/doc/eng_sys_checks.md index 3515f24c3f8c..28839bf6d6af 100644 --- a/doc/eng_sys_checks.md +++ b/doc/eng_sys_checks.md @@ -177,8 +177,6 @@ analyze_python_version = "3.11" This setting is read by `eng/scripts/dispatch_checks.py` and is passed to `azpysdk` via the `--python` flag (which requires `--isolate` and `uv`). This is useful for packages that use newer syntax or type features that require a more recent Python interpreter. > **Note:** This setting only affects the Python interpreter version used for the analyze venv; it does not change the minimum supported Python version declared in `setup.py`/`pyproject.toml`. -> -> **Warning:** This override applies to _all_ analyze checks dispatched by `dispatch_checks.py`, including `apistub`. The `apistub` tool currently requires Python < 3.11 (`PYTHON_VERSION_LIMIT = (3, 11)` in `azpysdk/apistub.py`). Do not set `analyze_python_version` to `3.11` or higher for packages that still run `apistub` through the standard dispatched analyze flow. ## Environment variables important to CI diff --git a/eng/scripts/Extract-APIViewMetadata-Python.ps1 b/eng/scripts/Extract-APIViewMetadata-Python.ps1 new file mode 100644 index 000000000000..abf162e71f20 --- /dev/null +++ b/eng/scripts/Extract-APIViewMetadata-Python.ps1 @@ -0,0 +1,153 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. + +<# +.SYNOPSIS +Extracts Python APIView metadata from API markdown and writes api.metadata.yml. + +.DESCRIPTION +Reads an API markdown file, extracts parser and Python runtime versions from the +Python APIView metadata header, removes that header from the markdown, trims leading +blank lines from the markdown body, and writes api.metadata.yml beside the markdown file. + +.PARAMETER ApiMarkdownPath +Optional. Path to API markdown file. If omitted, api.md will be resolved from OutputPath. + +.PARAMETER OutputPath +Optional. Directory containing API markdown output. Defaults to current directory. + +.EXAMPLE +./Extract-APIViewMetadata-Python.ps1 -OutputPath ./sdk/template/azure-template + +.EXAMPLE +./Extract-APIViewMetadata-Python.ps1 -ApiMarkdownPath ./sdk/template/azure-template/api.md +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $false)] + [string]$ApiMarkdownPath, + + [Parameter(Mandatory = $false)] + [string]$OutputPath = "." +) + +Set-StrictMode -Version 3 +$ErrorActionPreference = 'Stop' + +function Resolve-ApiMarkdownPath { + param( + [string]$ProvidedPath, + [string]$OutputDirectory + ) + + if ($ProvidedPath) { + return $ProvidedPath + } + + $resolvedOutput = Resolve-Path -LiteralPath $OutputDirectory -ErrorAction Stop + $apiLower = Join-Path $resolvedOutput.Path "api.md" + if (Test-Path -LiteralPath $apiLower -PathType Leaf) { + return $apiLower + } + + throw "Could not find API markdown file in '$OutputDirectory'. Expected api.md." +} + +function Trim-LeadingBlankLines { + param([string[]]$Lines) + + $start = 0 + while ($start -lt $Lines.Count -and [string]::IsNullOrWhiteSpace($Lines[$start])) { + $start++ + } + + if ($start -eq 0) { + return $Lines + } + + if ($start -ge $Lines.Count) { + return @() + } + + return $Lines[$start..($Lines.Count - 1)] +} + +function Get-Sha256Hex { + param([string]$Text) + + $sha256 = [System.Security.Cryptography.SHA256]::Create() + try { + $bytes = [System.Text.Encoding]::UTF8.GetBytes($Text) + $hashBytes = $sha256.ComputeHash($bytes) + return ([System.BitConverter]::ToString($hashBytes)).Replace("-", "").ToLowerInvariant() + } + finally { + $sha256.Dispose() + } +} + +$resolvedApiPath = Resolve-ApiMarkdownPath -ProvidedPath $ApiMarkdownPath -OutputDirectory $OutputPath +if (-not (Test-Path -LiteralPath $resolvedApiPath -PathType Leaf)) { + throw "API markdown file not found: $resolvedApiPath" +} + +$metadataPattern = '^# Package is parsed using apiview-stub-generator\(version:([^\)]+)\), Python version:\s*([^\s]+)\s*$' + +$fileText = Get-Content -LiteralPath $resolvedApiPath -Raw +$lineEnding = if ($fileText -match "`r`n") { "`r`n" } else { "`n" } +$lines = $fileText -split '\r?\n' + +$metadata = [ordered]@{} +$filtered = [System.Collections.Generic.List[string]]::new() + +foreach ($line in $lines) { + $match = [regex]::Match($line, $metadataPattern) + if ($match.Success) { + # Alphabetical keys in output YAML. + $metadata['parserVersion'] = $match.Groups[1].Value + $metadata['pythonVersion'] = $match.Groups[2].Value + continue + } + + $filtered.Add($line) +} + +# Remove blank lines after opening fence so markdown body starts at namespace. +if ($filtered.Count -gt 0 -and $filtered[0].StartsWith('```')) { + $fence = $filtered[0] + $body = Trim-LeadingBlankLines -Lines @($filtered | Select-Object -Skip 1) + $rewritten = [System.Collections.Generic.List[string]]::new() + $rewritten.Add($fence) + foreach ($line in $body) { + $rewritten.Add($line) + } + $filtered = $rewritten +} +else { + $trimmed = Trim-LeadingBlankLines -Lines @($filtered) + $filtered = [System.Collections.Generic.List[string]]::new($trimmed) +} + +$normalizedLinesForHash = @($filtered | ForEach-Object { $_.TrimEnd() }) +$newlineForHash = [string][char]10 +$normalizedTextForHash = $normalizedLinesForHash -join $newlineForHash +$metadata['apiMdSha256'] = Get-Sha256Hex -Text $normalizedTextForHash + +Set-Content -LiteralPath $resolvedApiPath -Value ($filtered -join $lineEnding) -NoNewline -Encoding utf8 +Write-Host "Updated markdown: $resolvedApiPath" + +$metadataPath = Join-Path (Split-Path -Parent $resolvedApiPath) "api.metadata.yml" +if ($metadata.Count -gt 0) { + $yamlLines = [System.Collections.Generic.List[string]]::new() + foreach ($key in ($metadata.Keys | Sort-Object)) { + $yamlLines.Add(("{0}: {1}" -f $key, $metadata[$key])) + } + + Set-Content -LiteralPath $metadataPath -Value ($yamlLines -join $lineEnding) -Encoding utf8 + Write-Host "Generated metadata: $metadataPath" +} +elseif (Test-Path -LiteralPath $metadataPath) { + Remove-Item -LiteralPath $metadataPath -Force + Write-Host "Removed stale metadata: $metadataPath" +} diff --git a/eng/tools/azure-sdk-tools/azpysdk/apistub.py b/eng/tools/azure-sdk-tools/azpysdk/apistub.py index 4465562950c3..e734eba00d27 100644 --- a/eng/tools/azure-sdk-tools/azpysdk/apistub.py +++ b/eng/tools/azure-sdk-tools/azpysdk/apistub.py @@ -13,7 +13,6 @@ from ci_tools.parsing import ParsedSetup REPO_ROOT = discover_repo_root() -PYTHON_VERSION_LIMIT = (3, 11) # apistub doesn't support Python 3.11+ def get_package_wheel_path(pkg_root: str) -> str: @@ -70,16 +69,28 @@ def register( action="store_true", help="Generate api.md from the JSON token file using Export-APIViewMarkdown.ps1. Output directory for api.md is the same as the generated token file.", ) + p.add_argument( + "--extract-metadata", + dest="extract_metadata", + default=False, + action="store_true", + help="Extract language-specific metadata from generated api.md into api.metadata.yml and remove metadata header from api.md.", + ) + p.add_argument( + "--install-deps", + dest="install_deps", + default=False, + action="store_true", + help="Install dev requirements and apiview dependencies before running. Skipped by default for faster local iteration.", + ) p.set_defaults(func=self.run) def run(self, args: argparse.Namespace) -> int: """Run the apistub check command.""" logger.info("Running apistub check...") - if sys.version_info >= PYTHON_VERSION_LIMIT: - logger.error( - f"Python version {sys.version_info.major}.{sys.version_info.minor} is not supported. Version must be less than {PYTHON_VERSION_LIMIT[0]}.{PYTHON_VERSION_LIMIT[1]}." - ) + if getattr(args, "extract_metadata", False) and not getattr(args, "generate_md", False): + logger.error("--extract-metadata requires --md.") return 1 set_envvar_defaults() @@ -101,22 +112,23 @@ def run(self, args: argparse.Namespace) -> int: ) logger.info(f"Processing {package_name} for apistub check") - # install dependencies - self.install_dev_reqs(executable, args, package_dir) - - try: - install_into_venv( - executable, - [ - "-r", - os.path.join(REPO_ROOT, "eng", "apiview_reqs.txt"), - "--index-url=https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/", - ], - package_dir, - ) - except CalledProcessError as e: - logger.error(f"Failed to install dependencies: {e}") - return e.returncode + if getattr(args, "install_deps", False): + # install dependencies + self.install_dev_reqs(executable, args, package_dir) + + try: + install_into_venv( + executable, + [ + "-r", + os.path.join(REPO_ROOT, "eng", "apiview_reqs.txt"), + "--index-url=https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/", + ], + package_dir, + ) + except CalledProcessError as e: + logger.error(f"Failed to install dependencies: {e}") + return e.returncode if not os.getenv("PREBUILT_WHEEL_DIR"): create_package_and_install( @@ -131,14 +143,15 @@ def run(self, args: argparse.Namespace) -> int: python_executable=executable, ) - self.pip_freeze(executable) + if getattr(args, "install_deps", False): + self.pip_freeze(executable) pkg_path = get_package_wheel_path(package_dir) pkg_path = os.path.abspath(pkg_path) dest_dir = getattr(args, "dest_dir", None) if dest_dir: - out_token_path = os.path.join(os.path.abspath(dest_dir), package_name) + out_token_path = os.path.abspath(dest_dir) os.makedirs(out_token_path, exist_ok=True) else: out_token_path = os.path.abspath(staging_directory) @@ -164,6 +177,7 @@ def run(self, args: argparse.Namespace) -> int: if getattr(args, "generate_md", False): token_json_path = os.path.join(out_token_path, f"{package_name}_python.json") md_script = os.path.join(REPO_ROOT, "eng", "common", "scripts", "Export-APIViewMarkdown.ps1") + metadata_script = os.path.join(REPO_ROOT, "eng", "scripts", "Extract-APIViewMetadata-Python.ps1") logger.info(f"Generating api.md for {package_name}") try: result = run( @@ -175,11 +189,22 @@ def run(self, args: argparse.Namespace) -> int: # pwsh script logs the api.md location if result.stdout: logger.info(result.stdout) + + if getattr(args, "extract_metadata", False): + logger.info(f"Extracting API metadata for {package_name}") + metadata_result = run( + ["pwsh", metadata_script, "-OutputPath", out_token_path], + check=True, + capture_output=True, + text=True, + ) + if metadata_result.stdout: + logger.info(metadata_result.stdout) except FileNotFoundError: logger.error("Failed to generate api.md: pwsh (PowerShell) is not installed or not on PATH.") results.append(1) except CalledProcessError as e: - logger.error(f"Failed to generate api.md (exit code {e.returncode}):") + logger.error(f"Failed to generate api.md or extract metadata (exit code {e.returncode}):") if e.stderr: logger.error(e.stderr) if e.stdout: diff --git a/eng/tools/azure-sdk-tools/tests/test_apistub.py b/eng/tools/azure-sdk-tools/tests/test_apistub.py index 85a60a407794..ff7ad42e5cd0 100644 --- a/eng/tools/azure-sdk-tools/tests/test_apistub.py +++ b/eng/tools/azure-sdk-tools/tests/test_apistub.py @@ -86,16 +86,15 @@ def _make_args(self, dest_dir=None, generate_md=False): @patch( "azpysdk.apistub.REPO_ROOT", os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) ) - @patch("azpysdk.apistub.PYTHON_VERSION_LIMIT", (99, 99)) @patch("azpysdk.apistub.get_cross_language_mapping_path", return_value=None) @patch("azpysdk.apistub.get_package_wheel_path", return_value="/fake/pkg.whl") @patch("azpysdk.apistub.create_package_and_install") @patch("azpysdk.apistub.install_into_venv") @patch("azpysdk.apistub.set_envvar_defaults") - def test_dest_dir_creates_package_subfolder( + def test_dest_dir_uses_destination_directory( self, _env, _install, _create, _get_whl, _get_mapping, tmp_path, monkeypatch ): - """When --dest-dir is given, output should go to //.""" + """When --dest-dir is given, output should go directly to /.""" monkeypatch.chdir(os.getcwd()) dest = tmp_path / "output" dest.mkdir() @@ -131,7 +130,7 @@ def fake_pwsh(cmd, **kwargs): stub.run(self._make_args(dest_dir=str(dest), generate_md=True)) - expected_out = os.path.join(str(dest), "azure-core") + expected_out = str(dest) assert os.path.isdir(expected_out) assert os.path.exists(os.path.join(expected_out, "api.md")) assert os.path.exists(os.path.join(expected_out, "azure-core_python.json")) @@ -139,7 +138,6 @@ def fake_pwsh(cmd, **kwargs): @patch( "azpysdk.apistub.REPO_ROOT", os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) ) - @patch("azpysdk.apistub.PYTHON_VERSION_LIMIT", (99, 99)) @patch("azpysdk.apistub.get_cross_language_mapping_path", return_value=None) @patch("azpysdk.apistub.get_package_wheel_path", return_value="/fake/pkg.whl") @patch("azpysdk.apistub.create_package_and_install") @@ -191,7 +189,6 @@ def fake_pwsh(cmd, **kwargs): @patch( "azpysdk.apistub.REPO_ROOT", os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) ) - @patch("azpysdk.apistub.PYTHON_VERSION_LIMIT", (99, 99)) @patch("azpysdk.apistub.get_cross_language_mapping_path", return_value=None) @patch("azpysdk.apistub.get_package_wheel_path", return_value="/fake/pkg.whl") @patch("azpysdk.apistub.create_package_and_install") @@ -235,7 +232,6 @@ def fake_pwsh(cmd, **kwargs): @patch( "azpysdk.apistub.REPO_ROOT", os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) ) - @patch("azpysdk.apistub.PYTHON_VERSION_LIMIT", (99, 99)) @patch("azpysdk.apistub.get_cross_language_mapping_path", return_value=None) @patch("azpysdk.apistub.get_package_wheel_path", return_value="/fake/pkg.whl") @patch("azpysdk.apistub.create_package_and_install") diff --git a/scripts/api_md_workflow/README.md b/scripts/api_md_workflow/README.md new file mode 100644 index 000000000000..1bda32ffceb5 --- /dev/null +++ b/scripts/api_md_workflow/README.md @@ -0,0 +1,111 @@ +# api.md Workflow Helpers + +This folder contains the helper scripts used by the GitHub Actions workflows that validate and update `api.md` files for changed SDK packages. + +## Purpose + +The workflow validates that when a pull request changes one or more SDK packages, the committed `api.md` files are still up to date. +`api.md` content is diff-gated by this workflow, and `api.metadata.yml` must be committed alongside it. +The workflow also validates adapter-selected metadata fields, such as `apiMdSha256` and `parserVersion`, so metadata differences can affect pass/fail. + +The logic is split between GitHub workflow YAML files and helper scripts in Python and JavaScript. + +## Workflow Files + +### `.github/workflows/api-consistency.yml` + +This is the main workflow. + +It runs on pull requests for changes under `sdk/**`. + +- Detects affected package directories from the PR diff. +- Regenerates `api.md` for those packages. +- Fails if the generated files differ from the committed files. +- Fails if an affected package does not have a committed `api.md`. +- Prints the mismatched or missing packages and the `azpysdk apistub --md --extract-metadata --dest-dir .` command needed to regenerate each `api.md` file from the repository root. + +## Script Layout + +### `common.js` + +Shared helpers used by the other scripts: + +- repository root resolution +- subprocess execution +- reading/writing line-based artifact files +- writing GitHub Actions outputs +- GitHub REST API helpers for listing/updating comments + +### `find_affected.js` + +Used by the `consistency` job. + +Reads `API_MD_BASE_REF`, compares the PR branch to `origin/`, and writes: + +- changed package directories to `API_MD_CHANGED_FILE` +- valid package roots to `API_MD_PACKAGES_FILE` + +Also writes `count=` to `GITHUB_OUTPUT`. + +### `regenerate.js` + +Reads package directories from `API_MD_PACKAGES_FILE` and runs `azpysdk apistub --md --extract-metadata --dest-dir ` for each package from the repository root. + +This script is used by the consistency check. + +### `find_mismatches.js` + +Reads package directories from `API_MD_PACKAGES_FILE`, checks whether `/api.md` is missing/untracked or differs from git, and writes: + +- mismatched files to `API_MD_MISMATCHES_FILE` +- missing files to `API_MD_MISSING_FILE` + +Also writes `mismatch_count=`, `missing_count=`, and `issue_count=` to `GITHUB_OUTPUT`. + +`api.metadata.yml` is also required and selected metadata fields are mismatch-checked according to the active adapter. + +### `create_api_review_pr.js` and adapters + +API review PR creation now uses a shared JavaScript orchestrator with a language adapter boundary: + +- `create_api_review_pr.js`: shared git/branch/PR orchestration logic. +- `adapters/python.js`: Python-specific package discovery, version parsing, and `api.md` generation. + +This split allows the core workflow to be reused across other language repos while keeping generation behavior language-specific. + +`create_api_review_pr.js` compares a baseline package release tag with a target API surface. The target can be a package release tag, an `origin` branch, or an `owner:branch` fork reference. When the target is a tag, the generated PR body identifies it as a target tag instead of a working branch. + +Example comparing two package release tags: + +```bash +node scripts/api_md_workflow/create_api_review_pr.js --package-name azure-ai-projects --base azure-ai-projects_2.1.0 --target azure-ai-projects_2.2.0 +``` + +### `api_md_workflow.config.json` + +Shared configuration for adapter selection across `api_md_workflow` scripts. + +- `adapter`: default adapter name (for this repo: `python`) + +Both `create_api_review_pr.js` and `find_affected.js` read this file for adapter selection. + +## Environment Variables Used + +The scripts are intentionally simple and read inputs from environment variables set by the workflow steps. + +Common variables include: + +- `API_MD_BASE_REF` +- `API_MD_PACKAGES_FILE` +- `API_MD_CHANGED_FILE` +- `API_MD_MISMATCHES_FILE` +- `API_MD_MISSING_FILE` + +## End-to-End Flow + +1. A PR changes files under `sdk/**`. +2. `consistency.yml` runs. +3. `find_affected.js` determines which packages were touched. +4. `regenerate.js` rebuilds `api.md` for those packages. +5. `find_mismatches.js` records any `api.md` drift, including missing or untracked `api.md` files. +6. If drift is found, the workflow fails and prints the affected packages plus the `azpysdk apistub --md --extract-metadata --dest-dir .` command to regenerate each `api.md` file locally from the repository root. diff --git a/scripts/api_md_workflow/adapter_config.js b/scripts/api_md_workflow/adapter_config.js new file mode 100644 index 000000000000..660cc9462419 --- /dev/null +++ b/scripts/api_md_workflow/adapter_config.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); + +const DEFAULT_CONFIG = { + adapter: "python", +}; + +function loadWorkflowConfig() { + const configPath = path.join(__dirname, "api_md_workflow.config.json"); + if (!fs.existsSync(configPath)) { + return { ...DEFAULT_CONFIG }; + } + + const raw = fs.readFileSync(configPath, "utf-8"); + let parsed; + try { + parsed = JSON.parse(raw); + } catch (error) { + throw new Error( + `ERROR: invalid JSON in ${configPath}: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + if (!parsed || typeof parsed !== "object") { + throw new Error(`ERROR: ${configPath} must contain a JSON object.`); + } + + return { + ...DEFAULT_CONFIG, + ...parsed, + }; +} + +function loadAdapter(name) { + const adapterPath = path.join(__dirname, "adapters", `${name}.js`); + if (!fs.existsSync(adapterPath)) { + throw new Error(`ERROR: adapter '${name}' not found at ${adapterPath}`); + } + + // eslint-disable-next-line global-require, import/no-dynamic-require + return require(adapterPath); +} + +module.exports = { + loadWorkflowConfig, + loadAdapter, +}; diff --git a/scripts/api_md_workflow/adapters/python.js b/scripts/api_md_workflow/adapters/python.js new file mode 100644 index 000000000000..e407211c43dd --- /dev/null +++ b/scripts/api_md_workflow/adapters/python.js @@ -0,0 +1,163 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); +const { spawnSync } = require("child_process"); + +function run(cmd, args, options = {}) { + const logger = options.logger || console; + const printable = [cmd, ...args].join(" "); + logger.info(`$ ${printable}`); + const result = spawnSync(cmd, args, { + cwd: options.cwd, + env: options.env, + encoding: "utf-8", + stdio: options.capture ? "pipe" : "inherit", + shell: options.shell ?? false, + }); + + if (result.error) { + const errorMessage = result.error instanceof Error ? result.error.message : String(result.error); + throw new Error(`Command failed to start: ${printable}\n${errorMessage}`); + } + + if ((options.check ?? true) && result.status !== 0) { + throw new Error(`Command failed (${result.status}): ${printable}`); + } + + return result; +} + +function findPackageDir(repoRoot, packageName) { + const sdkDir = path.join(repoRoot, "sdk"); + const serviceDirs = fs.readdirSync(sdkDir, { withFileTypes: true }); + const matches = []; + + for (const serviceDir of serviceDirs) { + if (!serviceDir.isDirectory()) { + continue; + } + + const candidate = path.join(sdkDir, serviceDir.name, packageName); + if (!fs.existsSync(candidate) || !fs.statSync(candidate).isDirectory()) { + continue; + } + + const hasBuildFile = fs.existsSync(path.join(candidate, "pyproject.toml")) || fs.existsSync(path.join(candidate, "setup.py")); + if (hasBuildFile) { + matches.push(candidate); + } + } + + if (matches.length === 0) { + throw new Error(`ERROR: package '${packageName}' not found under sdk/*/`); + } + + if (matches.length > 1) { + throw new Error(`ERROR: multiple matches for '${packageName}': ${matches.join(", ")}`); + } + + return matches[0]; +} + +function isPackageDir(repoRoot, packageDirRelative) { + const candidate = path.join(repoRoot, packageDirRelative); + if (!fs.existsSync(candidate) || !fs.statSync(candidate).isDirectory()) { + return false; + } + + return fs.existsSync(path.join(candidate, "pyproject.toml")) || fs.existsSync(path.join(candidate, "setup.py")); +} + +function* walkFiles(startDir) { + const entries = fs.readdirSync(startDir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(startDir, entry.name); + if (entry.isDirectory()) { + yield* walkFiles(fullPath); + } else { + yield fullPath; + } + } +} + +function readVersion(packageDir) { + const versionRegex = /^\s*VERSION\s*[:=]\s*["']([^"']+)["']/m; + const candidates = []; + + for (const file of walkFiles(packageDir)) { + const name = path.basename(file); + if (name === "_version.py" || name === "version.py") { + // Skip generated code directories — they often contain stale versions + const relative = path.relative(packageDir, file); + if (relative.includes("_generated") || relative.includes("generated_")) { + continue; + } + candidates.push(file); + } + } + + for (const candidate of candidates) { + let text; + try { + text = fs.readFileSync(candidate, "utf-8"); + } catch { + continue; + } + + const match = text.match(versionRegex); + if (match) { + return match[1]; + } + } + + throw new Error(`ERROR: could not find a version string in ${packageDir}`); +} + +function generateApiForPackage({ + repoRoot, + packageName, + runtimeExecutable, + logger, + refLabel, +}) { + const activeLogger = logger || console; + if (refLabel) { + activeLogger.info(`--- Generating api.md on ${refLabel} ---`); + } + + const packageDir = findPackageDir(repoRoot, packageName); + if (runtimeExecutable || process.env.RUNTIME_EXECUTABLE) { + const pythonExecutable = runtimeExecutable || process.env.RUNTIME_EXECUTABLE; + run( + pythonExecutable, + ["-m", "azpysdk.main", "apistub", "--md", "--extract-metadata", "--dest-dir", packageDir, packageName], + { + cwd: repoRoot, + check: true, + logger: activeLogger, + }, + ); + return; + } + + run("azpysdk", ["apistub", "--md", "--extract-metadata", "--dest-dir", packageDir, packageName], { + cwd: repoRoot, + check: true, + logger: activeLogger, + shell: process.platform === "win32", + }); +} + +// Fields in api.metadata.yml that must match between working tree and committed version. +// pythonVersion is excluded because it varies across CI environments. +const metadataFieldsToValidate = ["apiMdSha256", "parserVersion"]; + +module.exports = { + name: "python", + isPackageDir, + findPackageDir, + readVersion, + generateApiForPackage, + metadataFieldsToValidate, +}; diff --git a/scripts/api_md_workflow/api_md_workflow.config.json b/scripts/api_md_workflow/api_md_workflow.config.json new file mode 100644 index 000000000000..34b0ae2b8ce7 --- /dev/null +++ b/scripts/api_md_workflow/api_md_workflow.config.json @@ -0,0 +1,3 @@ +{ + "adapter": "python" +} \ No newline at end of file diff --git a/scripts/api_md_workflow/common.js b/scripts/api_md_workflow/common.js new file mode 100644 index 000000000000..8a97c7c50749 --- /dev/null +++ b/scripts/api_md_workflow/common.js @@ -0,0 +1,113 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const REPO_ROOT = path.resolve(__dirname, "..", ".."); +const SHARED_SRC_ROOT = path.join(REPO_ROOT, ".github", "shared", "src"); +const sharedModuleCache = new Map(); + +async function loadSharedModule(fileName) { + if (sharedModuleCache.has(fileName)) { + return sharedModuleCache.get(fileName); + } + + const filePath = path.join(SHARED_SRC_ROOT, fileName); + const modulePromise = import(pathToFileURL(filePath).href); + sharedModuleCache.set(fileName, modulePromise); + return modulePromise; +} + +async function getDefaultLogger() { + const { defaultLogger } = await loadSharedModule("logger.js"); + return defaultLogger; +} + +async function runAsync(cmd, args, options = {}) { + const { execFile, isExecError } = await loadSharedModule("exec.js"); + const check = options.check ?? true; + const logger = options.logger ?? (await getDefaultLogger()); + + try { + const result = await execFile(cmd, args, { + cwd: options.cwd, + logger, + maxBuffer: options.maxBuffer, + }); + + return { + status: 0, + stdout: result.stdout ?? "", + stderr: result.stderr ?? "", + }; + } catch (error) { + if (!isExecError(error)) { + throw error; + } + + const status = Number.isInteger(error.code) ? error.code : 1; + const stdout = error.stdout ?? ""; + const stderr = error.stderr ?? ""; + + if (!check) { + return { status, stdout, stderr }; + } + + throw new Error(`Command failed (${status}): ${[cmd, ...args].join(" ")}`); + } +} + +function readLines(filePath) { + if (!fs.existsSync(filePath)) { + return []; + } + + return fs + .readFileSync(filePath, "utf-8") + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => Boolean(line)); +} + +function writeLines(filePath, lines) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + if (!lines.length) { + fs.writeFileSync(filePath, "", "utf-8"); + return; + } + fs.writeFileSync(filePath, `${lines.join("\n")}\n`, "utf-8"); +} + +function appendGithubOutput(key, value) { + const outputPath = process.env.GITHUB_OUTPUT; + if (!outputPath) { + return; + } + + fs.appendFileSync(outputPath, `${key}=${value}\n`, "utf-8"); +} + +function envPath(name, fallback) { + return process.env[name] || fallback; +} + +function requireEnv(name) { + const value = process.env[name]; + if (!value) { + throw new Error(`Environment variable ${name} is required`); + } + return value; +} + +module.exports = { + REPO_ROOT, + loadSharedModule, + getDefaultLogger, + runAsync, + readLines, + writeLines, + appendGithubOutput, + envPath, + requireEnv, +}; diff --git a/scripts/api_md_workflow/create_api_review_pr.js b/scripts/api_md_workflow/create_api_review_pr.js new file mode 100644 index 000000000000..f4bf3a236a23 --- /dev/null +++ b/scripts/api_md_workflow/create_api_review_pr.js @@ -0,0 +1,1201 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); +const { spawnSync } = require("child_process"); +const { getDefaultLogger } = require("./common"); +const { loadAdapter, loadWorkflowConfig } = require("./adapter_config"); + +const REPO_ROOT = path.resolve(__dirname, "..", ".."); +const REMOTE = "origin"; +const MAIN_REF = `${REMOTE}/main`; +const SYNC_METADATA_MARKER = "api-md-review-sync"; +const SYNC_METADATA_WARNING = "DO NOT MODIFY THESE CONTENTS!"; +let logger = console; + +function logInfo(message) { + logger.info(message); +} + +function logWarning(message) { + if (typeof logger.warning === "function") { + logger.warning(message); + return; + } + logger.warn(message); +} + +function logError(message) { + logger.error(message); +} + +function parseArgs(argv) { + const config = loadWorkflowConfig(); + const args = { + packageName: null, + base: null, + target: null, + adapter: config.adapter, + runtimeExecutable: process.env.RUNTIME_EXECUTABLE || null, + }; + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + if (!arg.startsWith("--")) { + throw new Error(`Unexpected argument: ${arg}`); + } + + const key = arg.slice(2); + const value = argv[i + 1]; + if (!value || value.startsWith("--")) { + throw new Error(`Missing value for --${key}`); + } + + i += 1; + if (key === "package-name") { + args.packageName = value; + } else if (key === "base") { + args.base = value; + } else if (key === "target") { + args.target = value; + } else if (key === "adapter") { + args.adapter = value; + } else if (key === "python" || key === "runtime") { + args.runtimeExecutable = value; + } else { + throw new Error(`Unknown option: --${key}`); + } + } + + if (!args.packageName) { + throw new Error("Missing required --package-name"); + } + + if (!args.base) { + throw new Error("Missing required --base"); + } + + return args; +} + +function run(cmd, args, options = {}) { + const printable = [cmd, ...args].join(" "); + logInfo(`$ ${printable}`); + const result = spawnSync(cmd, args, { + cwd: options.cwd ?? REPO_ROOT, + env: options.env, + encoding: "utf-8", + stdio: options.capture ? "pipe" : "inherit", + shell: false, + }); + + if (result.error) { + const errorMessage = result.error instanceof Error ? result.error.message : String(result.error); + throw new Error(`Command failed to start: ${printable}\n${errorMessage}`); + } + + if ((options.check ?? true) && result.status !== 0) { + throw new Error(`Command failed (${result.status}): ${printable}`); + } + + return result; +} + +let cachedGitExecutable = undefined; +let cachedGitAwareEnv = undefined; + +function resolveGitExecutable() { + if (cachedGitExecutable !== undefined) { + return cachedGitExecutable; + } + + if (process.platform !== "win32") { + const resolved = spawnSync("git", ["--exec-path"], { + cwd: REPO_ROOT, + encoding: "utf-8", + env: process.env, + }); + cachedGitExecutable = resolved.status === 0 ? "git" : "git"; + return cachedGitExecutable; + } + + cachedGitExecutable = findPreferredGitExecutable() || "git"; + return cachedGitExecutable; +} + +let git = function gitCommand(args, options = {}) { + return run(resolveGitExecutable(), args, { + ...options, + env: buildGitAwareEnv(options.env), + }); +}; + +let gh = function ghCommand(args, options = {}) { + return run("gh", args, { + ...options, + env: buildGitAwareEnv(options.env), + }); +}; + +function gitOut(args) { + return git(args, { capture: true }).stdout.trim(); +} + +function ensureCleanWorktree() { + const status = gitOut(["status", "--porcelain"]); + if (status) { + throw new Error(`ERROR: working tree is not clean. Commit or stash changes before running.\n${status}`); + } +} + +function currentBranch() { + return gitOut(["rev-parse", "--abbrev-ref", "HEAD"]); +} + +function currentBranchOrSha() { + const name = currentBranch(); + if (name === "HEAD") { + return gitOut(["rev-parse", "--short", "HEAD"]); + } + return name; +} + +function tagExists(tag) { + const result = git(["rev-parse", "--verify", "--quiet", `refs/tags/${tag}`], { + capture: true, + check: false, + }); + return result.status === 0; +} + +function validateBaseTag(packageName, baseTag) { + if (!baseTag.startsWith(`${packageName}_`)) { + throw new Error(`ERROR: --base tag '${baseTag}' must start with '${packageName}_'.`); + } + + const version = baseTag.slice(packageName.length + 1); + if (!version) { + throw new Error(`ERROR: --base tag '${baseTag}' is missing the version suffix.`); + } + + if (!tagExists(baseTag)) { + throw new Error(`ERROR: tag '${baseTag}' does not exist in this repository.`); + } + + return version; +} + +function resolveTargetTag(target) { + if (tagExists(target)) { + return target; + } + + git(["fetch", REMOTE, "tag", target], { check: false, capture: true }); + return tagExists(target) ? target : null; +} + +function remoteBranchRef(branch) { + git(["fetch", REMOTE, branch]); + return `${REMOTE}/${branch}`; +} + +function resolveTargetRef(target) { + const targetTag = resolveTargetTag(target); + if (targetTag) { + return targetTag; + } + + if (!target.includes(":")) { + return remoteBranchRef(target); + } + + const [owner, branch] = target.split(":", 2); + if (!owner || !branch) { + throw new Error(`ERROR: invalid --target '${target}'. Expected 'tag', 'branch', or 'owner:branch'.`); + } + + const forkUrl = `https://github.com/${owner}/azure-sdk-for-python.git`; + git(["fetch", forkUrl, branch]); + return "FETCH_HEAD"; +} + +function packageRelDir(packageDir) { + return path.relative(REPO_ROOT, packageDir).split(path.sep).join("/"); +} + +function normalizePackageDir(packageDir) { + if (path.isAbsolute(packageDir)) { + return packageRelDir(packageDir); + } + + return packageDir.split(path.sep).join("/"); +} + +function apiMdPath(packageDir) { + return path.join(packageDir, "api.md"); +} + +function apiMdRel(packageDir) { + return `${packageRelDir(packageDir)}/api.md`; +} + +function metadataPath(packageDir) { + return path.join(packageDir, "api.metadata.yml"); +} + +function metadataRel(packageDir) { + return `${packageRelDir(packageDir)}/api.metadata.yml`; +} + +function apiReviewBranchName(kind, packageName, version) { + return `apireview/${kind}_${packageName}_${version}`; +} + +function scoreGitCandidate(candidate) { + const normalized = candidate.replace(/\//g, "\\").toLowerCase(); + if (normalized.includes("\\program files\\git\\cmd\\git.exe")) { + return 0; + } + + if (normalized.includes("\\program files\\git\\bin\\git.exe")) { + return 1; + } + + if (normalized.includes("\\git\\cmd\\git.exe")) { + return 2; + } + + if (normalized.includes("\\git\\bin\\git.exe")) { + return 3; + } + + if (normalized.includes("\\windows\\")) { + return 100; + } + + return 10; +} + +function findPreferredGitExecutable() { + if (process.platform !== "win32") { + return null; + } + + const candidates = new Set(); + const roots = [process.env.ProgramW6432, process.env.ProgramFiles, process.env["ProgramFiles(x86)"], process.env.LocalAppData]; + for (const root of roots) { + if (!root) { + continue; + } + + candidates.add(path.join(root, "Git", "cmd", "git.exe")); + candidates.add(path.join(root, "Git", "bin", "git.exe")); + candidates.add(path.join(root, "Programs", "Git", "cmd", "git.exe")); + candidates.add(path.join(root, "Programs", "Git", "bin", "git.exe")); + } + + const pathKey = getPathEnvKey(process.env); + for (const rawEntry of (process.env[pathKey] || "").split(path.delimiter)) { + const entry = rawEntry.replace(/^"|"$/g, ""); + if (!entry) { + continue; + } + + candidates.add(path.join(entry, "git.exe")); + } + + const existing = [...candidates].filter((candidate) => fs.existsSync(candidate)); + existing.sort((left, right) => scoreGitCandidate(left) - scoreGitCandidate(right) || left.localeCompare(right)); + return existing[0] || null; +} + +function getGitExecPath(gitExecutable) { + if (process.platform === "win32" && !path.isAbsolute(gitExecutable)) { + return null; + } + + const result = spawnSync(gitExecutable, ["--exec-path"], { + cwd: REPO_ROOT, + encoding: "utf-8", + }); + + if (result.status !== 0) { + return null; + } + + return result.stdout.trim() || null; +} + +function samePathEntry(left, right) { + if (process.platform === "win32") { + return left.replace(/\\+$/, "").toLowerCase() === right.replace(/\\+$/, "").toLowerCase(); + } + + return left === right; +} + +function getPathEnvKey(env) { + return Object.keys(env).find((key) => key.toLowerCase() === "path") || "PATH"; +} + +function buildGitAwareEnv(baseEnv = process.env) { + if (baseEnv === process.env && cachedGitAwareEnv) { + return cachedGitAwareEnv; + } + + const env = { ...baseEnv }; + const pathKey = getPathEnvKey(env); + const gitExecutable = resolveGitExecutable(); + const gitExecPath = getGitExecPath(gitExecutable); + + if (path.isAbsolute(gitExecutable)) { + const gitDir = path.dirname(gitExecutable); + const currentEntries = (env[pathKey] || "").split(path.delimiter).filter(Boolean); + const first = currentEntries[0] || ""; + if (!first || !samePathEntry(first, gitDir)) { + env[pathKey] = [gitDir, ...currentEntries].join(path.delimiter); + logInfo(`(using resolved git executable: ${gitExecutable})`); + } + } + + if (gitExecPath) { + env.GIT_EXEC_PATH = gitExecPath; + } + + if (baseEnv === process.env) { + cachedGitAwareEnv = env; + } + + return env; +} + +function parseJsonOrNull(text) { + try { + const value = JSON.parse(text || "[]"); + return Array.isArray(value) ? value : null; + } catch { + return null; + } +} + +function parseJsonObjectOrNull(text) { + try { + const value = JSON.parse(text || "null"); + return value && typeof value === "object" && !Array.isArray(value) ? value : null; + } catch { + return null; + } +} + +function parseSimpleYaml(text) { + const result = {}; + for (const line of text.split(/\r?\n/)) { + const match = line.match(/^(\w+)\s*:\s*(.*)$/); + if (match) { + result[match[1]] = match[2].trim(); + } + } + return result; +} + +function metadataShaOrNull(metadataBytes) { + if (!metadataBytes) { + return null; + } + + const metadata = parseSimpleYaml(metadataBytes.toString("utf-8")); + return metadata.apiMdSha256 || null; +} + +function branchRemoteRef(branch) { + return `${REMOTE}/${branch}`; +} + +function listRemoteBranchesWithPrefix(prefix) { + const result = git(["ls-remote", "--heads", REMOTE, `refs/heads/${prefix}*`], { + capture: true, + check: false, + }); + + if (result.status !== 0 || !result.stdout.trim()) { + return []; + } + + return result.stdout + .split(/\r?\n/) + .map((line) => line.trim().split(/\s+/, 2)[1] || "") + .filter((ref) => ref.startsWith("refs/heads/")) + .map((ref) => ref.slice("refs/heads/".length)) + .filter((branch) => branch === prefix || branch.startsWith(`${prefix}_`)); +} + +function fetchRemoteBranch(branch) { + git(["fetch", REMOTE, branch]); + return branchRemoteRef(branch); +} + +function readRefFileBytes(ref, relativePath) { + const result = git(["show", `${ref}:${relativePath}`], { + capture: true, + check: false, + }); + + if (result.status !== 0) { + return null; + } + + return Buffer.from(result.stdout, "utf-8"); +} + +function desiredBranchState(result) { + if (result === null) { + return { + hasApiMd: false, + hasMetadata: false, + apiMdSha256: null, + }; + } + + return { + hasApiMd: true, + hasMetadata: Boolean(result.metadata), + apiMdSha256: metadataShaOrNull(result.metadata), + }; +} + +function apiResultsHaveApiDiff(baseResult, targetResult) { + return !Buffer.from(baseResult.apiMd).equals(Buffer.from(targetResult.apiMd)); +} + +function branchStateMatchesDesired(actual, desired) { + return ( + actual.hasApiMd === desired.hasApiMd && + actual.hasMetadata === desired.hasMetadata && + actual.apiMdSha256 === desired.apiMdSha256 + ); +} + +function readBranchState(ref, apiRelative, metaRelative) { + const metadataBytes = readRefFileBytes(ref, metaRelative); + const apiMdBytes = readRefFileBytes(ref, apiRelative); + + return { + hasApiMd: Boolean(apiMdBytes), + hasMetadata: Boolean(metadataBytes), + apiMdSha256: metadataShaOrNull(metadataBytes), + }; +} + +function branchSuffixFromIndex(index) { + let value = index; + let suffix = ""; + + do { + suffix = String.fromCharCode(97 + (value % 26)) + suffix; + value = Math.floor(value / 26) - 1; + } while (value >= 0); + + return suffix; +} + +function compareBranchCandidates(left, right, preferredBranch) { + if (left === preferredBranch && right !== preferredBranch) { + return -1; + } + + if (right === preferredBranch && left !== preferredBranch) { + return 1; + } + + return left.localeCompare(right); +} + +function nextAvailableBranchName(preferredBranch, existingBranches) { + if (!existingBranches.has(preferredBranch)) { + return preferredBranch; + } + + let index = 0; + while (existingBranches.has(`${preferredBranch}_${branchSuffixFromIndex(index)}`)) { + index += 1; + } + + return `${preferredBranch}_${branchSuffixFromIndex(index)}`; +} + +function isAncestorRef(ancestorRef, branchRef) { + const result = git(["merge-base", "--is-ancestor", ancestorRef, branchRef], { + capture: true, + check: false, + }); + return result.status === 0; +} + +function resolveBranchSelection({ preferredBranch, desiredState, apiRelative, metaRelative, requiredAncestorRef = null }) { + const existingBranches = new Set(listRemoteBranchesWithPrefix(preferredBranch)); + const orderedCandidates = [...existingBranches].sort((left, right) => + compareBranchCandidates(left, right, preferredBranch), + ); + + for (const candidateBranch of orderedCandidates) { + const remoteRef = fetchRemoteBranch(candidateBranch); + const actualState = readBranchState(remoteRef, apiRelative, metaRelative); + if (!branchStateMatchesDesired(actualState, desiredState)) { + continue; + } + + if (requiredAncestorRef && !isAncestorRef(requiredAncestorRef, remoteRef)) { + continue; + } + + return { + branchName: candidateBranch, + reused: true, + remoteRef, + }; + } + + return { + branchName: nextAvailableBranchName(preferredBranch, existingBranches), + reused: false, + remoteRef: null, + }; +} + +function ensureBranchStateHasMetadataSha(branchLabel, state) { + if (state.hasApiMd && !state.apiMdSha256) { + throw new Error(`ERROR: ${branchLabel} is missing apiMdSha256 in api.metadata.yml.`); + } +} + +function selectBestPr(prs) { + const candidates = prs.filter((pr) => + pr && typeof pr === "object" && "number" in pr && "url" in pr && "state" in pr && "updatedAt" in pr, + ); + if (candidates.length === 0) { + return null; + } + + const openPrs = candidates.filter((pr) => String(pr.state || "").toLowerCase() === "open"); + const pool = openPrs.length ? openPrs : candidates; + pool.sort((a, b) => String(b.updatedAt || "").localeCompare(String(a.updatedAt || ""))); + return pool[0]; +} + +function branchReferenceParts(headSelector) { + if (headSelector === MAIN_REF) { + return { + owner: "Azure", + branch: "main", + display: headSelector, + }; + } + + if (headSelector.includes(":")) { + const [owner, branch] = headSelector.split(":", 2); + return { + owner, + branch, + display: headSelector, + }; + } + + return { + owner: "Azure", + branch: headSelector, + display: headSelector, + }; +} + +function syncWorkingBranchInfo(headSelector) { + if (!headSelector) { + return null; + } + + const targetTag = resolveTargetTag(headSelector); + if (targetTag) { + return null; + } + + const { owner, branch } = branchReferenceParts(headSelector); + return { owner, branch }; +} + +function buildSyncMetadataObject({ packageName, packageDir, baseBranch, reviewBranch, headSelector }) { + const workingBranch = syncWorkingBranchInfo(headSelector); + if (!workingBranch) { + return null; + } + + const metadata = { + schemaVersion: 1, + repository: "Azure/azure-sdk-for-python", + packageName, + packageDir: normalizePackageDir(packageDir), + baseBranch, + reviewBranch, + workingOwner: workingBranch.owner, + workingBranch: workingBranch.branch, + }; + + const workingPr = findOpenPrForHead(headSelector); + metadata.workingPrNumber = workingPr && Number.isInteger(workingPr.number) ? workingPr.number : null; + + return metadata; +} + +function buildSyncMetadataBlock(metadata) { + if (!metadata) { + return null; + } + + return [ + `", + ].join("\n"); +} + +function replaceSyncMetadataBlock(body, metadataBlock) { + const cleanedBody = String(body || "") + .replace(new RegExp(`\\s*`, "g"), "") + .trimEnd(); + + if (!metadataBlock) { + return cleanedBody; + } + + return `${cleanedBody}\n\n${metadataBlock}`; +} + +function buildReviewPrBody({ packageName, targetVersion, baseVersion, workingReference, baselineRef, syncMetadataBlock }) { + const lines = [ + `Automated API review PR for ${packageName}.`, + "", + `- **${workingReference.label}:** ${workingReference.markdown} (version ${targetVersion})`, + `- **Baseline:** ${baselineRef} (version ${baseVersion})`, + ]; + + if (workingReference.label === "Target tag") { + lines.push("- **Update behavior:** Static tag-to-tag review; this PR cannot be automatically updated from a working branch."); + } + + lines.push("", "Generated by scripts/api_md_workflow/create_api_review_pr.js."); + + return replaceSyncMetadataBlock(lines.join("\n"), syncMetadataBlock); +} + +function updatePrBody(prNumber, body) { + return gh( + [ + "api", + `repos/Azure/azure-sdk-for-python/pulls/${prNumber}`, + "--method", + "PATCH", + "--field", + `body=${body}`, + ], + { check: false, capture: true }, + ); +} + +function ensurePrBodySyncMetadata(pr, metadataBlock) { + if (!metadataBlock || !pr || !Number.isInteger(pr.number)) { + return; + } + + const desiredBody = replaceSyncMetadataBlock(pr.body || "", metadataBlock); + if (desiredBody === (pr.body || "")) { + return; + } + + const result = updatePrBody(pr.number, desiredBody); + if (result.status === 0) { + logInfo(`Updated API review sync metadata on PR #${pr.number}.`); + return; + } + + const details = [ + result.stderr ? `stderr: ${result.stderr.replace(/\r?\n/g, " ").trim()}` : "", + result.stdout ? `stdout: ${result.stdout.replace(/\r?\n/g, " ").trim()}` : "", + ] + .filter(Boolean) + .join("\n "); + logWarning(`WARNING: failed to update API review sync metadata on PR #${pr.number}.` + (details ? `\n ${details}` : "")); +} + +function findOpenPrForHead(headSelector) { + const { owner, branch } = branchReferenceParts(headSelector); + const selector = `${owner}:${branch}`; + const allPrs = []; + + const direct = gh( + [ + "pr", + "list", + "--repo", + "Azure/azure-sdk-for-python", + "--head", + selector, + "--state", + "open", + "--json", + "number,url,state,updatedAt,headRefName,headRepositoryOwner", + "--limit", + "50", + ], + { check: false, capture: true }, + ); + + if (direct.status === 0) { + const prs = parseJsonOrNull(direct.stdout); + if (prs) { + allPrs.push(...prs); + } + } + + const searchQuery = `repo:Azure/azure-sdk-for-python is:pr is:open head:${branch}`; + const search = gh( + [ + "pr", + "list", + "--repo", + "Azure/azure-sdk-for-python", + "--search", + searchQuery, + "--state", + "open", + "--json", + "number,url,state,updatedAt,headRefName,headRepositoryOwner", + "--limit", + "50", + ], + { check: false, capture: true }, + ); + + if (search.status === 0) { + const prs = parseJsonOrNull(search.stdout); + if (prs) { + allPrs.push(...prs); + } + } + + if (allPrs.length === 0) { + return null; + } + + const deduped = new Map(); + for (const pr of allPrs) { + if ( + pr && + typeof pr === "object" && + "number" in pr && + pr.headRefName === branch && + pr.headRepositoryOwner && + pr.headRepositoryOwner.login === owner + ) { + deduped.set(pr.number, pr); + } + } + + return selectBestPr([...deduped.values()]); +} + +function findOpenPrForBranches(baseBranch, headBranch) { + const direct = gh( + [ + "pr", + "list", + "--repo", + "Azure/azure-sdk-for-python", + "--base", + baseBranch, + "--head", + headBranch, + "--state", + "open", + "--json", + "number,url,state,updatedAt,body", + "--limit", + "20", + ], + { check: false, capture: true }, + ); + + if (direct.status === 0) { + const prs = parseJsonOrNull(direct.stdout); + if (prs && prs.length > 0) { + return selectBestPr(prs); + } + } + + const search = gh( + [ + "pr", + "list", + "--repo", + "Azure/azure-sdk-for-python", + "--search", + `repo:Azure/azure-sdk-for-python is:pr is:open head:${headBranch} base:${baseBranch}`, + "--json", + "number,url,state,updatedAt,body", + "--limit", + "20", + ], + { check: false, capture: true }, + ); + + if (search.status !== 0) { + return null; + } + + const prs = parseJsonOrNull(search.stdout); + return prs ? selectBestPr(prs) : null; +} + +function createDraftPr(baseBranch, headBranch, title, body) { + const result = gh( + [ + "api", + "repos/Azure/azure-sdk-for-python/pulls", + "--method", + "POST", + "--field", + `base=${baseBranch}`, + "--field", + `head=${headBranch}`, + "--field", + `title=${title}`, + "--field", + `body=${body}`, + "--field", + "draft=true", + ], + { check: false, capture: true }, + ); + + if (result.status === 0) { + const createdPr = parseJsonObjectOrNull(result.stdout); + return { + ok: true, + url: createdPr && typeof createdPr.html_url === "string" ? createdPr.html_url : "", + stderr: result.stderr || "", + stdout: result.stdout || "", + }; + } + + return { + ok: false, + status: result.status, + stdout: result.stdout || "", + stderr: result.stderr || "", + }; +} + +function branchReferenceMarkdown(headSelector) { + const { owner, branch, display } = branchReferenceParts(headSelector); + const branchUrl = `https://github.com/${owner}/azure-sdk-for-python/tree/${encodeURIComponent(branch)}`; + return `[branch \`${display}\`](${branchUrl})`; +} + +function baselineReferenceMarkdown(baseTag) { + if (!baseTag) { + return "empty"; + } + + const commitSha = gitOut(["rev-list", "-n", "1", baseTag]); + const commitUrl = `https://github.com/Azure/azure-sdk-for-python/commit/${commitSha}`; + return `[tag \`${baseTag}\`](${commitUrl})`; +} + +function targetReferenceInfo(headSelector) { + const targetTag = resolveTargetTag(headSelector); + if (targetTag) { + return { + label: "Target tag", + markdown: baselineReferenceMarkdown(targetTag), + }; + } + + const pr = findOpenPrForHead(headSelector); + if (pr) { + return { + label: "Working PR", + markdown: `[PR #${pr.number}](${pr.url})`, + }; + } + + return { + label: "Working branch", + markdown: branchReferenceMarkdown(headSelector), + }; +} + +function writeBytes(filePath, bytes) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, bytes); +} + +async function generateApiBytesForRef({ + adapter, + repoRoot, + packageName, + packageDir, + runtimeExecutable, + ref, + refLabel, + logger, +}) { + const packageRelative = packageRelDir(packageDir); + logInfo(`Overlaying package source from ${refLabel} (${ref})`); + + // Overlay just the package directory from the target ref onto the working tree + git(["checkout", ref, "--", packageRelative]); + + try { + const version = adapter.readVersion(packageDir); + + await adapter.generateApiForPackage({ + repoRoot, + packageName, + runtimeExecutable, + logger, + refLabel, + }); + + const outputPath = apiMdPath(packageDir); + if (!fs.existsSync(outputPath)) { + throw new Error(`ERROR: did not produce ${outputPath}`); + } + + const result = { apiMd: fs.readFileSync(outputPath), metadata: null, version }; + + const metaPath = metadataPath(packageDir); + if (fs.existsSync(metaPath)) { + result.metadata = fs.readFileSync(metaPath); + } + + return result; + } finally { + // Restore the package directory to the current branch state + git(["reset", "--", packageRelative], { check: false }); + git(["checkout", "HEAD", "--", packageRelative]); + // Clean any untracked files that the generation may have left behind + git(["clean", "-fd", "--", packageRelative], { check: false }); + } +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + const adapter = loadAdapter(args.adapter); + + const packageDir = adapter.findPackageDir(REPO_ROOT, args.packageName); + logInfo(`Found package at: ${packageDir}`); + + ensureCleanWorktree(); + const originalBranch = currentBranch(); + if (originalBranch === "HEAD") { + throw new Error("ERROR: refusing to run from a detached HEAD."); + } + + git(["fetch", REMOTE, "main"]); + + const baseVersion = validateBaseTag(args.packageName, args.base); + + const targetRef = args.target ? resolveTargetRef(args.target) : MAIN_REF; + + try { + logInfo(`\n=== Capturing baseline api.md from tag ${args.base} ===`); + const baseResult = await generateApiBytesForRef({ + adapter, + repoRoot: REPO_ROOT, + packageName: args.packageName, + packageDir, + runtimeExecutable: args.runtimeExecutable, + ref: args.base, + refLabel: args.base, + logger, + }); + + logInfo(`\n=== Capturing target api.md from ${targetRef} ===`); + const targetResult = await generateApiBytesForRef({ + adapter, + repoRoot: REPO_ROOT, + packageName: args.packageName, + packageDir, + runtimeExecutable: args.runtimeExecutable, + ref: targetRef, + refLabel: targetRef, + logger, + }); + const targetVersion = targetResult.version; + + if (!apiResultsHaveApiDiff(baseResult, targetResult)) { + logInfo( + `\nNo API differences found for ${args.packageName} between ${args.base} (version ${baseVersion}) and ${targetRef} (version ${targetVersion}). No API review branches or PR were created.`, + ); + return 0; + } + + const apiPath = apiMdPath(packageDir); + const apiRelative = apiMdRel(packageDir); + const metaFilePath = metadataPath(packageDir); + const metaRelative = metadataRel(packageDir); + const desiredBaseState = desiredBranchState(baseResult); + const desiredReviewState = desiredBranchState(targetResult); + + ensureBranchStateHasMetadataSha("baseline API result", desiredBaseState); + ensureBranchStateHasMetadataSha("target API result", desiredReviewState); + + const baseSelection = resolveBranchSelection({ + preferredBranch: apiReviewBranchName("base", args.packageName, baseVersion), + desiredState: desiredBaseState, + apiRelative, + metaRelative, + }); + const baseBranch = baseSelection.branchName; + + if (baseSelection.reused) { + logInfo(`\n=== Reusing base branch ${baseBranch} ===`); + git(["checkout", "-B", baseBranch, baseSelection.remoteRef]); + } else { + logInfo(`\n=== Creating base branch ${baseBranch} ===`); + git(["checkout", "-B", baseBranch, MAIN_REF]); + writeBytes(apiPath, baseResult.apiMd); + git(["add", apiRelative]); + if (baseResult.metadata) { + writeBytes(metaFilePath, baseResult.metadata); + git(["add", metaRelative]); + } + git(["commit", "-m", `[API Review] Baseline api.md for ${args.packageName} ${baseVersion}`]); + + git(["push", "--force-with-lease", REMOTE, baseBranch]); + } + + const reviewSelection = resolveBranchSelection({ + preferredBranch: apiReviewBranchName("review", args.packageName, targetVersion), + desiredState: desiredReviewState, + apiRelative, + metaRelative, + requiredAncestorRef: baseBranch, + }); + const reviewBranch = reviewSelection.branchName; + + if (reviewSelection.reused) { + logInfo(`\n=== Reusing review branch ${reviewBranch} ===`); + git(["checkout", "-B", reviewBranch, reviewSelection.remoteRef]); + } else { + logInfo(`\n=== Creating review branch ${reviewBranch} ===`); + git(["checkout", "-B", reviewBranch, baseBranch]); + writeBytes(apiPath, targetResult.apiMd); + git(["add", apiRelative]); + if (targetResult.metadata) { + writeBytes(metaFilePath, targetResult.metadata); + git(["add", metaRelative]); + } + git(["commit", "-m", `[API Review] api.md for ${args.packageName} ${targetVersion}`]); + + git(["push", "--force-with-lease", REMOTE, reviewBranch]); + } + + const title = `[API Review] ${args.packageName} ${targetVersion} (base ${baseVersion})`; + const workingSelector = args.target || "main"; + const workingReference = targetReferenceInfo(workingSelector); + const baselineRef = baselineReferenceMarkdown(args.base); + const syncMetadata = buildSyncMetadataObject({ + packageName: args.packageName, + packageDir, + baseBranch, + reviewBranch, + headSelector: workingSelector, + }); + const syncMetadataBlock = buildSyncMetadataBlock(syncMetadata); + + const body = buildReviewPrBody({ + packageName: args.packageName, + targetVersion, + baseVersion, + workingReference, + baselineRef, + syncMetadataBlock, + }); + + if (baseSelection.reused && reviewSelection.reused) { + const existingPr = findOpenPrForBranches(baseBranch, reviewBranch); + if (existingPr) { + ensurePrBodySyncMetadata(existingPr, syncMetadataBlock); + logInfo(`\n=== Reusing existing PR #${existingPr.number} ===`); + logInfo(existingPr.url); + return 0; + } + } + + logInfo("\n=== Opening PR ==="); + const compareUrl = `https://github.com/Azure/azure-sdk-for-python/compare/${baseBranch}...${reviewBranch}?expand=1`; + const prCreate = createDraftPr(baseBranch, reviewBranch, title, body); + + if (prCreate.ok) { + if (prCreate.url) { + logInfo(prCreate.url); + } + } else { + const existingPr = findOpenPrForBranches(baseBranch, reviewBranch); + if (existingPr) { + ensurePrBodySyncMetadata(existingPr, syncMetadataBlock); + logInfo(`\n=== Reusing existing PR #${existingPr.number} ===`); + logInfo(existingPr.url); + return 0; + } + + const errorDetails = [ + `Exit code: ${prCreate.status}`, + prCreate.stderr ? `stderr: ${prCreate.stderr.replace(/\r?\n/g, " ").trim()}` : "", + prCreate.stdout ? `stdout: ${prCreate.stdout.replace(/\r?\n/g, " ").trim()}` : "", + "Debug repro: GH_DEBUG=1 gh api repos/Azure/azure-sdk-for-python/pulls --method POST --field base= --field head= --field title= --field body=<body> --field draft=true", + ] + .filter(Boolean) + .join("\n "); + logWarning( + "\nWARNING: `gh api` PR creation failed. Both branches were pushed successfully -- open the PR manually here:\n" + + ` ${compareUrl}\n` + + ` Title: ${title}` + + (errorDetails ? `\n ${errorDetails}` : ""), + ); + } + + return 0; + } finally { + git(["checkout", originalBranch], { check: false }); + } +} + +if (require.main === module) { + (async () => { + logger = await getDefaultLogger(); + try { + process.exit(await main()); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logError(message); + process.exit(1); + } + })(); +} else { + module.exports = { + __setCommandRunners({ git: gitRunner, gh: ghRunner }) { + if (gitRunner) { + git = gitRunner; + } + if (ghRunner) { + gh = ghRunner; + } + }, + buildSyncMetadataBlock, + buildSyncMetadataObject, + buildReviewPrBody, + apiResultsHaveApiDiff, + replaceSyncMetadataBlock, + targetReferenceInfo, + }; +} diff --git a/scripts/api_md_workflow/create_api_review_pr.test.js b/scripts/api_md_workflow/create_api_review_pr.test.js new file mode 100644 index 000000000000..cecd4677d03a --- /dev/null +++ b/scripts/api_md_workflow/create_api_review_pr.test.js @@ -0,0 +1,366 @@ +const assert = require("node:assert/strict"); +const test = require("node:test"); + +const workflow = require("./create_api_review_pr"); + +function commandResult(stdout = "[]", status = 0) { + return { + status, + stdout, + stderr: "", + }; +} + +function stubGitNoTags() { + return commandResult("", 1); +} + +function stubGhWithSearchResults(results) { + return (args) => { + if (args.includes("--search")) { + return commandResult(JSON.stringify(results)); + } + + return commandResult("[]"); + }; +} + +function parseSyncMetadataBlock(block) { + const jsonText = block + .replace(/^<!-- api-md-review-sync\n/, "") + .replace(/^DO NOT MODIFY THESE CONTENTS!\n/, "") + .replace(/\n-->$/, ""); + return JSON.parse(jsonText); +} + +test("targetReferenceInfo links matching open PR from direct head query", () => { + workflow.__setCommandRunners({ + git: stubGitNoTags, + gh: (args) => { + if (args.includes("--head")) { + return commandResult( + JSON.stringify([ + { + number: 45678, + url: "https://github.com/Azure/azure-sdk-for-python/pull/45678", + state: "OPEN", + updatedAt: "2026-06-05T00:00:00Z", + headRefName: "users/example/direct-feature", + headRepositoryOwner: { login: "example" }, + }, + ]), + ); + } + + return commandResult("[]"); + }, + }); + + assert.deepEqual(workflow.targetReferenceInfo("example:users/example/direct-feature"), { + label: "Working PR", + markdown: "[PR #45678](https://github.com/Azure/azure-sdk-for-python/pull/45678)", + }); +}); + +test("targetReferenceInfo links matching open PR for owner-qualified branch target", () => { + workflow.__setCommandRunners({ + git: stubGitNoTags, + gh: stubGhWithSearchResults([ + { + number: 12345, + url: "https://github.com/Azure/azure-sdk-for-python/pull/12345", + state: "OPEN", + updatedAt: "2026-06-05T00:00:00Z", + headRefName: "users/example/feature", + headRepositoryOwner: { login: "example" }, + }, + ]), + }); + + assert.deepEqual(workflow.targetReferenceInfo("example:users/example/feature"), { + label: "Working PR", + markdown: "[PR #12345](https://github.com/Azure/azure-sdk-for-python/pull/12345)", + }); +}); + +test("targetReferenceInfo keeps origin/main as branch when search returns fork PRs named main", () => { + workflow.__setCommandRunners({ + git: stubGitNoTags, + gh: stubGhWithSearchResults([ + { + number: 23456, + url: "https://github.com/Azure/azure-sdk-for-python/pull/23456", + state: "OPEN", + updatedAt: "2026-06-05T00:00:00Z", + headRefName: "main", + headRepositoryOwner: { login: "example" }, + }, + ]), + }); + + assert.deepEqual(workflow.targetReferenceInfo("origin/main"), { + label: "Working branch", + markdown: "[branch `origin/main`](https://github.com/Azure/azure-sdk-for-python/tree/main)", + }); +}); + +test("targetReferenceInfo keeps branch reference when no open PR matches both owner and branch", () => { + workflow.__setCommandRunners({ + git: stubGitNoTags, + gh: stubGhWithSearchResults([ + { + number: 34567, + url: "https://github.com/Azure/azure-sdk-for-python/pull/34567", + state: "OPEN", + updatedAt: "2026-06-05T00:00:00Z", + headRefName: "users/example/feature", + headRepositoryOwner: { login: "someone-else" }, + }, + ]), + }); + + assert.deepEqual(workflow.targetReferenceInfo("example:users/example/feature"), { + label: "Working branch", + markdown: "[branch `example:users/example/feature`](https://github.com/example/azure-sdk-for-python/tree/users%2Fexample%2Ffeature)", + }); +}); + +test("targetReferenceInfo treats existing target tag as tag and does not query PRs", () => { + let prLookupCount = 0; + + workflow.__setCommandRunners({ + git: (args) => { + if (args[0] === "rev-parse" && args.includes("refs/tags/azure-example_1.2.3")) { + return commandResult("", 0); + } + + if (args[0] === "rev-list") { + return commandResult("abc123def456\n", 0); + } + + return commandResult("", 1); + }, + gh: () => { + prLookupCount += 1; + return commandResult("[]"); + }, + }); + + assert.deepEqual(workflow.targetReferenceInfo("azure-example_1.2.3"), { + label: "Target tag", + markdown: "[tag `azure-example_1.2.3`](https://github.com/Azure/azure-sdk-for-python/commit/abc123def456)", + }); + assert.equal(prLookupCount, 0); +}); + +test("buildSyncMetadataObject creates hidden metadata for origin branch target", () => { + workflow.__setCommandRunners({ + git: stubGitNoTags, + gh: (args) => { + if (args.includes("--head")) { + return commandResult( + JSON.stringify([ + { + number: 47203, + url: "https://github.com/Azure/azure-sdk-for-python/pull/47203", + state: "OPEN", + updatedAt: "2026-06-05T00:00:00Z", + headRefName: "feature/api-change", + headRepositoryOwner: { login: "Azure" }, + }, + ]), + ); + } + + return commandResult("[]"); + }, + }); + + const metadata = workflow.buildSyncMetadataObject({ + packageName: "azure-example", + packageDir: "sdk/service/azure-example", + baseBranch: "apireview/base_azure-example_1.0.0", + reviewBranch: "apireview/review_azure-example_1.1.0", + headSelector: "feature/api-change", + }); + const block = workflow.buildSyncMetadataBlock(metadata); + + assert.ok(block.startsWith("<!-- api-md-review-sync\nDO NOT MODIFY THESE CONTENTS!\n")); + assert.ok(block.endsWith("\n-->")); + assert.deepEqual(parseSyncMetadataBlock(block), { + schemaVersion: 1, + repository: "Azure/azure-sdk-for-python", + packageName: "azure-example", + packageDir: "sdk/service/azure-example", + baseBranch: "apireview/base_azure-example_1.0.0", + reviewBranch: "apireview/review_azure-example_1.1.0", + workingOwner: "Azure", + workingBranch: "feature/api-change", + workingPrNumber: 47203, + }); +}); + +test("buildSyncMetadataObject records fork owner and branch target", () => { + workflow.__setCommandRunners({ + git: stubGitNoTags, + gh: stubGhWithSearchResults([ + { + number: 47204, + url: "https://github.com/Azure/azure-sdk-for-python/pull/47204", + state: "OPEN", + updatedAt: "2026-06-05T00:00:00Z", + headRefName: "users/example/feature", + headRepositoryOwner: { login: "example" }, + }, + ]), + }); + + const metadata = workflow.buildSyncMetadataObject({ + packageName: "azure-example", + packageDir: "sdk/service/azure-example", + baseBranch: "apireview/base_azure-example_1.0.0", + reviewBranch: "apireview/review_azure-example_1.1.0", + headSelector: "example:users/example/feature", + }); + + assert.equal(metadata.workingOwner, "example"); + assert.equal(metadata.workingBranch, "users/example/feature"); + assert.equal(metadata.workingPrNumber, 47204); +}); + +test("buildSyncMetadataObject omits metadata for tag targets", () => { + let prLookupCount = 0; + + workflow.__setCommandRunners({ + git: (args) => { + if (args[0] === "rev-parse" && args.includes("refs/tags/azure-example_1.2.3")) { + return commandResult("", 0); + } + + return commandResult("", 1); + }, + gh: () => { + prLookupCount += 1; + return commandResult("[]"); + }, + }); + + assert.equal( + workflow.buildSyncMetadataObject({ + packageName: "azure-example", + packageDir: "sdk/service/azure-example", + baseBranch: "apireview/base_azure-example_1.0.0", + reviewBranch: "apireview/review_azure-example_1.1.0", + headSelector: "azure-example_1.2.3", + }), + null, + ); + assert.equal(prLookupCount, 0); +}); + +test("buildSyncMetadataObject records main branch target", () => { + workflow.__setCommandRunners({ + git: stubGitNoTags, + gh: () => commandResult("[]"), + }); + + const metadata = workflow.buildSyncMetadataObject({ + packageName: "azure-example", + packageDir: "sdk/service/azure-example", + baseBranch: "apireview/base_azure-example_1.0.0", + reviewBranch: "apireview/review_azure-example_1.1.0", + headSelector: "main", + }); + + assert.equal(metadata.workingOwner, "Azure"); + assert.equal(metadata.workingBranch, "main"); + assert.equal(metadata.workingPrNumber, null); +}); + +test("buildSyncMetadataObject records null working PR for branch target without PR", () => { + workflow.__setCommandRunners({ + git: stubGitNoTags, + gh: () => commandResult("[]"), + }); + + const metadata = workflow.buildSyncMetadataObject({ + packageName: "azure-example", + packageDir: "sdk/service/azure-example", + baseBranch: "apireview/base_azure-example_1.0.0", + reviewBranch: "apireview/review_azure-example_1.1.0", + headSelector: "feature/no-pr", + }); + + assert.equal(metadata.workingOwner, "Azure"); + assert.equal(metadata.workingBranch, "feature/no-pr"); + assert.equal(metadata.workingPrNumber, null); +}); + +test("buildReviewPrBody calls out static tag-to-tag reviews", () => { + const body = workflow.buildReviewPrBody({ + packageName: "azure-example", + targetVersion: "1.2.3", + baseVersion: "1.2.2", + workingReference: { + label: "Target tag", + markdown: "[tag `azure-example_1.2.3`](https://github.com/Azure/azure-sdk-for-python/commit/abc123)", + }, + baselineRef: "[tag `azure-example_1.2.2`](https://github.com/Azure/azure-sdk-for-python/commit/def456)", + syncMetadataBlock: null, + }); + + assert.ok(body.includes("Static tag-to-tag review")); + assert.ok(body.includes("cannot be automatically updated from a working branch")); + assert.equal(body.includes("api-md-review-sync"), false); +}); + +test("apiResultsHaveApiDiff returns false for identical API markdown", () => { + assert.equal( + workflow.apiResultsHaveApiDiff( + { apiMd: Buffer.from("# API\n\nclass Same\n"), metadata: Buffer.from("apiMdSha256: old") }, + { apiMd: Buffer.from("# API\n\nclass Same\n"), metadata: Buffer.from("apiMdSha256: new") }, + ), + false, + ); +}); + +test("apiResultsHaveApiDiff returns true for changed API markdown", () => { + assert.equal( + workflow.apiResultsHaveApiDiff( + { apiMd: Buffer.from("# API\n\nclass Old\n") }, + { apiMd: Buffer.from("# API\n\nclass New\n") }, + ), + true, + ); +}); + +test("replaceSyncMetadataBlock replaces stale hidden metadata", () => { + const oldBlock = workflow.buildSyncMetadataBlock({ + schemaVersion: 1, + repository: "Azure/azure-sdk-for-python", + packageName: "old-package", + packageDir: "sdk/service/old-package", + baseBranch: "apireview/base_old-package_1.0.0", + reviewBranch: "apireview/review_old-package_1.1.0", + workingOwner: "Azure", + workingBranch: "old-feature", + }); + const newBlock = workflow.buildSyncMetadataBlock({ + schemaVersion: 1, + repository: "Azure/azure-sdk-for-python", + packageName: "azure-example", + packageDir: "sdk/service/azure-example", + baseBranch: "apireview/base_azure-example_1.0.0", + reviewBranch: "apireview/review_azure-example_1.1.0", + workingOwner: "Azure", + workingBranch: "feature/api-change", + }); + + const body = workflow.replaceSyncMetadataBlock(`Review body\n\n${oldBlock}`, newBlock); + + assert.ok(body.startsWith("Review body\n\n<!-- api-md-review-sync")); + assert.ok(body.includes("DO NOT MODIFY THESE CONTENTS!")); + assert.ok(body.includes('"packageName": "azure-example"')); + assert.equal(body.includes("old-package"), false); + assert.equal((body.match(/api-md-review-sync/g) || []).length, 1); +}); \ No newline at end of file diff --git a/scripts/api_md_workflow/find_affected.js b/scripts/api_md_workflow/find_affected.js new file mode 100644 index 000000000000..51047e732bd5 --- /dev/null +++ b/scripts/api_md_workflow/find_affected.js @@ -0,0 +1,74 @@ +#!/usr/bin/env node + +const { + REPO_ROOT, + appendGithubOutput, + envPath, + getDefaultLogger, + loadSharedModule, + requireEnv, + runAsync, + writeLines, +} = require("./common"); +const { loadAdapter, loadWorkflowConfig } = require("./adapter_config"); + +async function main() { + const { includesSegment } = await loadSharedModule("path.js"); + + const config = loadWorkflowConfig(); + const adapterName = config.adapter; + const adapter = loadAdapter(adapterName); + if (typeof adapter.isPackageDir !== "function") { + throw new Error(`ERROR: adapter '${adapterName}' does not implement isPackageDir(repoRoot, packageDirRelative).`); + } + + const baseRef = requireEnv("API_MD_BASE_REF"); + const packagesFile = envPath("API_MD_PACKAGES_FILE", ".artifacts/affected_package_dirs.txt"); + const changedFile = envPath("API_MD_CHANGED_FILE", ".artifacts/changed_package_dirs.txt"); + + await runAsync("git", ["fetch", "--no-tags", "--depth=1", "origin", baseRef], { + cwd: REPO_ROOT, + }); + const diff = ( + await runAsync("git", ["diff", "--name-only", `origin/${baseRef}..HEAD`], { + cwd: REPO_ROOT, + }) + ).stdout; + + const changedDirs = new Set(); + for (const filePath of diff.split(/\r?\n/)) { + const trimmed = filePath.trim(); + if (!trimmed) { + continue; + } + if (!includesSegment(trimmed, "sdk")) { + continue; + } + + const parts = trimmed.split("/"); + if (parts.length < 3 || parts[0] !== "sdk") { + continue; + } + + changedDirs.add(parts.slice(0, 3).join("/")); + } + + const sortedChanged = [...changedDirs].sort(); + writeLines(changedFile, sortedChanged); + + const affected = []; + for (const packageDir of sortedChanged) { + if (adapter.isPackageDir(REPO_ROOT, packageDir)) { + affected.push(packageDir); + } + } + + writeLines(packagesFile, affected); + appendGithubOutput("count", affected.length); +} + +main().catch(async (error) => { + const logger = await getDefaultLogger(); + logger.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +}); diff --git a/scripts/api_md_workflow/find_mismatches.js b/scripts/api_md_workflow/find_mismatches.js new file mode 100644 index 000000000000..d2651c2ccc8f --- /dev/null +++ b/scripts/api_md_workflow/find_mismatches.js @@ -0,0 +1,99 @@ +#!/usr/bin/env node + +const fs = require("fs"); + +const { appendGithubOutput, envPath, getDefaultLogger, readLines, runAsync, writeLines } = require("./common"); +const { loadAdapter, loadWorkflowConfig } = require("./adapter_config"); + +/** + * Parse a simple key: value YAML file into an object. + * Only handles flat scalar mappings (no nesting, no multi-line values). + */ +function parseSimpleYaml(text) { + const result = {}; + for (const line of text.split(/\r?\n/)) { + const match = line.match(/^(\w+)\s*:\s*(.*)$/); + if (match) { + result[match[1]] = match[2].trim(); + } + } + return result; +} + +async function main() { + const config = loadWorkflowConfig(); + const adapter = loadAdapter(config.adapter); + + // Fields to compare in api.metadata.yml. If the adapter doesn't specify, + // compare all fields (strict default for languages that don't opt out). + const fieldsToValidate = adapter.metadataFieldsToValidate || null; + + const packagesFile = envPath("API_MD_PACKAGES_FILE", ".artifacts/affected_package_dirs.txt"); + const mismatchesFile = envPath("API_MD_MISMATCHES_FILE", ".artifacts/mismatched_api_files.txt"); + const missingFile = envPath("API_MD_MISSING_FILE", ".artifacts/missing_api_files.txt"); + const packages = readLines(packagesFile); + + const mismatches = []; + const missing = []; + for (const pkgDir of packages) { + const apiFile = `${pkgDir}/api.md`; + const metadataFile = `${pkgDir}/api.metadata.yml`; + + // Enforce that each affected package has a committed api.md file. + if (!fs.existsSync(apiFile) || !fs.statSync(apiFile).isFile()) { + missing.push(apiFile); + continue; + } + + const diffResult = await runAsync("git", ["ls-files", "--error-unmatch", "--", apiFile], { + check: false, + }); + if (diffResult.status !== 0) { + missing.push(apiFile); + continue; + } + + // api.metadata.yml must be present alongside api.md. + if (!fs.existsSync(metadataFile) || !fs.statSync(metadataFile).isFile()) { + missing.push(metadataFile); + } else { + const committedMeta = await runAsync("git", ["show", `HEAD:${metadataFile}`], { + check: false, + }); + if (committedMeta.status !== 0) { + // Not yet committed — treat as missing + missing.push(metadataFile); + } else { + const current = parseSimpleYaml(fs.readFileSync(metadataFile, "utf-8")); + const committed = parseSimpleYaml(committedMeta.stdout); + + // Compare only adapter-specified fields, or all fields if not specified. + const keys = fieldsToValidate || Object.keys({ ...committed, ...current }); + const mismatch = keys.some((key) => current[key] !== committed[key]); + if (mismatch) { + mismatches.push(metadataFile); + } + } + } + + // Diff-gate the full api.md content; metadata is field-gated above. + const quietDiffResult = await runAsync("git", ["diff", "--quiet", "--", apiFile], { + check: false, + }); + if (quietDiffResult.status !== 0) { + mismatches.push(apiFile); + } + } + + writeLines(mismatchesFile, mismatches); + writeLines(missingFile, missing); + appendGithubOutput("mismatch_count", mismatches.length); + appendGithubOutput("missing_count", missing.length); + appendGithubOutput("issue_count", mismatches.length + missing.length); +} + +main().catch(async (error) => { + const logger = await getDefaultLogger(); + logger.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +}); diff --git a/scripts/api_md_workflow/regenerate.js b/scripts/api_md_workflow/regenerate.js new file mode 100644 index 000000000000..555a6ead48fd --- /dev/null +++ b/scripts/api_md_workflow/regenerate.js @@ -0,0 +1,41 @@ +#!/usr/bin/env node + +const path = require("path"); + +const { REPO_ROOT, envPath, getDefaultLogger, readLines } = require("./common"); +const { loadAdapter, loadWorkflowConfig } = require("./adapter_config"); + +async function main() { + const logger = await getDefaultLogger(); + const config = loadWorkflowConfig(); + const adapter = loadAdapter(config.adapter); + if (typeof adapter.generateApiForPackage !== "function") { + throw new Error( + `ERROR: adapter '${config.adapter}' does not implement generateApiForPackage({ repoRoot, packageName, runtimeExecutable }).`, + ); + } + + const packagesFile = envPath("API_MD_PACKAGES_FILE", ".artifacts/affected_package_dirs.txt"); + const packages = readLines(packagesFile); + if (!packages.length) { + return; + } + + const runtimeExecutable = process.env.RUNTIME_EXECUTABLE || null; + for (const pkgDir of packages) { + const packageName = path.basename(pkgDir); + logger.info(`Generating api.md for ${packageName}`); + await adapter.generateApiForPackage({ + repoRoot: REPO_ROOT, + packageName, + runtimeExecutable, + logger, + }); + } +} + +main().catch(async (error) => { + const logger = await getDefaultLogger(); + logger.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +}); diff --git a/sdk/template/azure-template/README.md b/sdk/template/azure-template/README.md index 4e7b3cc44531..0425130a69c8 100644 --- a/sdk/template/azure-template/README.md +++ b/sdk/template/azure-template/README.md @@ -31,5 +31,3 @@ More sample code should go here, along with links out to the appropriate example # Contributing If you encounter any bugs or have suggestions, please file an issue in the [Issues](<https://github.com/Azure/azure-sdk-for-python/issues>) section of the project. - - diff --git a/sdk/template/azure-template/api.md b/sdk/template/azure-template/api.md new file mode 100644 index 000000000000..638c13dae01a --- /dev/null +++ b/sdk/template/azure-template/api.md @@ -0,0 +1,12 @@ +```py +namespace azure.template + + def azure.template.template_main() -> bool: ... + + +namespace azure.template.template_code + + def azure.template.template_code.template_main() -> bool: ... + + +``` \ No newline at end of file diff --git a/sdk/template/azure-template/api.metadata.yml b/sdk/template/azure-template/api.metadata.yml new file mode 100644 index 000000000000..f29742a2b179 --- /dev/null +++ b/sdk/template/azure-template/api.metadata.yml @@ -0,0 +1,3 @@ +apiMdSha256: 9b0fa6154e3a859680da1a07f5106508983884de567522c5166fc57bacb9cb00 +parserVersion: 0.3.28 +pythonVersion: 3.12.9