diff --git a/tools/js-sdk-release-tools/package-lock.json b/tools/js-sdk-release-tools/package-lock.json index 8f0be8ac1e6..b84ddbbd602 100644 --- a/tools/js-sdk-release-tools/package-lock.json +++ b/tools/js-sdk-release-tools/package-lock.json @@ -1,26 +1,27 @@ { - "name": "@azure-tools/js-sdk-release-tools", - "version": "2.7.11", + "name": "js-sdk-release-tools", + "version": "2.7.21-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@azure-tools/js-sdk-release-tools", - "version": "2.7.11", + "name": "js-sdk-release-tools", + "version": "2.7.21-beta", "license": "MIT", "dependencies": { "@azure-tools/openapi-tools-common": "^1.2.2", + "@npmcli/package-json": "^5.2.0", "@ts-common/azure-js-dev-tools": "^21.1.0", "colors": "1.4.0", "command-line-args": "^5.1.1", "comment-json": "^4.1.0", + "copyfiles": "^2.4.1", "fs-extra": "^11.2.0", "glob": "^11.0.0", "js-yaml": "^4.1.0", "parse-ts-to-ast": "^0.1.1", "semver": "^7.3.5", "shelljs": "^0.8.4", - "shx": "^0.3.4", "simple-git": "^3.5.0", "ts-morph": "^23.0.0", "tslib": "^1.9.3", @@ -37,7 +38,9 @@ "rlc-code-gen": "dist/rlcCodegenCli.js" }, "devDependencies": { + "@types/fs-extra": "^11.0.4", "@types/node": "^20.12.12", + "@types/npmcli__package-json": "^4.0.4", "@types/shelljs": "^0.8.15", "@types/unixify": "^1.0.2", "rimraf": "^3.0.2", @@ -451,6 +454,177 @@ "node": ">= 8" } }, + "node_modules/@npmcli/git": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.8.tgz", + "integrity": "sha512-liASfw5cqhjNW9UFd+ruwwdEf/lbOAQjLL2XY2dFW/bkJheXDYZgOyul/4gVvEV4BWkTXjYGmDqMw9uegdbJNQ==", + "dependencies": { + "@npmcli/promise-spawn": "^7.0.0", + "ini": "^4.1.3", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^9.0.0", + "proc-log": "^4.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.2.0.tgz", + "integrity": "sha512-qe/kiqqkW0AGtvBjL8TJKZk/eBBSpnJkUWvHdQ9jM2lKHXRYYJuyNpJPlJw3c8QjC2ow6NZYiLExhUaeJelbxQ==", + "dependencies": { + "@npmcli/git": "^5.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^7.0.0", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "proc-log": "^4.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/@npmcli/package-json/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.2.tgz", + "integrity": "sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==", + "dependencies": { + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, "node_modules/@octokit/auth-token": { "version": "2.5.0", "license": "MIT", @@ -1023,6 +1197,16 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, "node_modules/@types/glob": { "version": "8.1.0", "license": "MIT", @@ -1035,6 +1219,15 @@ "version": "3.12.10", "license": "MIT" }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/minimatch": { "version": "5.1.2", "license": "MIT" @@ -1066,6 +1259,12 @@ "node": ">= 6" } }, + "node_modules/@types/npmcli__package-json": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/npmcli__package-json/-/npmcli__package-json-4.0.4.tgz", + "integrity": "sha512-6QjlFUSHBmZJWuC08bz1ZCx6tm4t+7+OJXAdvM6tL2pI7n6Bh5SIp/YxQvnOLFf8MzCXs2ijyFgrzaiu1UFBGA==", + "dev": true + }, "node_modules/@types/retry": { "version": "0.12.5", "license": "MIT" @@ -1424,6 +1623,36 @@ "node": ">=4" } }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansi-styles/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ansi-styles/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, "node_modules/archiver": { "version": "3.1.1", "license": "MIT", @@ -1742,36 +1971,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chalk/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "peer": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/chalk/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "peer": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/chalk/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT", - "peer": true - }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -1952,6 +2151,162 @@ "version": "0.0.1", "license": "MIT" }, + "node_modules/copyfiles": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", + "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", + "dependencies": { + "glob": "^7.0.5", + "minimatch": "^3.0.3", + "mkdirp": "^1.0.4", + "noms": "0.0.0", + "through2": "^2.0.1", + "untildify": "^4.0.0", + "yargs": "^16.1.0" + }, + "bin": { + "copyfiles": "copyfiles", + "copyup": "copyfiles" + } + }, + "node_modules/copyfiles/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/copyfiles/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/copyfiles/node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/copyfiles/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/copyfiles/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/copyfiles/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/copyfiles/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/copyfiles/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/copyfiles/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/copyfiles/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/copyfiles/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/copyfiles/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "engines": { + "node": ">=10" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "license": "MIT" @@ -2118,6 +2473,11 @@ "version": "1.1.2", "license": "BSD-2-Clause" }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" + }, "node_modules/esbuild": { "version": "0.23.0", "dev": true, @@ -2156,6 +2516,14 @@ "@esbuild/win32-x64": "0.23.0" } }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "license": "MIT", @@ -2650,7 +3018,8 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" }, "node_modules/function-bind": { "version": "1.1.2", @@ -2833,6 +3202,22 @@ "version": "5.0.0", "license": "MIT" }, + "node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, "node_modules/human-signals": { "version": "5.0.0", "dev": true, @@ -2903,6 +3288,15 @@ "version": "2.0.4", "license": "ISC" }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/interpret": { "version": "1.4.0", "license": "MIT", @@ -3064,6 +3458,14 @@ "license": "MIT", "peer": true }, + "node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "license": "MIT", @@ -3462,6 +3864,49 @@ } } }, + "node_modules/noms": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", + "integrity": "sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "~1.0.31" + } + }, + "node_modules/noms/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "node_modules/noms/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/noms/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "license": "MIT", @@ -3469,6 +3914,57 @@ "node": ">=0.10.0" } }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.2.tgz", + "integrity": "sha512-IGN0IAwmhDJwy13Wc8k+4PEbTPhpJnMtfR53ZbOyjkvmEcLS4nCwp6mvMWjS5sUjeiW3mpx6cHmuhKEu9XmcQw==", + "license": "ISC", + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.1.0.tgz", + "integrity": "sha512-nkc+3pIIhqHVQr085X9d2JzPzLyjzQS96zbruppqC9aZRm/x8xx6xhI98gHtsfELP2bE+loHq8ZaHFHhe+NauA==", + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, "node_modules/npm-run-path": { "version": "5.3.0", "dev": true, @@ -3787,6 +4283,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4060,6 +4557,15 @@ "node": ">= 0.8.0" } }, + "node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/process": { "version": "0.11.10", "license": "MIT", @@ -4075,6 +4581,31 @@ "version": "4.0.0", "license": "MIT" }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-retry/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "engines": { + "node": ">= 4" + } + }, "node_modules/pump": { "version": "3.0.0", "license": "MIT", @@ -4409,20 +4940,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/shx": { - "version": "0.3.4", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.3", - "shelljs": "^0.8.5" - }, - "bin": { - "shx": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/siginfo": { "version": "2.0.0", "dev": true, @@ -4508,6 +5025,34 @@ "node": ">=0.10.0" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==" + }, "node_modules/split2": { "version": "4.2.0", "license": "ISC", @@ -4701,6 +5246,42 @@ "real-require": "^0.2.0" } }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/tinybench": { "version": "2.9.0", "dev": true, @@ -4930,6 +5511,14 @@ "node": ">=0.10.0" } }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "engines": { + "node": ">=8" + } + }, "node_modules/uri-js": { "version": "4.4.1", "license": "BSD-2-Clause", @@ -4949,6 +5538,23 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/varint": { "version": "0.0.3", "license": "MIT" @@ -5379,6 +5985,8 @@ "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -5394,40 +6002,17 @@ }, "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-convert": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { "node": ">=8" @@ -5435,6 +6020,8 @@ }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -5447,6 +6034,8 @@ }, "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -5516,6 +6105,14 @@ "node": ">=4.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "4.0.3", "license": "ISC" diff --git a/tools/js-sdk-release-tools/package.json b/tools/js-sdk-release-tools/package.json index 40cb6e19053..a3b0fee6ff5 100644 --- a/tools/js-sdk-release-tools/package.json +++ b/tools/js-sdk-release-tools/package.json @@ -1,12 +1,16 @@ { "name": "@azure-tools/js-sdk-release-tools", - "version": "2.7.11", + "version": "2.7.12", "description": "", + "files": [ + "dist" + ], "scripts": { "dev": "tsx watch src/changelogToolCli.ts", "start": "node dist/changelogToolCli.js", "debug": "node --inspect-brk dist/changelogToolCli.js", - "build": "rimraf dist && tsc -p .", + "build": "rimraf dist && tsc -p . && npm run copy-files", + "copy-files": "copyfiles -f src/common/ciYamlTemplates/*.template.yml dist/common/ciYamlTemplates/", "prepack": "npm run build", "test": "vitest --run", "test:watch": "vitest" @@ -22,17 +26,18 @@ "license": "MIT", "dependencies": { "@azure-tools/openapi-tools-common": "^1.2.2", + "@npmcli/package-json": "^5.2.0", "@ts-common/azure-js-dev-tools": "^21.1.0", "colors": "1.4.0", "command-line-args": "^5.1.1", "comment-json": "^4.1.0", + "copyfiles": "^2.4.1", "fs-extra": "^11.2.0", "glob": "^11.0.0", "js-yaml": "^4.1.0", "parse-ts-to-ast": "^0.1.1", "semver": "^7.3.5", "shelljs": "^0.8.4", - "shx": "^0.3.4", "simple-git": "^3.5.0", "ts-morph": "^23.0.0", "tslib": "^1.9.3", @@ -42,7 +47,9 @@ "yaml": "^1.10.2" }, "devDependencies": { + "@types/fs-extra": "^11.0.4", "@types/node": "^20.12.12", + "@types/npmcli__package-json": "^4.0.4", "@types/shelljs": "^0.8.15", "@types/unixify": "^1.0.2", "rimraf": "^3.0.2", diff --git a/tools/js-sdk-release-tools/packages/typescript-codegen-breaking-change-detector/src/azure/detect-breaking-changes.ts b/tools/js-sdk-release-tools/packages/typescript-codegen-breaking-change-detector/src/azure/detect-breaking-changes.ts index 7ee330fc8e3..cf43d64d48e 100644 --- a/tools/js-sdk-release-tools/packages/typescript-codegen-breaking-change-detector/src/azure/detect-breaking-changes.ts +++ b/tools/js-sdk-release-tools/packages/typescript-codegen-breaking-change-detector/src/azure/detect-breaking-changes.ts @@ -145,7 +145,7 @@ async function detectBreakingChangesCore(projectContext: ProjectContext): Promis ); return breakingChangeResults; } catch (err) { - logger.error(`Failed to detect breaking changes due to ${(err as Error).stack ?? err}`); + logger.error(`Failed to detect breaking changes due to ${(err as Error)?.stack ?? err}`); return undefined; } } diff --git a/tools/js-sdk-release-tools/src/autoGenerateInPipeline.ts b/tools/js-sdk-release-tools/src/autoGenerateInPipeline.ts index 858f7b749e9..431b4d8894e 100644 --- a/tools/js-sdk-release-tools/src/autoGenerateInPipeline.ts +++ b/tools/js-sdk-release-tools/src/autoGenerateInPipeline.ts @@ -1,88 +1,112 @@ #!/usr/bin/env node import * as path from 'path'; -import { generateMgmt } from "./hlc/generateMgmt"; +import { generateMgmt } from './hlc/generateMgmt'; import { backupNodeModules, restoreNodeModules } from './utils/backupNodeModules'; -import { logger } from "./utils/logger"; -import { generateRLCInPipeline } from "./llc/generateRLCInPipeline/generateRLCInPipeline"; -import { RunningEnvironment } from "./utils/runningEnvironment"; +import { logger } from './utils/logger'; +import { generateRLCInPipeline } from './llc/generateRLCInPipeline/generateRLCInPipeline'; +import { ModularClientPackageOptions, SDKType } from './common/types'; +import { generateAzureSDKPackage } from './mlc/clientGenerator/modularClientPackageGenerator'; +import { parseInputJson } from './utils/generateInputUtils'; const shell = require('shelljs'); const fs = require('fs'); -async function automationGenerateInPipeline(inputJsonPath: string, outputJsonPath: string, use: string | undefined, typespecEmitter: string | undefined, sdkGenerationType: string | undefined) { +async function automationGenerateInPipeline( + inputJsonPath: string, + outputJsonPath: string, + use: string | undefined, + typespecEmitter: string | undefined, + sdkGenerationType: string | undefined, + local: boolean +) { const inputJson = JSON.parse(fs.readFileSync(inputJsonPath, { encoding: 'utf-8' })); - const specFolder: string = inputJson['specFolder']; - const readmeFiles: string[] | string | undefined = inputJson['relatedReadmeMdFiles'] ? inputJson['relatedReadmeMdFiles'] : inputJson['relatedReadmeMdFile']; - const typespecProjectFolder: string[] | string | undefined = inputJson['relatedTypeSpecProjectFolder']; - const gitCommitId: string = inputJson['headSha']; - const repoHttpsUrl: string = inputJson['repoHttpsUrl']; - const autorestConfig: string | undefined = inputJson['autorestConfig']; - const downloadUrlPrefix: string | undefined = inputJson.installInstructionInput?.downloadUrlPrefix; - const skipGeneration: boolean | undefined = inputJson['skipGeneration']; + const { + sdkType, + specFolder, + readmeMd, + gitCommitId, + outputJson, + repoHttpsUrl, + downloadUrlPrefix, + skipGeneration, + runningEnvironment, + typespecProject, + autorestConfig + } = await parseInputJson(inputJson); - if (!readmeFiles && !typespecProjectFolder) { - throw new Error(`readme files and typespec project info are both undefined`); - } - - if (readmeFiles && (typeof readmeFiles !== 'string') && readmeFiles.length !== 1) { - throw new Error(`get ${readmeFiles.length} readme files`); - } - - if (typespecProjectFolder && (typeof typespecProjectFolder !== 'string') && typespecProjectFolder.length !== 1) { - throw new Error(`get ${typespecProjectFolder.length} typespec project`); - } - - const isTypeSpecProject = !!typespecProjectFolder; - - const packages: any[] = []; - const outputJson = { - packages: packages - }; - const readmeMd = isTypeSpecProject ? undefined : typeof readmeFiles === 'string' ? readmeFiles : readmeFiles![0]; - const typespecProject = isTypeSpecProject ? typeof typespecProjectFolder === 'string' ? typespecProjectFolder : typespecProjectFolder![0] : undefined; - const isMgmt = isTypeSpecProject ? false : readmeMd!.includes('resource-manager'); - const runningEnvironment = typeof readmeFiles === 'string' || typeof typespecProjectFolder === 'string' ? RunningEnvironment.SdkGeneration : RunningEnvironment.SwaggerSdkAutomation; try { - await backupNodeModules(String(shell.pwd())); - if (isMgmt) { - await generateMgmt({ - sdkRepo: String(shell.pwd()), - swaggerRepo: specFolder, - readmeMd: readmeMd!, - gitCommitId: gitCommitId, - use: use, - outputJson: outputJson, - swaggerRepoUrl: repoHttpsUrl, - downloadUrlPrefix: downloadUrlPrefix, - skipGeneration: skipGeneration, - runningEnvironment: runningEnvironment - }); - } else { - await generateRLCInPipeline({ - sdkRepo: String(shell.pwd()), - swaggerRepo: path.isAbsolute(specFolder) ? specFolder : path.join(String(shell.pwd()), specFolder), - readmeMd: readmeMd, - typespecProject: typespecProject, - autorestConfig, - use: use, - typespecEmitter: !!typespecEmitter ? typespecEmitter : `@azure-tools/typespec-ts`, - outputJson: outputJson, - skipGeneration: skipGeneration, - sdkGenerationType: (sdkGenerationType === "command") ? "command" : "script", - runningEnvironment: runningEnvironment, - swaggerRepoUrl: repoHttpsUrl, - gitCommitId: gitCommitId, - }) + if (!local) { + await backupNodeModules(String(shell.pwd())); + } + switch (sdkType) { + case SDKType.HighLevelClient: + await generateMgmt({ + sdkRepo: String(shell.pwd()), + swaggerRepo: specFolder, + readmeMd: readmeMd!, + gitCommitId: gitCommitId, + use: use, + outputJson: outputJson, + swaggerRepoUrl: repoHttpsUrl, + downloadUrlPrefix: downloadUrlPrefix, + skipGeneration: skipGeneration, + runningEnvironment: runningEnvironment + }); + break; + case SDKType.RestLevelClient: + await generateRLCInPipeline({ + sdkRepo: String(shell.pwd()), + swaggerRepo: path.isAbsolute(specFolder) ? specFolder : path.join(String(shell.pwd()), specFolder), + readmeMd: readmeMd, + typespecProject: typespecProject, + autorestConfig, + use: use, + typespecEmitter: !!typespecEmitter ? typespecEmitter : `@azure-tools/typespec-ts`, + outputJson: outputJson, + skipGeneration: skipGeneration, + sdkGenerationType: sdkGenerationType === 'command' ? 'command' : 'script', + runningEnvironment: runningEnvironment, + swaggerRepoUrl: repoHttpsUrl, + gitCommitId: gitCommitId + }); + break; + + case SDKType.ModularClient: { + const typeSpecDirectory = path.posix.join(specFolder, typespecProject!); + const sdkRepoRoot = String(shell.pwd()); + const skip = skipGeneration ?? false; + const repoUrl = repoHttpsUrl; + const options: ModularClientPackageOptions = { + sdkRepoRoot, + specRepoRoot: specFolder, + typeSpecDirectory, + gitCommitId, + skip, + repoUrl, + local, + // support MPG for now + versionPolicyName: 'management' + }; + const packageResult = await generateAzureSDKPackage(options); + outputJson.packages = [packageResult]; + break; + } + default: + break; } } catch (e) { - const packageName = outputJson.packages?.[0].packageName; - logger.error(`Failed to generate SDK for package ${"'" + packageName + "'" ?? ''} due to ${(e as Error)?.stack ?? e}.`); + const packageNameStr = `'${outputJson.packages?.[0]?.packageName}' `; + logger.error(`Failed to generate SDK for package ${packageNameStr ?? ''}due to ${(e as Error)?.stack ?? e}.`); logger.error(`Please review the detail errors for potential fixes.`); - logger.error(`If the issue persists, contact the support channel at https://aka.ms/azsdk/js-teams-channel and include this spec pull request.`) + logger.error( + `If the issue persists, contact the support channel at https://aka.ms/azsdk/js-teams-channel and include this spec pull request.` + ); throw e; } finally { - await restoreNodeModules(String(shell.pwd())); + if (!local) { + await restoreNodeModules(String(shell.pwd())); + } fs.writeFileSync(outputJsonPath, JSON.stringify(outputJson, null, ' '), { encoding: 'utf-8' }); } } @@ -93,10 +117,13 @@ const optionDefinitions = [ { name: 'sdkGenerationType', type: String }, { name: 'inputJsonPath', type: String }, { name: 'outputJsonPath', type: String }, + // this option should be only used in local run, it will skip backup node modules, etc. + // do NOT set to true in sdk automation pipeline + { name: 'local', type: Boolean, defaultValue: false } ]; const commandLineArgs = require('command-line-args'); const options = commandLineArgs(optionDefinitions); -automationGenerateInPipeline(options.inputJsonPath, options.outputJsonPath, options.use, options.typespecEmitter, options.sdkGenerationType).catch(e => { +automationGenerateInPipeline(options.inputJsonPath, options.outputJsonPath, options.use, options.typespecEmitter, options.sdkGenerationType, options.local ?? false).catch(e => { logger.error(e.message); process.exit(1); }); diff --git a/tools/js-sdk-release-tools/src/common/ciYamlTemplates/ci.mgmt.template.yml b/tools/js-sdk-release-tools/src/common/ciYamlTemplates/ci.mgmt.template.yml new file mode 100644 index 00000000000..a16668078ff --- /dev/null +++ b/tools/js-sdk-release-tools/src/common/ciYamlTemplates/ci.mgmt.template.yml @@ -0,0 +1,33 @@ +# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file. + +trigger: + branches: + include: + - main + - feature/* + - release/* + - hotfix/* + exclude: + - feature/v4 + paths: + include: null + +pr: + branches: + include: + - main + - feature/* + - release/* + - hotfix/* + exclude: + - feature/v4 + paths: + include: null + +extends: + template: /eng/pipelines/templates/stages/archetype-sdk-client.yml + parameters: + ServiceDirectory: null + Artifacts: + - name: null + safeName: null diff --git a/tools/js-sdk-release-tools/src/common/ciYamlUtils.ts b/tools/js-sdk-release-tools/src/common/ciYamlUtils.ts new file mode 100644 index 00000000000..58166243815 --- /dev/null +++ b/tools/js-sdk-release-tools/src/common/ciYamlUtils.ts @@ -0,0 +1,151 @@ +import { NpmPackageInfo, VersionPolicyName } from './types'; +import { basename, join, posix, resolve } from 'path'; +import { getNpmPackageName, getNpmPackageSafeName } from './npmUtils'; +import { parse, stringify } from 'yaml'; +import { readFile, writeFile } from 'fs/promises'; + +import { existsAsync } from './utils'; +import { logger } from '../utils/logger'; + +interface ArtifactInfo { + name: string; + safeName: string; +} + +const comment = '# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file.\n\n'; + +async function createOrUpdateManagePlaneCiYaml( + packageDirToSdkRoot: string, + npmPackageInfo: NpmPackageInfo +): Promise { + const serviceDirToSDKDir = posix.join(packageDirToSdkRoot, '..'); + const ciMgmtPath = posix.join(serviceDirToSDKDir, 'ci.mgmt.yml'); + + if (!(await existsAsync(ciMgmtPath))) { + await createManagementPlaneCiYaml( + packageDirToSdkRoot, + ciMgmtPath, + serviceDirToSDKDir, + npmPackageInfo + ); + return ciMgmtPath; + } + await updateManagementPlaneCiYaml(packageDirToSdkRoot, ciMgmtPath, npmPackageInfo); + return ciMgmtPath; +} + +function tryAddItemInArray( + array: TItem[], + item: TItem, + include: (array: TItem[], item: TItem) => boolean = (a, i) => a.includes(i) +): boolean { + let needUpdate = false; + if (include(array, item) !== true) { + needUpdate = true; + array.push(item); + } + return needUpdate; +} + +function makeSureArrayAvailableInCiYaml(current: any, path: string[]) { + path.forEach((p, i) => { + if (!current?.[p]) { + current[p] = i === path.length - 1 ? [] : {}; + } + current = current[p]; + }); +} + +async function updateManagementPlaneCiYaml( + generatedPackageDirectory: string, + ciMgmtPath: string, + npmPackageInfo: NpmPackageInfo +): Promise { + const content = await readFile(ciMgmtPath, { encoding: 'utf-8' }); + let parsed = parse(content.toString()); + + makeSureArrayAvailableInCiYaml(parsed, ['trigger', 'branches', 'exclude']); + makeSureArrayAvailableInCiYaml(parsed, ['pr', 'branches', 'exclude']); + makeSureArrayAvailableInCiYaml(parsed, ['trigger', 'paths', 'include']); + makeSureArrayAvailableInCiYaml(parsed, ['pr', 'paths', 'include']); + makeSureArrayAvailableInCiYaml(parsed, ['extends', 'parameters', 'Artifacts']); + + var artifact: ArtifactInfo = getArtifact(npmPackageInfo); + var artifactInclude = (array: ArtifactInfo[], item: ArtifactInfo) => array.map((a) => a.name).includes(item.name); + + let needUpdate = false; + needUpdate = tryAddItemInArray(parsed.trigger.branches.exclude, 'feature/v4') || needUpdate; + needUpdate = tryAddItemInArray(parsed.pr.branches.exclude, 'feature/v4') || needUpdate; + needUpdate = tryAddItemInArray(parsed.trigger.paths.include, generatedPackageDirectory) || needUpdate; + needUpdate = tryAddItemInArray(parsed.trigger.paths.include, ciMgmtPath) || needUpdate; + needUpdate = tryAddItemInArray(parsed.pr.paths.include, generatedPackageDirectory) || needUpdate; + needUpdate = tryAddItemInArray(parsed.pr.paths.include, ciMgmtPath) || needUpdate; + needUpdate = tryAddItemInArray(parsed.extends.parameters.Artifacts, artifact, artifactInclude) || needUpdate; + + writeCiYaml(ciMgmtPath, parsed); +} + +function getArtifact(npmPackageInfo: NpmPackageInfo): ArtifactInfo { + const name = getNpmPackageName(npmPackageInfo); + const safeName = getNpmPackageSafeName(npmPackageInfo); + return { name, safeName }; +} + +async function createManagementPlaneCiYaml( + packageDirToSdkRoot: string, + ciMgmtPath: string, + serviceDirToSdkRoot: string, + npmPackageInfo: NpmPackageInfo +): Promise { + const artifact = getArtifact(npmPackageInfo); + const templatePath = join(__dirname, 'ciYamlTemplates/ci.mgmt.template.yml'); + const template = await readFile(templatePath, { encoding: 'utf-8' }); + const parsed = parse(template.toString()); + parsed.trigger.paths.include = [packageDirToSdkRoot, ciMgmtPath]; + parsed.pr.paths.include = [packageDirToSdkRoot, ciMgmtPath]; + parsed.extends.parameters.ServiceDirectory = serviceDirToSdkRoot; + parsed.extends.parameters.Artifacts = [artifact]; + + await writeCiYaml(ciMgmtPath, parsed); +} + +async function writeCiYaml(ciMgmtPath: string, config: any) { + const content = comment + stringify(config); + await writeFile(ciMgmtPath, content, { encoding: 'utf-8', flush: true }); + logger.info(`Created Management CI file '${resolve(ciMgmtPath)}' with content: \n${content}`); +} + +async function createOrUpdateDataPlaneCiYaml( + generatedPackageDirectory: string, + npmPackageInfo: NpmPackageInfo +): Promise { + throw new Error('Not implemented function'); +} + +export async function createOrUpdateCiYaml( + relativeGeneratedPackageDirectoryToSdkRoot: string, + versionPolicyName: VersionPolicyName, + npmPackageInfo: NpmPackageInfo +): Promise { + logger.info('Start to create or update CI files'); + switch (versionPolicyName) { + case 'management': { + const ciPath = await createOrUpdateManagePlaneCiYaml( + relativeGeneratedPackageDirectoryToSdkRoot, + npmPackageInfo + ); + logger.info('Created or updated MPG CI files successfully.'); + return ciPath; + } + case 'client': { + const ciPath = await createOrUpdateDataPlaneCiYaml( + relativeGeneratedPackageDirectoryToSdkRoot, + npmPackageInfo + ); + logger.info('Created or updated DPG CI files successfully.'); + return ciPath; + } + default: + throw new Error(`Unsupported version policy name: ${versionPolicyName}`); + } +} diff --git a/tools/js-sdk-release-tools/src/common/npmUtils.ts b/tools/js-sdk-release-tools/src/common/npmUtils.ts new file mode 100644 index 00000000000..92f0c94c547 --- /dev/null +++ b/tools/js-sdk-release-tools/src/common/npmUtils.ts @@ -0,0 +1,31 @@ +import { load } from '@npmcli/package-json'; +import { NpmPackageInfo } from './types'; + +export async function getNpmPackageInfo(packageDirectory): Promise { + const packageJson = await load(packageDirectory); + if (!packageJson.content.name) { + throw new Error(`package.json doesn't contains name property`); + } + if (!packageJson.content.version) { + throw new Error(`package.json doesn't contains version property`); + } + const name = packageJson.content.name; + const version = packageJson.content.version; + return { name, version }; +} + +export function getNpmPackageName(info: NpmPackageInfo) { + return info.name.replace('@azure/', 'azure-'); +} + +export function getNpmPackageSafeName(info: NpmPackageInfo) { + const name = getNpmPackageName(info); + const safeName = name.replace(/-/g, ''); + return safeName; +} + +export function getArtifactName(info: NpmPackageInfo) { + const name = getNpmPackageName(info); + const version = info.version; + return `${name}-${version}.tgz`; +} diff --git a/tools/js-sdk-release-tools/src/common/packageResultUtils.ts b/tools/js-sdk-release-tools/src/common/packageResultUtils.ts new file mode 100644 index 00000000000..e4c5216dd6b --- /dev/null +++ b/tools/js-sdk-release-tools/src/common/packageResultUtils.ts @@ -0,0 +1,46 @@ +import { ChangelogResult, NpmPackageInfo, PackageResult } from './types'; + +import { Changelog } from '../changelog/changelogGenerator'; + +export function initPackageResult(): PackageResult { + const breakingChangeItems = []; + const hasBreakingChange = false; + const content = ''; + const changelogInfo: ChangelogResult = { content, hasBreakingChange, breakingChangeItems }; + const packageInfo: PackageResult = { + // pipeline framework limit, it cannot handle result with empty string + packageName: 'default', + version: '', + language: 'JavaScript', + path: ['rush.json', 'common/config/rush/pnpm-lock.yaml'], + apiViewArtifact: '', + packageFolder: '', + typespecProject: [], + artifacts: [], + changelog: changelogInfo, + result: 'failed' + }; + return packageInfo; +} + +export function updateChangelogResult(packageResult: PackageResult, changelog: Changelog | undefined): void { + packageResult.changelog.breakingChangeItems = changelog?.getBreakingChangeItems() ?? []; + packageResult.changelog.content = changelog?.displayChangeLog() ?? ''; + packageResult.changelog.hasBreakingChange = changelog?.hasBreakingChange ?? false; +} + +// TODO: need a instruction +export function updateInstructionResult(packageResult: PackageResult, instruction: string): void {} + +export function updateNpmPackageResult( + packageResult: PackageResult, + npmPackageInfo: NpmPackageInfo, + relativeTypeSpecDirectoryToSpecRoot: string, + relativeGeneratedPackageDirectoryToSdkRoot: string +): void { + packageResult.packageName = npmPackageInfo.name; + packageResult.version = npmPackageInfo.version; + packageResult.typespecProject = [relativeTypeSpecDirectoryToSpecRoot]; + packageResult.packageFolder = relativeGeneratedPackageDirectoryToSdkRoot; + packageResult.path.push(relativeGeneratedPackageDirectoryToSdkRoot); +} diff --git a/tools/js-sdk-release-tools/src/common/rushUtils.ts b/tools/js-sdk-release-tools/src/common/rushUtils.ts new file mode 100644 index 00000000000..e57d5ac6547 --- /dev/null +++ b/tools/js-sdk-release-tools/src/common/rushUtils.ts @@ -0,0 +1,134 @@ +import { CommentArray, CommentJSONValue, CommentObject, assign, parse, stringify } from 'comment-json'; +import { ModularClientPackageOptions, PackageResult } from './types'; +import { access } from 'node:fs/promises'; +import { basename, join, normalize, posix, relative, resolve } from 'node:path'; +import { ensureDir, readFile, writeFile } from 'fs-extra'; +import { getArtifactName, getNpmPackageInfo } from './npmUtils'; +import { runCommand, runCommandOptions } from './utils'; + +import { glob } from 'glob'; +import { logger } from '../utils/logger'; +import unixify from 'unixify'; + +interface ProjectItem { + packageName: string; + projectFolder: string; + versionPolicyName: string; +} + +async function updateRushJson(projectItem: ProjectItem) { + const content = await readFile('rush.json', { encoding: 'utf-8' }); + const rushJson = parse(content.toString()); + const projects = (rushJson as CommentObject)?.['projects'] as CommentArray; + if (!projects) { + throw new Error('Failed to parse projects in rush.json.'); + } + const isCurrentPackageExist = projects.filter((p) => p?.['packageName'] === projectItem.packageName).length > 0; + if (isCurrentPackageExist) { + logger.info(`'${projectItem.packageName}' exists, no need to update rush.json.`); + return; + } + // add new project and keep comment at the same time + const newProjects = assign(projects, [...projects, projectItem]); + const newRushJson = assign(rushJson, { ...(rushJson as CommentObject), projects: newProjects }); + const newRushJsonContent = stringify(newRushJson, undefined, 2); + writeFile('rush.json', newRushJsonContent, { encoding: 'utf-8', flush: true }); + logger.info('Updated rush.json successfully.'); +} + +async function packPackage(packageDirectory: string, packageName: string, rushxScript: string) { + const cwd = join(packageDirectory); + await runCommand('node', [rushxScript, 'pack'], { ...runCommandOptions, cwd }, false); + logger.info(`Pack '${packageName}' successfully.`); +} + +async function addApiViewInfo( + packageDirectory: string, + sdkRoot: string, + packageResult: PackageResult +): Promise<{ name: string; content: string }> { + const apiViewPathPattern = posix.join(packageDirectory, 'temp', '**/*.api.json'); + const apiViews = await glob(apiViewPathPattern); + if (!apiViews || apiViews.length === 0) throw new Error(`Failed to get API views in '${apiViewPathPattern}'. cwd: ${process.cwd()}`); + if (apiViews && apiViews.length > 1) throw new Error(`Failed to get exactly one API view: ${apiViews}.`); + packageResult.apiViewArtifact = relative(sdkRoot, apiViews[0]); + const content = (await readFile(apiViews[0], { encoding: 'utf-8' })).toString(); + const name = basename(apiViews[0]); + return { content, name }; +} + +export async function buildPackage( + packageDirectory: string, + options: ModularClientPackageOptions, + packageResult: PackageResult, + rushScript: string, + rushxScript: string +) { + const relativePackageDirectoryToSdkRoot = relative(normalize(options.sdkRepoRoot), normalize(packageDirectory)); + logger.info(`Start building package in '${relativePackageDirectoryToSdkRoot}'.`); + + const { name } = await getNpmPackageInfo(relativePackageDirectoryToSdkRoot); + await updateRushJson({ + packageName: name, + projectFolder: unixify(relativePackageDirectoryToSdkRoot), + versionPolicyName: options.versionPolicyName + }); + + logger.info(`Start to rush update.`); + await runCommand(`node`, [rushScript, 'update'], runCommandOptions, false); + logger.info(`Rush update successfully.`); + + logger.info(`Start to build package '${name}'.`); + await runCommand('node', [rushScript, 'build', '-t', name, '--verbose'], runCommandOptions); + const apiViewContext = await addApiViewInfo(packageDirectory, options.sdkRepoRoot, packageResult); + logger.info(`Build package '${name}' successfully.`); + + // build sample and test package will NOT throw exceptions + // note: these commands will delete temp folder + await tryBuildSamples(packageDirectory, rushxScript); + await tryTestPackage(packageDirectory, rushxScript); + + // restore in temp folder + const tempFolder = join(packageDirectory, 'temp'); + await ensureDir(tempFolder); + const apiViewPath = join(tempFolder, apiViewContext.name); + await writeFile(apiViewPath, apiViewContext.content, { encoding: 'utf-8', flush: true }); +} + +// no exception will be thrown, since we don't want it stop sdk generation. sdk author will need to resolve the failure +export async function tryBuildSamples(packageDirectory: string, rushxScript: string) { + logger.info(`Start to build samples in '${packageDirectory}'.`); + const cwd = packageDirectory; + const options = { ...runCommandOptions, cwd }; + try { + await runCommand(`node`, [rushxScript, 'build:samples'], options, true, 300); + logger.info(`built samples successfully.`); + } catch (err) { + logger.error(`Failed to build samples due to: ${(err as Error)?.stack ?? err}`); + } +} + +// no exception will be thrown, since we don't want it stop sdk generation. sdk author will need to resolve the failure +export async function tryTestPackage(packageDirectory: string, rushxScript: string) { + logger.info(`Start to test package in '${packageDirectory}'.`); + const env = { ...process.env, TEST_MODE: 'record' }; + const cwd = join(packageDirectory); + const options = { ...runCommandOptions, env, cwd }; + try { + await runCommand(`node`, [rushxScript, 'test:node'], options, true, 300); + logger.info(`tested package successfully.`); + } catch (err) { + logger.error(`Failed to test package due to: ${(err as Error)?.stack ?? err}`); + } +} + +export async function createArtifact(packageDirectory: string, rushxScript: string): Promise { + logger.info(`Start to create artifact in '${packageDirectory}'`); + const info = await getNpmPackageInfo(packageDirectory); + await packPackage(packageDirectory, info.name, rushxScript); + const artifactName = getArtifactName(info); + const artifactPath = posix.join(packageDirectory, artifactName); + await access(artifactPath); + logger.info(`Created artifact '${info.name}' in '${resolve(artifactPath)}' successfully.`); + return artifactPath; +} diff --git a/tools/js-sdk-release-tools/src/common/types.ts b/tools/js-sdk-release-tools/src/common/types.ts index c1db6dccf6a..24de746aa66 100644 --- a/tools/js-sdk-release-tools/src/common/types.ts +++ b/tools/js-sdk-release-tools/src/common/types.ts @@ -1,11 +1,61 @@ export enum SDKType { HighLevelClient = 'HighLevelClient', RestLevelClient = 'RestLevelClient', - ModularClient = 'ModularClient', -}; + ModularClient = 'ModularClient' +} export enum ApiVersionType { None = 'None', Stable = 'Stable', - Preview = 'Preview', -} \ No newline at end of file + Preview = 'Preview' +} + +export interface ChangelogResult { + content: string; + hasBreakingChange: boolean; + breakingChangeItems: string[]; +} + +export interface InstallInstructionsResult { + full: string; +} + +// TODO: investigate the inconsistency to https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/sdkautomation/GenerateOutputSchema.json +// the PackageResult here is stricter by making some optional field to required, due to we always want the package result contains specific fields +export interface PackageResult { + language: "JavaScript"; + packageName: string; + version: string; + path: string[]; + changelog: ChangelogResult; + artifacts: string[]; + apiViewArtifact: string; + result: 'succeeded' | 'failed' | 'warning'; + packageFolder: string; + typespecProject?: string[]; + readmeMd?: string[]; + installInstructions?: InstallInstructionsResult; +} + +export interface GenerationOutputInfo { + packages: PackageResult[]; +} + +export type VersionPolicyName = 'management' | 'client'; +export type EmitterName = '@azure-tools/typespec-ts' | ''; + +export interface ModularClientPackageOptions { + sdkRepoRoot: string; + specRepoRoot: string; + typeSpecDirectory: string; + gitCommitId: string; + skip: boolean; + repoUrl: string; + versionPolicyName: VersionPolicyName; + local: boolean; +} + +export interface NpmPackageInfo { + name: string; + version: string; +} diff --git a/tools/js-sdk-release-tools/src/common/utils.ts b/tools/js-sdk-release-tools/src/common/utils.ts index ba700b3eb1c..a6a10b4861e 100644 --- a/tools/js-sdk-release-tools/src/common/utils.ts +++ b/tools/js-sdk-release-tools/src/common/utils.ts @@ -1,27 +1,14 @@ import shell from 'shelljs'; import path, { join, posix } from 'path'; import fs from 'fs'; - -import { SDKType } from './types' -import { logger } from "../utils/logger"; +import { SDKType } from './types'; +import { logger } from '../utils/logger'; import { Project, ScriptTarget, SourceFile } from 'ts-morph'; import { replaceAll } from '@ts-common/azure-js-dev-tools'; import { readFile } from 'fs/promises'; import { parse } from 'yaml'; -import { spawn, SpawnOptions } from 'child_process'; - -function printErrorDetails(output: { stdout: string; stderr: string, code: number | null } | undefined) { - if (!output) return; - logger.error(`Summary:`); - const printErrorSummary = (content: string) => content.split('\n') - .filter(line => line.includes('error') || line.includes('ERROR')) - .forEach(line => logger.error(line)); - printErrorSummary(output.stderr); - printErrorSummary(output.stdout); - logger.error(`Details:`); - logger.error(output.stderr); - logger.error(output.stdout); -} +import { access } from 'node:fs/promises'; +import { SpawnOptions, spawn } from 'child_process'; // ./eng/common/scripts/TypeSpec-Project-Process.ps1 script forces to use emitter '@azure-tools/typespec-ts', // so do NOT change the emitter @@ -31,16 +18,50 @@ const emitterName = '@azure-tools/typespec-ts'; const messageToTspConfigSample = 'Please refer to https://github.com/Azure/azure-rest-api-specs/blob/main/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml for the right schema.'; -async function loadTspConfig(typeSpecDirectory: string): Promise> { - const configPath = join(typeSpecDirectory, 'tspconfig.yaml'); - const content = await readFile(configPath, { encoding: 'utf-8' }); - const config = parse(content.toString()); - if (!config) { - throw new Error(`Failed to parse tspconfig.yaml in ${typeSpecDirectory}`); +const errorKeywordsInLowercase = new Set(['error', 'err_pnpm_no_matching_version']); + +function removeLastNewline(line: string): string { + return line.replace(/\n$/, '') +} + +function printErrorDetails( + output: { stdout: string; stderr: string; code: number | null } | undefined, + printDetails: boolean = false +) { + if (!output) return; + const getErrorSummary = (content: string) => + content + .split('\n') + .filter((line) => { + for (const keyword of errorKeywordsInLowercase) { + if (line.toLowerCase().includes(keyword)) return true; + } + return false; + }) + .map((line) => ` ${line}\n`); + let summary = [...getErrorSummary(output.stderr), ...getErrorSummary(output.stdout)]; + logger.error(`Exit code: ${output.code}`); + if (summary.length > 0) { + logger.error(`Summary:`); + summary.forEach((line) => logger.error(removeLastNewline(line))); + } + if (printDetails) { + const stderr = removeLastNewline(output.stderr); + const stdout = removeLastNewline(output.stdout); + logger.error(`Details:`); + if (stderr) { + logger.error(` stderr:`); + stderr.split('\n').forEach((line) => logger.warn(` ${line}`)); + } + if (stdout) { + logger.error(` stdout:`); + stdout.split('\n').forEach((line) => logger.warn(` ${line}`)); + } } - return config; } +export const runCommandOptions: SpawnOptions = { shell: true, stdio: ['pipe', 'pipe', 'pipe'] }; + export function getClassicClientParametersPath(packageRoot: string): string { return path.join(packageRoot, 'src', 'models', 'parameters.ts'); } @@ -70,7 +91,7 @@ export function getApiReviewPath(packageRoot: string): string { switch (sdkType) { case SDKType.ModularClient: const npmPackageName = getNpmPackageName(packageRoot); - const packageName = npmPackageName.substring("@azure/".length); + const packageName = npmPackageName.substring('@azure/'.length); const apiViewFileName = `${packageName}.api.md`; return path.join(packageRoot, 'review', apiViewFileName); case SDKType.HighLevelClient: @@ -92,9 +113,9 @@ export function getTsSourceFile(filePath: string): SourceFile | undefined { // changelog policy: https://aka.ms/azsdk/guideline/changelogs export function fixChangelogFormat(content: string) { content = replaceAll(content, '**Features**', '### Features Added')!; - content = replaceAll(content, '**Breaking Changes**', '### Breaking Changes')!; - content = replaceAll(content, '**Bugs Fixed**', '### Bugs Fixed')!; - content = replaceAll(content, '**Other Changes**', '### Other Changes')!; + content = replaceAll(content, '**Breaking Changes**', '### Breaking Changes')!; + content = replaceAll(content, '**Bugs Fixed**', '### Bugs Fixed')!; + content = replaceAll(content, '**Other Changes**', '### Other Changes')!; return content; } @@ -113,83 +134,112 @@ export function tryReadNpmPackageChangelog(packageFolderPath: string): string { } } +export async function loadTspConfig(typeSpecDirectory: string): Promise> { + const configPath = join(typeSpecDirectory, 'tspconfig.yaml'); + const content = await readFile(configPath, { encoding: 'utf-8' }); + const config = parse(content.toString()); + if (!config) { + throw new Error(`Failed to parse tspconfig.yaml in ${typeSpecDirectory}`); + } + return config; +} + // generated path is in posix format // e.g. sdk/mongocluster/arm-mongocluster -export async function getGeneratedPackageDirectory(typeSpecDirectory: string): Promise { +export async function getGeneratedPackageDirectory(typeSpecDirectory: string, sdkRepoRoot: string): Promise { const tspConfig = await loadTspConfig(typeSpecDirectory); const serviceDir = tspConfig.parameters?.['service-dir']?.default; if (!serviceDir) { - throw new Error(`Misses service-dir in parameters section of tspconfig.yaml. ${messageToTspConfigSample}`); + throw new Error(`Miss service-dir in parameters section of tspconfig.yaml. ${messageToTspConfigSample}`); } const packageDir = tspConfig.options?.[emitterName]?.['package-dir']; if (!packageDir) { - throw new Error(`Misses package-dir in ${emitterName} options of tspconfig.yaml. ${messageToTspConfigSample}`); + throw new Error(`Miss package-dir in ${emitterName} options of tspconfig.yaml. ${messageToTspConfigSample}`); } - const packageDirFromRoot = posix.join(serviceDir, packageDir); + const packageDirFromRoot = posix.join(sdkRepoRoot, serviceDir, packageDir); return packageDirFromRoot; } -export function runCommand( +export async function runCommand( command: string, args: readonly string[], - options: SpawnOptions, + options: SpawnOptions = runCommandOptions, realtimeOutput: boolean = true, - timeoutSeconds: number | undefined = undefined -): Promise<{ stdout: string; stderr: string, code }> { - return new Promise((resolve, reject) => { - let stdout = ''; - let stderr = ''; - const commandStr = `${command} ${args.join(' ')}`; - logger.info(`Start to run command: '${commandStr}'.`); - const child = spawn(command, args, options); - - let timedOut = false; - const timer = timeoutSeconds &&setTimeout(() => { + timeoutSeconds: number | undefined = undefined +): Promise<{ stdout: string; stderr: string; code: number | null }> { + let stdout = ''; + let stderr = ''; + const commandStr = `${command} ${args.join(' ')}`; + logger.info(`Start to run command: '${commandStr}'.`); + const child = spawn(command, args, options); + + let timedOut = false; + const timer = + timeoutSeconds && + setTimeout(() => { timedOut = true; child.kill(); - reject(new Error(`Process timed out after ${timeoutSeconds}s`)); + throw new Error(`Process timed out after ${timeoutSeconds}s`); }, timeoutSeconds * 1000); - - child.stdout?.on('data', (data) => { - const str = data.toString(); - stdout += str; - if (realtimeOutput) logger.info(str); - }); - - child.stderr?.on('data', (data) => { - const str = data.toString(); - stderr += str; - if (realtimeOutput) console.error(str); - }); - - child.on('close', (code) => { - if (code === 0) { - resolve({ stdout, stderr, code }); - } else { - logger.error(`Command closed with code '${code}'.`); - printErrorDetails({ stdout, stderr, code }); - reject(new Error(`Command closed with code '${code}'.`)); - } - }); - - child.on('exit', (code, signal) => { - if (timer) clearTimeout(timer); - if (!timedOut) { - if (signal || code && code !== 0) { - logger.error(`Command '${commandStr}' exited with signal '${signal ?? 'SIGTERM'}' and code ${code}.`); - printErrorDetails({ stdout, stderr, code }); - reject(new Error(`Process was killed with signal '${signal ?? 'SIGTERM'}'.`)); - } else { - resolve({ stdout, stderr, code }); - } - } - }); - - child.on('error', (err) => { - logger.error((err as Error)?.stack ?? err); - printErrorDetails({ stdout, stderr, code: null }); - reject(err); - }); + + child.stdout?.setEncoding('utf8'); + child.stderr?.setEncoding('utf8'); + + child.stdout?.on('data', (data) => { + const str = data.toString(); + stdout += str; + if (realtimeOutput) logger.info(str); + }); + + child.stderr?.on('data', (data) => { + const str = data.toString(); + stderr += str; + if (realtimeOutput) logger.warn(str); + }); + + let resolve: (value: void | PromiseLike) => void; + let reject: (reason?: any) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + let code: number | null = 0; + + child.on('exit', (exitCode, signal) => { + if (timer) clearTimeout(timer); + if (timedOut || !signal) { return; } + logger.error(`Command '${commandStr}' exited with signal '${signal ?? 'SIGTERM'}' and code ${exitCode}.`); + }); + + child.on('close', (exitCode) => { + if (exitCode === 0) { + resolve(); + logger.info(`Command '${commandStr}' closed with code '${exitCode}'.`); + return; + } + code = exitCode; + logger.error(`Command closed with code '${exitCode}'.`); + printErrorDetails({ stdout, stderr, code: exitCode }, !realtimeOutput); + reject(Error(`Command closed with code '${exitCode}'.`)); + }); + + child.on('error', (err) => { + logger.error((err as Error)?.stack ?? err); + printErrorDetails({ stdout, stderr, code: null }, !realtimeOutput); + reject(err); }); + + await promise; + return {stdout, stderr, code}; +} + +export async function existsAsync(path: string): Promise { + try { + await access(path); + return true; + } catch (error) { + logger.warn(`Fail to find ${path} for error: ${error}`); + return false; + } } diff --git a/tools/js-sdk-release-tools/src/hlc/utils/automaticGenerateChangeLogAndBumpVersion.ts b/tools/js-sdk-release-tools/src/hlc/utils/automaticGenerateChangeLogAndBumpVersion.ts index 86fa412200e..9624beace66 100644 --- a/tools/js-sdk-release-tools/src/hlc/utils/automaticGenerateChangeLogAndBumpVersion.ts +++ b/tools/js-sdk-release-tools/src/hlc/utils/automaticGenerateChangeLogAndBumpVersion.ts @@ -90,7 +90,7 @@ export async function generateChangelogAndBumpVersion(packageFolderPath: string) } originalChangeLogContent = fixChangelogFormat(originalChangeLogContent); if (!changelog.hasBreakingChange && !changelog.hasFeature) { - logger.error('Failed to generate changelog because the codes of local and npm may be the same.'); + logger.warn('Failed to generate changelog because the codes of local and npm may be the same.'); logger.info('Start to bump a fix version.'); const oriPackageJson = execSync(`git show HEAD:${path.relative(jsSdkRepoPath, path.join(packageFolderPath, 'package.json')).replace(/\\/g, '/')}`, {encoding: 'utf-8'}); const oriVersion = JSON.parse(oriPackageJson).version; diff --git a/tools/js-sdk-release-tools/src/llc/apiVersion/apiVersionTypeExtractor.ts b/tools/js-sdk-release-tools/src/llc/apiVersion/apiVersionTypeExtractor.ts index b643fa32346..a750270b621 100644 --- a/tools/js-sdk-release-tools/src/llc/apiVersion/apiVersionTypeExtractor.ts +++ b/tools/js-sdk-release-tools/src/llc/apiVersion/apiVersionTypeExtractor.ts @@ -14,7 +14,8 @@ export const getApiVersionType: IApiVersionTypeExtractor = async ( typeFromClient = await getApiVersionTypeFromRestClient(packageRoot, clientPattern, tryFindRestClientPath); if (typeFromClient !== ApiVersionType.None) return typeFromClient; - const typeFromOperations = getApiVersionTypeFromOperations(packageRoot, clientPattern, findParametersPath); + const parametersFolder = "src/"; + const typeFromOperations = getApiVersionTypeFromOperations(packageRoot, parametersFolder, findParametersPath); if (typeFromOperations !== ApiVersionType.None) return typeFromOperations; return ApiVersionType.Stable; }; diff --git a/tools/js-sdk-release-tools/src/llc/generateRLCInPipeline/generateRLCInPipeline.ts b/tools/js-sdk-release-tools/src/llc/generateRLCInPipeline/generateRLCInPipeline.ts index 72d0b39934f..5c051bcbae7 100644 --- a/tools/js-sdk-release-tools/src/llc/generateRLCInPipeline/generateRLCInPipeline.ts +++ b/tools/js-sdk-release-tools/src/llc/generateRLCInPipeline/generateRLCInPipeline.ts @@ -41,11 +41,12 @@ export async function generateRLCInPipeline(options: { let relativePackagePath: string | undefined = undefined; if (options.typespecProject) { const typespecProject = path.join(options.swaggerRepo, options.typespecProject); - const generatedPackageDir = await getGeneratedPackageDirectory(typespecProject); + const generatedPackageDir = await getGeneratedPackageDirectory(typespecProject, options.sdkRepo); await remove(generatedPackageDir); if (!options.skipGeneration) { - logger.info(`Start to generate SDK from '${options.typespecProject}'.`); + logger.info(`Start to generate rest level client SDK from '${options.typespecProject}'.`); + // TODO: remove it, since this function is used in pipeline. if(options.sdkGenerationType === "command") { logger.info("Start to run TypeSpec command directly."); const copyPackageJsonName = 'emitter-package.json'; @@ -191,10 +192,14 @@ export async function generateRLCInPipeline(options: { const outputPackageInfo = getOutputPackageInfo(options.runningEnvironment, options.readmeMd, options.typespecProject); try { + // TODO: need to refactor + // too tricky here, when relativePackagePath === undefined, + // the project should be typespec, + // and the changedPackageDirectories should be join(service-dir, package-dir) if (!packagePath || !relativePackagePath) { const changedPackageDirectories: Set = await getChangedPackageDirectory(!options.skipGeneration); if (changedPackageDirectories.size !== 1) { - throw new Error(`Find unexpected changed package directory. Length: ${changedPackageDirectories}. Value: ${[...changedPackageDirectories].join(', ')}. Please only change files in one directory`) + throw new Error(`Find unexpected changed package directory. Length: ${changedPackageDirectories.size}. Value: ${[...changedPackageDirectories].join(', ')}. Please only change files in one directory`) } for (const d of changedPackageDirectories) relativePackagePath = d; packagePath = path.join(options.sdkRepo, relativePackagePath!); @@ -212,6 +217,7 @@ export async function generateRLCInPipeline(options: { await changeRushJson(options.sdkRepo, packageName, getRelativePackagePath(packagePath), 'client'); + // TODO: remove it for typespec project, since no need now, the test and sample are decouple from build // change configuration to skip build test, sample changeConfigOfTestAndSample(packagePath, ChangeModel.Change, SdkType.Rlc); } @@ -232,7 +238,7 @@ export async function generateRLCInPipeline(options: { execSync('node common/scripts/install-run-rush.js update', {stdio: 'inherit'}); logger.info(`Start to build '${packageName}', except for tests and samples, which may be written manually.`); // To build generated codes except test and sample, we need to change tsconfig.json. - execSync(`node common/scripts/install-run-rush.js build -t ${packageName}`, {stdio: 'inherit'}); + execSync(`node common/scripts/install-run-rush.js build -t ${packageName} --verbose`, {stdio: 'inherit'}); logger.info(`Start to run command 'node common/scripts/install-run-rush.js pack --to ${packageName} --verbose'.`); execSync(`node common/scripts/install-run-rush.js pack --to ${packageName} --verbose`, {stdio: 'inherit'}); if (!options.skipGeneration) { @@ -258,6 +264,7 @@ export async function generateRLCInPipeline(options: { if (outputPackageInfo) { outputPackageInfo.result = 'failed'; } + throw e; } finally { if (options.outputJson && outputPackageInfo) { options.outputJson.packages.push(outputPackageInfo); diff --git a/tools/js-sdk-release-tools/src/llc/utils/generateChangelog.ts b/tools/js-sdk-release-tools/src/llc/utils/generateChangelog.ts index e22ab6d39ca..95cc09537ed 100644 --- a/tools/js-sdk-release-tools/src/llc/utils/generateChangelog.ts +++ b/tools/js-sdk-release-tools/src/llc/utils/generateChangelog.ts @@ -80,6 +80,7 @@ export async function generateChangelog(packagePath) { } catch (e: any) { logger.error(`Failed to generate changelog: ${e.message}.`); + throw e; } finally { fs.rmSync(path.join(packagePath, 'changelog-temp'), { recursive: true, force: true }); await shell.cd(jsSdkRepoPath); diff --git a/tools/js-sdk-release-tools/src/llc/utils/utils.ts b/tools/js-sdk-release-tools/src/llc/utils/utils.ts index a0aa3ad22a8..183a4cfa1a3 100644 --- a/tools/js-sdk-release-tools/src/llc/utils/utils.ts +++ b/tools/js-sdk-release-tools/src/llc/utils/utils.ts @@ -64,7 +64,7 @@ export async function getLatestCodegen(packagePath) { } export function getRelativePackagePath(packagePath) { - const match = /.*[\/\\](sdk[\/\\][a-zA-Z0-9-]+[\/\\][a-zA-Z0-9-]+)/.exec(packagePath); + const match = /.*[\/\\](sdk[\/\\][a-zA-Z0-9-.]+[\/\\][a-zA-Z0-9-]+)/.exec(packagePath); if (!!match && match.length == 2) { return match[1].replace(/\\/g, '/'); } else { diff --git a/tools/js-sdk-release-tools/src/mlc/apiVersion/apiVersionTypeExtractor.ts b/tools/js-sdk-release-tools/src/mlc/apiVersion/apiVersionTypeExtractor.ts index 591c0f70e5b..90023dd8325 100644 --- a/tools/js-sdk-release-tools/src/mlc/apiVersion/apiVersionTypeExtractor.ts +++ b/tools/js-sdk-release-tools/src/mlc/apiVersion/apiVersionTypeExtractor.ts @@ -13,7 +13,8 @@ export const getApiVersionType: IApiVersionTypeExtractor = async ( typeFromClient = await getApiVersionTypeFromRestClient(packageRoot, clientPattern, tryFindRestClientPath); if (typeFromClient !== ApiVersionType.None) return typeFromClient; - const typeFromOperations = getApiVersionTypeFromOperations(packageRoot, clientPattern, findParametersPath); + const parametersFolder = "src/rest"; + const typeFromOperations = getApiVersionTypeFromOperations(packageRoot, parametersFolder, findParametersPath); if (typeFromOperations !== ApiVersionType.None) return typeFromOperations; return ApiVersionType.Stable; }; diff --git a/tools/js-sdk-release-tools/src/mlc/changlog/generateChangelog.ts b/tools/js-sdk-release-tools/src/mlc/changlog/generateChangelog.ts new file mode 100644 index 00000000000..7103d345176 --- /dev/null +++ b/tools/js-sdk-release-tools/src/mlc/changlog/generateChangelog.ts @@ -0,0 +1,14 @@ +import { Changelog } from '../../changelog/changelogGenerator'; +import { generateChangelogAndBumpVersion as generateChangelogAndBumpVersionBase } from '../../hlc/utils/automaticGenerateChangeLogAndBumpVersion'; +import { logger } from '../../utils/logger'; + +// TODO: reuse HLC's changelog's generator for now, add api layer changelog generation later +// TODO: consider decouple version bump and changelog generation +// TODO: version bump should reuse version bumper in azure-sdk-for-js +// TODO: when there's no breaking changes and new features, generateChangelogAndBumpVersion return undefined, which causes output json contains empty changelog content. looks like it doesn't impact review flow. keep this logic for now. +export async function generateChangelogAndBumpVersion(packageDirectory: string): Promise { + logger.info(`Start to generate changelog in ${packageDirectory}`); + const changelog = await generateChangelogAndBumpVersionBase(packageDirectory); + logger.info(`Generated changelog successfully.`); + return changelog; +} diff --git a/tools/js-sdk-release-tools/src/mlc/clientGenerator/modularClientPackageGenerator.ts b/tools/js-sdk-release-tools/src/mlc/clientGenerator/modularClientPackageGenerator.ts new file mode 100644 index 00000000000..8368d5a5ac5 --- /dev/null +++ b/tools/js-sdk-release-tools/src/mlc/clientGenerator/modularClientPackageGenerator.ts @@ -0,0 +1,80 @@ +import { ModularClientPackageOptions, NpmPackageInfo, PackageResult } from '../../common/types'; +import { buildPackage, createArtifact } from '../../common/rushUtils'; +import { initPackageResult, updateChangelogResult, updateNpmPackageResult } from '../../common/packageResultUtils'; +import { join, normalize, posix, relative } from 'node:path'; + +import { createOrUpdateCiYaml } from '../../common/ciYamlUtils'; +import { generateChangelogAndBumpVersion } from '../changlog/generateChangelog'; +import { generateTypeScriptCodeFromTypeSpec } from './utils/typeSpecUtils'; +import { getGeneratedPackageDirectory } from '../../common/utils'; +import { getNpmPackageInfo } from '../../common/npmUtils'; +import { logger } from '../../utils/logger'; +import { exists, remove } from 'fs-extra'; +import unixify from 'unixify'; + +// !!!IMPORTANT: +// this function should be used ONLY in +// 1. the CodeGen pipeline of azure-rest-api-specs pull request for generating packages in azure-sdk-for-js +// 2. in the root directory of azure-sdk-for-js repo +// it has extra steps to generate a releasable azure sdk package (no modular client's doc for now, use RLC's for now) after typescript code is generate: +// https://github.com/Azure/azure-sdk-for-js/blob/main/documentation/steps-after-generations.md +export async function generateAzureSDKPackage(options: ModularClientPackageOptions): Promise { + logger.info(`Start to generate modular client package for azure-sdk-for-js.`); + const packageResult = initPackageResult(); + const rushScript = join(options.sdkRepoRoot, 'common/scripts/install-run-rush.js'); + const rushxScript = join(options.sdkRepoRoot, 'common/scripts/install-run-rushx.js'); + + try { + const packageDirectory = await getGeneratedPackageDirectory(options.typeSpecDirectory, options.sdkRepoRoot); + const packageJsonPath = join(packageDirectory, 'package.json'); + let originalNpmPackageInfo: undefined | NpmPackageInfo; + if (await exists(packageJsonPath)) originalNpmPackageInfo = await getNpmPackageInfo(packageDirectory); + + await remove(packageDirectory); + + await generateTypeScriptCodeFromTypeSpec(options, originalNpmPackageInfo?.version, packageDirectory); + const relativePackageDirToSdkRoot = relative(normalize(options.sdkRepoRoot), normalize(packageDirectory)); + + await buildPackage(packageDirectory, options, packageResult, rushScript, rushxScript); + + // changelog generation will compute package version and bump it in package.json, + // so changelog generation should be put before any task needs package.json's version, + // TODO: consider to decouple version bump and changelog generation + // TODO: to be compatible with current tool, input relative generated package dir + const changelog = await generateChangelogAndBumpVersion(relativePackageDirToSdkRoot); + updateChangelogResult(packageResult, changelog); + + const npmPackageInfo = await getNpmPackageInfo(packageDirectory); + const relativeTypeSpecDirToSpecRoot = posix.relative( + unixify(options.specRepoRoot), + unixify(options.typeSpecDirectory) + ); + updateNpmPackageResult( + packageResult, + npmPackageInfo, + relativeTypeSpecDirToSpecRoot, + relativePackageDirToSdkRoot + ); + + const artifactPath = await createArtifact(packageDirectory, rushxScript); + const relativeArtifactPath = posix.relative(unixify(options.sdkRepoRoot), unixify(artifactPath)); + packageResult.artifacts.push(relativeArtifactPath); + + const ciYamlPath = await createOrUpdateCiYaml( + relativePackageDirToSdkRoot, + options.versionPolicyName, + npmPackageInfo + ); + packageResult.path.push(ciYamlPath); + + packageResult.result = 'succeeded'; + logger.info(`Generated package successfully.`); + logger.info(`Package summary: ${JSON.stringify(packageResult, undefined, 2)}`); + } catch (err) { + packageResult.result = 'failed'; + logger.error(`Failed to generate package due to ${(err as Error)?.stack ?? err}`); + throw err; + } finally { + return packageResult; + } +} diff --git a/tools/js-sdk-release-tools/src/mlc/clientGenerator/utils/typeSpecUtils.ts b/tools/js-sdk-release-tools/src/mlc/clientGenerator/utils/typeSpecUtils.ts new file mode 100644 index 00000000000..0dbaaf00821 --- /dev/null +++ b/tools/js-sdk-release-tools/src/mlc/clientGenerator/utils/typeSpecUtils.ts @@ -0,0 +1,40 @@ +import { join } from 'path'; +import { ModularClientPackageOptions } from '../../../common/types'; +import { getGeneratedPackageDirectory, runCommand, runCommandOptions } from '../../../common/utils'; +import { logger } from '../../../utils/logger'; +import { load } from '@npmcli/package-json'; + +export async function updatePackageVersion(packageDirectory: string, version: string): Promise { + const packageJson = await load(packageDirectory); + packageJson.content.version = version; + packageJson.save(); +} + +export async function generateTypeScriptCodeFromTypeSpec( + options: ModularClientPackageOptions, + originalVersion: string | undefined, + packageDirectory: string +): Promise { + const tspConfigPath = join(options.typeSpecDirectory, 'tspconfig.yaml'); + logger.info('Start to generate code by tsp-client.'); + await runCommand( + 'tsp-client', + [ + 'init', + '--debug', + '--tsp-config', + tspConfigPath, + '--local-spec-repo', + options.typeSpecDirectory, + '--repo', + options.specRepoRoot, + '--commit', + options.gitCommitId + ], + { shell: true, stdio: 'inherit' }, + false + ); + + if (originalVersion) await updatePackageVersion(packageDirectory, originalVersion); + logger.info(`Generated typescript code successfully.`); +} diff --git a/tools/js-sdk-release-tools/src/test/apiVersion/apiVersionExtractor.test.ts b/tools/js-sdk-release-tools/src/test/apiVersion/apiVersionExtractor.test.ts index bac8f4dfa52..6a43e45fd80 100644 --- a/tools/js-sdk-release-tools/src/test/apiVersion/apiVersionExtractor.test.ts +++ b/tools/js-sdk-release-tools/src/test/apiVersion/apiVersionExtractor.test.ts @@ -1,72 +1,71 @@ -import { describe, expect, test } from "vitest"; -import { getApiVersionType } from "../../mlc/apiVersion/apiVersionTypeExtractor"; -import { getApiVersionType as getApiVersionTypeInRLC } from "../../llc/apiVersion/apiVersionTypeExtractor"; -import { join } from "path"; -import { ApiVersionType } from "../../common/types"; -import { findApiVersionInRestClient } from "../../xlc/apiVersion/utils"; +import { describe, expect, test } from 'vitest'; +import { getApiVersionType } from '../../mlc/apiVersion/apiVersionTypeExtractor'; +import { getApiVersionType as getApiVersionTypeInRLC } from '../../llc/apiVersion/apiVersionTypeExtractor'; +import { join } from 'path'; +import { ApiVersionType } from '../../common/types'; +import { tryFindApiVersionInRestClient } from '../../xlc/apiVersion/utils'; -test("MLC api-version Extractor: new createClient function", async () => { - const clientPath = join(__dirname, 'testCases/new/src/rest/newClient.ts'); - const version = findApiVersionInRestClient(clientPath); - expect(version).toBe('2024-03-01-preview'); -}); +describe('Modular client api-version Extractor', () => { + test('new createClient function', async () => { + const clientPath = join(__dirname, 'testCases/new/src/rest/newClient.ts'); + const version = tryFindApiVersionInRestClient(clientPath); + expect(version).toBe('2024-03-01-preview'); + }); -test("MLC api-version Extractor: get api version type from new createClient function", async () => { - const root = join(__dirname, 'testCases/new/'); - const version = await getApiVersionType(root); - expect(version).toBe(ApiVersionType.Preview); -}); + test('get api version type from new createClient function', async () => { + const root = join(__dirname, 'testCases/new/'); + const version = await getApiVersionType(root); + expect(version).toBe(ApiVersionType.Preview); + }); -test("MLC api-version Extractor: old createClient function 1", async () => { - const clientPath = join(__dirname, 'testCases/old1/src/rest/oldClient.ts'); - const version = findApiVersionInRestClient(clientPath); - expect(version).toBe('v1.1-preview.1'); -}); + test('old createClient function 1', async () => { + const clientPath = join(__dirname, 'testCases/old1/src/rest/oldClient.ts'); + const version = tryFindApiVersionInRestClient(clientPath); + expect(version).toBe('v1.1-preview.1'); + }); -test("MLC api-version Extractor: get api version type from old createClient function 1", async () => { - const root = join(__dirname, 'testCases/old1/'); - const version = await getApiVersionType(root); - expect(version).toBe(ApiVersionType.Preview); -}); + test('get api version type from old createClient function 1', async () => { + const root = join(__dirname, 'testCases/old1/'); + const version = await getApiVersionType(root); + expect(version).toBe(ApiVersionType.Preview); + }); -test("MLC api-version Extractor: old createClient function 2", async () => { - const clientPath = join(__dirname, 'testCases/old2/src/rest/oldClient.ts'); - const version = findApiVersionInRestClient(clientPath); - expect(version).toBe('2024-03-01'); -}); + test('old createClient function 2', async () => { + const clientPath = join(__dirname, 'testCases/old2/src/rest/oldClient.ts'); + const version = tryFindApiVersionInRestClient(clientPath); + expect(version).toBe('2024-03-01'); + }); -test("MLC api-version Extractor: get api version type from old createClient function 2", async () => { - const root = join(__dirname, 'testCases/old2/'); - const version = await getApiVersionType(root); - expect(version).toBe(ApiVersionType.Stable); + test('get api version type from old createClient function 2', async () => { + const root = join(__dirname, 'testCases/old2/'); + const version = await getApiVersionType(root); + expect(version).toBe(ApiVersionType.Stable); + }); }); -describe("Rest client file fallbacks", () => { - // Modular - { - test("Modular: src/api/xxxContext.ts exists", async () => { +describe('Rest client file fallbacks', () => { + describe('Modular client', () => { + test('src/api/xxxContext.ts exists', async () => { const root = join(__dirname, 'testCases/new-context/'); const version = await getApiVersionType(root); expect(version).toBe(ApiVersionType.Preview); }); - test("Modular: src/api/xxxContext.ts doesn't exists, fallback to src/rest/xxxClient.ts", async () => { + test("src/api/xxxContext.ts doesn't exists, fallback to src/rest/xxxClient.ts", async () => { const root = join(__dirname, 'testCases/new/'); const version = await getApiVersionType(root); expect(version).toBe(ApiVersionType.Preview); }); - } - // RLC - { - test("RLC: src/xxxContext.ts exists", async () => { + }); + describe('RLC', () => { + test('src/xxxContext.ts exists', async () => { const root = join(__dirname, 'testCases/rlc-context/'); const version = await getApiVersionTypeInRLC(root); expect(version).toBe(ApiVersionType.Preview); - }); - test("RLC: src/xxxContext.ts doesn't exists, fallback to src/xxxClient.ts", async () => { + test("src/xxxContext.ts doesn't exists, fallback to src/xxxClient.ts", async () => { const root = join(__dirname, 'testCases/rlc-client/'); const version = await getApiVersionTypeInRLC(root); expect(version).toBe(ApiVersionType.Preview); }); - } -}); \ No newline at end of file + }); +}); diff --git a/tools/js-sdk-release-tools/src/test/apiVersion/testCases/new-context/src/api/newContext.ts b/tools/js-sdk-release-tools/src/test/apiVersion/testCases/new-context/src/api/newContext.ts deleted file mode 100644 index 83492bbb81b..00000000000 --- a/tools/js-sdk-release-tools/src/test/apiVersion/testCases/new-context/src/api/newContext.ts +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { getClient, ClientOptions } from "@azure-rest/core-client"; -import { logger } from "../logger.js"; -import { TokenCredential } from "@azure/core-auth"; -import { DocumentDBContext } from "./clientDefinitions.js"; - -/** The optional parameters for the client */ -export interface DocumentDBContextOptions extends ClientOptions { - /** The api version option of the client */ - apiVersion?: string; -} - -/** - * Initialize a new instance of `DocumentDBContext` - * @param credentials - uniquely identify client credential - * @param options - the parameter for all optional parameters - */ -export default function createClient( - credentials: TokenCredential, - { - apiVersion = "2024-03-01-preview", - ...options - }: DocumentDBContextOptions = {}, -): DocumentDBContext { - const endpointUrl = - options.endpoint ?? options.baseUrl ?? `https://management.azure.com`; - const userAgentInfo = `azsdk-js-arm-mongocluster/1.0.0-beta.1`; - const userAgentPrefix = - options.userAgentOptions && options.userAgentOptions.userAgentPrefix - ? `${options.userAgentOptions.userAgentPrefix} ${userAgentInfo}` - : `${userAgentInfo}`; - options = { - ...options, - userAgentOptions: { - userAgentPrefix, - }, - loggingOptions: { - logger: options.loggingOptions?.logger ?? logger.info, - }, - credentials: { - scopes: options.credentials?.scopes ?? [`${endpointUrl}/.default`], - }, - }; - const client = getClient( - endpointUrl, - credentials, - options, - ) as DocumentDBContext; - - client.pipeline.removePolicy({ name: "ApiVersionPolicy" }); - client.pipeline.addPolicy({ - name: "ClientApiVersionPolicy", - sendRequest: (req, next) => { - // Use the apiVersion defined in request url directly - // Append one if there is no apiVersion and we have one at client options - const url = new URL(req.url); - if (!url.searchParams.get("api-version") && apiVersion) { - req.url = `${req.url}${Array.from(url.searchParams.keys()).length > 0 ? "&" : "?" - }api-version=${apiVersion}`; - } - - return next(req); - }, - }); - return client; -} diff --git a/tools/js-sdk-release-tools/src/test/apiVersion/testCases/new-context/src/api/openAIContext.ts b/tools/js-sdk-release-tools/src/test/apiVersion/testCases/new-context/src/api/openAIContext.ts new file mode 100644 index 00000000000..6d62fa3db01 --- /dev/null +++ b/tools/js-sdk-release-tools/src/test/apiVersion/testCases/new-context/src/api/openAIContext.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { TokenCredential, KeyCredential } from "@azure/core-auth"; +import { ClientOptions, Client, getClient } from "@azure-rest/core-client"; +import { logger } from "../logger.js"; + +export interface OpenAIContext extends Client {} + +/** Optional parameters for the client. */ +export interface OpenAIClientOptionalParams extends ClientOptions { + /** The API version to use for this operation. */ + apiVersion?: string; +} + +export function createOpenAI( + endpointParam: string, + credential: KeyCredential | TokenCredential, + options: OpenAIClientOptionalParams = {}, +): OpenAIContext { + const endpointUrl = + options.endpoint ?? options.baseUrl ?? `${endpointParam}/openai`; + + const prefixFromOptions = options?.userAgentOptions?.userAgentPrefix; + const userAgentPrefix = prefixFromOptions + ? `${prefixFromOptions} azsdk-js-api` + : "azsdk-js-api"; + const { apiVersion: _, ...updatedOptions } = { + ...options, + userAgentOptions: { userAgentPrefix }, + loggingOptions: { logger: options.loggingOptions?.logger ?? logger.info }, + credentials: { + scopes: options.credentials?.scopes ?? [ + "https://cognitiveservices.azure.com/.default", + ], + apiKeyHeaderName: options.credentials?.apiKeyHeaderName ?? "api-key", + }, + }; + const clientContext = getClient(endpointUrl, credential, updatedOptions); + clientContext.pipeline.removePolicy({ name: "ApiVersionPolicy" }); + const apiVersion = options.apiVersion ?? "2024-06-01-preview"; + clientContext.pipeline.addPolicy({ + name: "ClientApiVersionPolicy", + sendRequest: (req, next) => { + // Use the apiVersion defined in request url directly + // Append one if there is no apiVersion and we have one at client options + const url = new URL(req.url); + if (!url.searchParams.get("api-version")) { + req.url = `${req.url}${ + Array.from(url.searchParams.keys()).length > 0 ? "&" : "?" + }api-version=${apiVersion}`; + } + + return next(req); + }, + }); + return clientContext; +} \ No newline at end of file diff --git a/tools/js-sdk-release-tools/src/test/apiVersion/testCases/rlc-context/src/newContext.ts b/tools/js-sdk-release-tools/src/test/apiVersion/testCases/rlc-context/src/newContext.ts deleted file mode 100644 index 83492bbb81b..00000000000 --- a/tools/js-sdk-release-tools/src/test/apiVersion/testCases/rlc-context/src/newContext.ts +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { getClient, ClientOptions } from "@azure-rest/core-client"; -import { logger } from "../logger.js"; -import { TokenCredential } from "@azure/core-auth"; -import { DocumentDBContext } from "./clientDefinitions.js"; - -/** The optional parameters for the client */ -export interface DocumentDBContextOptions extends ClientOptions { - /** The api version option of the client */ - apiVersion?: string; -} - -/** - * Initialize a new instance of `DocumentDBContext` - * @param credentials - uniquely identify client credential - * @param options - the parameter for all optional parameters - */ -export default function createClient( - credentials: TokenCredential, - { - apiVersion = "2024-03-01-preview", - ...options - }: DocumentDBContextOptions = {}, -): DocumentDBContext { - const endpointUrl = - options.endpoint ?? options.baseUrl ?? `https://management.azure.com`; - const userAgentInfo = `azsdk-js-arm-mongocluster/1.0.0-beta.1`; - const userAgentPrefix = - options.userAgentOptions && options.userAgentOptions.userAgentPrefix - ? `${options.userAgentOptions.userAgentPrefix} ${userAgentInfo}` - : `${userAgentInfo}`; - options = { - ...options, - userAgentOptions: { - userAgentPrefix, - }, - loggingOptions: { - logger: options.loggingOptions?.logger ?? logger.info, - }, - credentials: { - scopes: options.credentials?.scopes ?? [`${endpointUrl}/.default`], - }, - }; - const client = getClient( - endpointUrl, - credentials, - options, - ) as DocumentDBContext; - - client.pipeline.removePolicy({ name: "ApiVersionPolicy" }); - client.pipeline.addPolicy({ - name: "ClientApiVersionPolicy", - sendRequest: (req, next) => { - // Use the apiVersion defined in request url directly - // Append one if there is no apiVersion and we have one at client options - const url = new URL(req.url); - if (!url.searchParams.get("api-version") && apiVersion) { - req.url = `${req.url}${Array.from(url.searchParams.keys()).length > 0 ? "&" : "?" - }api-version=${apiVersion}`; - } - - return next(req); - }, - }); - return client; -} diff --git a/tools/js-sdk-release-tools/src/test/apiVersion/testCases/rlc-context/src/openAIContext.ts b/tools/js-sdk-release-tools/src/test/apiVersion/testCases/rlc-context/src/openAIContext.ts new file mode 100644 index 00000000000..6d62fa3db01 --- /dev/null +++ b/tools/js-sdk-release-tools/src/test/apiVersion/testCases/rlc-context/src/openAIContext.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { TokenCredential, KeyCredential } from "@azure/core-auth"; +import { ClientOptions, Client, getClient } from "@azure-rest/core-client"; +import { logger } from "../logger.js"; + +export interface OpenAIContext extends Client {} + +/** Optional parameters for the client. */ +export interface OpenAIClientOptionalParams extends ClientOptions { + /** The API version to use for this operation. */ + apiVersion?: string; +} + +export function createOpenAI( + endpointParam: string, + credential: KeyCredential | TokenCredential, + options: OpenAIClientOptionalParams = {}, +): OpenAIContext { + const endpointUrl = + options.endpoint ?? options.baseUrl ?? `${endpointParam}/openai`; + + const prefixFromOptions = options?.userAgentOptions?.userAgentPrefix; + const userAgentPrefix = prefixFromOptions + ? `${prefixFromOptions} azsdk-js-api` + : "azsdk-js-api"; + const { apiVersion: _, ...updatedOptions } = { + ...options, + userAgentOptions: { userAgentPrefix }, + loggingOptions: { logger: options.loggingOptions?.logger ?? logger.info }, + credentials: { + scopes: options.credentials?.scopes ?? [ + "https://cognitiveservices.azure.com/.default", + ], + apiKeyHeaderName: options.credentials?.apiKeyHeaderName ?? "api-key", + }, + }; + const clientContext = getClient(endpointUrl, credential, updatedOptions); + clientContext.pipeline.removePolicy({ name: "ApiVersionPolicy" }); + const apiVersion = options.apiVersion ?? "2024-06-01-preview"; + clientContext.pipeline.addPolicy({ + name: "ClientApiVersionPolicy", + sendRequest: (req, next) => { + // Use the apiVersion defined in request url directly + // Append one if there is no apiVersion and we have one at client options + const url = new URL(req.url); + if (!url.searchParams.get("api-version")) { + req.url = `${req.url}${ + Array.from(url.searchParams.keys()).length > 0 ? "&" : "?" + }api-version=${apiVersion}`; + } + + return next(req); + }, + }); + return clientContext; +} \ No newline at end of file diff --git a/tools/js-sdk-release-tools/src/test/changelog/changelogGenerator.test.ts b/tools/js-sdk-release-tools/src/test/changelog/changelogGenerator.test.ts index 855c43097cc..66d29d077be 100644 --- a/tools/js-sdk-release-tools/src/test/changelog/changelogGenerator.test.ts +++ b/tools/js-sdk-release-tools/src/test/changelog/changelogGenerator.test.ts @@ -5,10 +5,7 @@ import { SDKType } from "../../common/types"; import { describe } from "node:test"; import { tryReadNpmPackageChangelog } from "../../common/utils"; import { ensureDirSync, removeSync, outputFileSync } from "fs-extra"; - -function getRandomInt(max) { - return Math.floor(Math.random() * max); -} +import { getRandomInt } from "../utils/utils"; describe("Breaking change detection", () => { test("HLC -> Modular: Rename", async () => { diff --git a/tools/js-sdk-release-tools/src/test/command/runCommand.test.ts b/tools/js-sdk-release-tools/src/test/command/runCommand.test.ts new file mode 100644 index 00000000000..162d86f7bb7 --- /dev/null +++ b/tools/js-sdk-release-tools/src/test/command/runCommand.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from "vitest"; +import { runCommand, runCommandOptions } from "../../common/utils"; + +describe('Run command', () => { + test('Invalid command should throw error', async () => { + await expect(runCommand('invalid-command', [], runCommandOptions, false)).rejects.toThrow(); + }); + test('Valid command should not throw error', async () => { + const result = await runCommand('echo 123', [], runCommandOptions, false); + expect(result.stdout.replaceAll('\r', '').replaceAll('\n', '')).toBe('123'); + }); + test('Inherit stdio should not throw error', async () => { + await expect(runCommand('echo 123', [], { shell: true, stdio: 'inherit' }, false)).resolves.not.toThrow(); + }); +}); \ No newline at end of file diff --git a/tools/js-sdk-release-tools/src/test/npm/npm.test.ts b/tools/js-sdk-release-tools/src/test/npm/npm.test.ts new file mode 100644 index 00000000000..ea24f0d8184 --- /dev/null +++ b/tools/js-sdk-release-tools/src/test/npm/npm.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, test } from "vitest"; +import { updatePackageVersion } from "../../mlc/clientGenerator/utils/typeSpecUtils"; +import { join } from "path"; +import { load } from '@npmcli/package-json'; + +describe('Npm package json', () => { + test('Replace package version', async () => { + const packageDirectory = join(__dirname, 'testCases'); + await updatePackageVersion(packageDirectory, '2.0.0'); + const packageJson = await load(packageDirectory); + expect(packageJson.content.version).toBe('2.0.0'); + }); +}); \ No newline at end of file diff --git a/tools/js-sdk-release-tools/src/test/npm/testCases/package.json b/tools/js-sdk-release-tools/src/test/npm/testCases/package.json new file mode 100644 index 00000000000..9abaf3d4024 --- /dev/null +++ b/tools/js-sdk-release-tools/src/test/npm/testCases/package.json @@ -0,0 +1,109 @@ +{ + "name": "@azure/schema-registry-json", + "version": "2.0.0", + "description": "Schema Registry JSON Serializer Library with typescript type definitions for node.js and browser.", + "sdk-type": "client", + "main": "dist/index.js", + "module": "dist-esm/src/index.js", + "types": "types/schema-registry-json.d.ts", + "scripts": { + "audit": "node ../../../common/scripts/rush-audit.js && rimraf node_modules package-lock.json && npm i --package-lock-only 2>&1 && npm audit", + "bundle": "tsc -p . && dev-tool run bundle --polyfill-node false --inject-node-polyfills true --ignore-missing-node-builtins true", + "build:browser": "npm run bundle", + "build:node": "npm run bundle", + "build:samples": "echo Obsolete.", + "build:test": "npm run bundle", + "build": "npm run clean && npm run bundle && dev-tool run extract-api", + "check-format": "dev-tool run vendored prettier --list-different --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"samples-dev/**/*.ts\" \"*.{js,json}\"", + "clean": "rimraf --glob dist dist-* temp types *.tgz *.log", + "execute:samples": "dev-tool samples run samples-dev", + "extract-api": "tsc -p . && dev-tool run extract-api", + "format": "dev-tool run vendored prettier --write --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"samples-dev/**/*.ts\" \"*.{js,json}\"", + "integration-test:browser": "dev-tool run test:browser", + "integration-test:node": "dev-tool run test:node-js-input -- --timeout 5000000 'dist-esm/test/**/*.spec.js'", + "integration-test": "npm run integration-test:node && npm run integration-test:browser", + "lint:fix": "eslint package.json api-extractor.json README.md src test --ext .ts,.javascript,.js --fix --fix-type [problem,suggestion]", + "lint": "eslint package.json api-extractor.json README.md src test --ext .ts,.javascript,.js", + "pack": "npm pack 2>&1", + "test:browser": "npm run build:test && npm run unit-test:browser && npm run integration-test:browser", + "test:node": "npm run build:test && npm run unit-test:node && npm run integration-test:node", + "test": "npm run build:test && npm run unit-test && npm run integration-test", + "unit-test:browser": "dev-tool run test:browser", + "unit-test:node": "dev-tool run test:node-ts-input -- --timeout 1200000 \"test/{,!(browser)/**/}*.spec.ts\"", + "unit-test": "npm run unit-test:node && npm run unit-test:browser" + }, + "files": [ + "dist/", + "dist-esm/src/", + "types/schema-registry-json.d.ts", + "README.md", + "LICENSE" + ], + "repository": "github:Azure/azure-sdk-for-js", + "engines": { + "node": ">=18.0.0" + }, + "keywords": [ + "azure", + "cloud", + "typescript" + ], + "author": "Microsoft Corporation", + "license": "MIT", + "bugs": { + "url": "https://github.com/Azure/azure-sdk-for-js/issues" + }, + "homepage": "https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/schemaregistry/schema-registry-json/", + "sideEffects": false, + "prettier": "@azure/eslint-plugin-azure-sdk/prettier.json", + "//sampleConfiguration": { + "disableDocsMs": true, + "productName": "Azure Schema Registry", + "productSlugs": [ + "azure", + "azure-schema-registry-json" + ], + "requiredResources": { + "Azure Schema Registry resource": "https://aka.ms/schemaregistry" + } + }, + "dependencies": { + "@azure/logger": "^1.0.0", + "@azure/schema-registry": "^1.3.0", + "lru-cache": "^10.2.0", + "tslib": "^2.2.0" + }, + "devDependencies": { + "@azure/core-util": "^1.3.0", + "@azure/dev-tool": "^1.0.0", + "@azure/eslint-plugin-azure-sdk": "^3.0.0", + "@azure/event-hubs": "^5.8.0", + "@azure/identity": "^4.0.1", + "@azure-tools/test-utils": "^1.0.1", + "@azure-tools/test-credential": "^1.0.0", + "@azure-tools/test-recorder": "^3.0.0", + "@microsoft/api-extractor": "^7.31.1", + "@types/mocha": "^10.0.0", + "@types/node": "^18.0.0", + "ajv": "^8.12.0", + "cross-env": "^7.0.2", + "dotenv": "^16.0.0", + "eslint": "^8.0.0", + "karma": "^6.2.0", + "karma-chrome-launcher": "^3.0.0", + "karma-coverage": "^2.0.0", + "karma-env-preprocessor": "^0.1.1", + "karma-firefox-launcher": "^1.1.0", + "karma-json-preprocessor": "^0.3.3", + "karma-json-to-file-reporter": "^1.0.1", + "karma-junit-reporter": "^2.0.1", + "karma-mocha": "^2.0.1", + "karma-mocha-reporter": "^2.2.5", + "karma-sourcemap-loader": "^0.3.8", + "mocha": "^10.0.0", + "nyc": "^17.0.0", + "rimraf": "^5.0.5", + "typescript": "~5.5.3", + "ts-node": "^10.0.0" + } +} diff --git a/tools/js-sdk-release-tools/src/test/pipeline/sdkGeneration.test.ts b/tools/js-sdk-release-tools/src/test/pipeline/sdkGeneration.test.ts new file mode 100644 index 00000000000..76fb3c92e6f --- /dev/null +++ b/tools/js-sdk-release-tools/src/test/pipeline/sdkGeneration.test.ts @@ -0,0 +1,68 @@ +import path from 'path'; +import { describe, expect, test } from 'vitest'; +import { getRandomInt } from '../utils/utils'; +import { ensureDir, remove, writeFile } from 'fs-extra'; +import { stringify } from 'yaml'; +import { SDKType } from '../../common/types'; +import { parseInputJson } from '../../utils/generateInputUtils'; + +describe('Auto SDK generation path test', () => { + test('high level client generation', async () => { + const inputJson = { + relatedReadmeMdFiles: ['zzz/resource-manager/xxx.readme'] + }; + const { sdkType } = await parseInputJson(inputJson); + expect(sdkType).toBe(SDKType.HighLevelClient); + }); + + test('rest level client generation', async () => { + const fakeTspConfig = { + options: { + '@azure-tools/typespec-ts': {} + } + }; + const tempSpecFolder = path.join(__dirname, `tmp/spec-${getRandomInt(10000)}`); + const inputJson = { + relatedTypeSpecProjectFolder: ['tsp'], + specFolder: tempSpecFolder + }; + try { + await ensureDir(path.join(tempSpecFolder, 'tsp')); + + await writeFile(path.join(tempSpecFolder, 'tsp/tspconfig.yaml'), stringify(fakeTspConfig), { + encoding: 'utf8', + flush: true + }); + const { sdkType } = await parseInputJson(inputJson); + expect(sdkType).toBe(SDKType.RestLevelClient); + } finally { + await remove(tempSpecFolder); + } + }); + + test('modular client generation', async () => { + const fakeTspConfig = { + options: { + '@azure-tools/typespec-ts': { + isModularLibrary: true + } + } + }; + const tempSpecFolder = path.join(__dirname, `tmp/spec-${getRandomInt(10000)}`); + const inputJson = { + relatedTypeSpecProjectFolder: ['tsp'], + specFolder: tempSpecFolder + }; + try { + await ensureDir(path.join(tempSpecFolder, 'tsp')); + await writeFile(path.join(tempSpecFolder, 'tsp/tspconfig.yaml'), stringify(fakeTspConfig), { + encoding: 'utf8', + flush: true + }); + const { sdkType } = await parseInputJson(inputJson); + expect(sdkType).toBe(SDKType.ModularClient); + } finally { + await remove(tempSpecFolder); + } + }); +}); diff --git a/tools/js-sdk-release-tools/src/test/utils/utils.ts b/tools/js-sdk-release-tools/src/test/utils/utils.ts new file mode 100644 index 00000000000..b4cb1429a2a --- /dev/null +++ b/tools/js-sdk-release-tools/src/test/utils/utils.ts @@ -0,0 +1,3 @@ +export function getRandomInt(max) { + return Math.floor(Math.random() * max); +} \ No newline at end of file diff --git a/tools/js-sdk-release-tools/src/utils/changeConfigOfTestAndSample.ts b/tools/js-sdk-release-tools/src/utils/changeConfigOfTestAndSample.ts index f6d6da5a93c..3e840670d04 100644 --- a/tools/js-sdk-release-tools/src/utils/changeConfigOfTestAndSample.ts +++ b/tools/js-sdk-release-tools/src/utils/changeConfigOfTestAndSample.ts @@ -42,6 +42,7 @@ export function changeConfigOfTestAndSample(packagePath: string, mode: ChangeMod apiExtractor['mainEntryPointFilePath'] = "./types/index.d.ts"; } } + // TODO: bug? never change the files back, since the it reads the changed files } else if (mode === ChangeModel.Revert) { tsConfig = oriTsConfig; packageJson = JSON.parse(packageJsonFile); diff --git a/tools/js-sdk-release-tools/src/utils/generateInputUtils.ts b/tools/js-sdk-release-tools/src/utils/generateInputUtils.ts new file mode 100644 index 00000000000..3704b44eff2 --- /dev/null +++ b/tools/js-sdk-release-tools/src/utils/generateInputUtils.ts @@ -0,0 +1,90 @@ +import path from "path"; +import { SDKType } from "../common/types"; +import { loadTspConfig } from "../common/utils"; +import { RunningEnvironment } from "./runningEnvironment"; +import { exists } from "fs-extra"; + +async function isManagementPlaneModularClient(specFolder: string, typespecProjectFolder: string[] | string | undefined) { + if (!typespecProjectFolder) { + return false; + } + + if (Array.isArray(typespecProjectFolder) && (typespecProjectFolder as string[]).length !== 1) { + throw new Error(`Unexpected typespecProjectFolder length: ${(typespecProjectFolder as string[]).length} (expect 1)`); + } + + const resolvedRelativeTspFolder = Array.isArray(typespecProjectFolder) ? typespecProjectFolder[0] : typespecProjectFolder as string; + const tspFolderFromSpecRoot = path.join(specFolder, resolvedRelativeTspFolder); + const tspConfigPath = path.join(tspFolderFromSpecRoot, 'tspconfig.yaml'); + if (!(await exists(tspConfigPath))) { + return false; + } + + const tspConfig = await loadTspConfig(tspFolderFromSpecRoot); + if (tspConfig?.options?.['@azure-tools/typespec-ts']?.['isModularLibrary'] !== true) { + return false; + } + return true; +} + +// TODO: consider add stricter rules for RLC in when update SDK automation for RLC +function getSDKType(isMgmtWithHLC: boolean, isMgmtWithModular: boolean) { + if (isMgmtWithHLC) { + return SDKType.HighLevelClient; + } + if (isMgmtWithModular) { + return SDKType.ModularClient; + } + return SDKType.RestLevelClient; +} + +// TODO: generate interface for inputJson +export async function parseInputJson(inputJson: any) { + // inputJson schema: https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/sdkautomation/GenerateInputSchema.json + // todo: add interface for the schema + const specFolder: string = inputJson['specFolder']; + const readmeFiles: string[] | string | undefined = inputJson['relatedReadmeMdFiles'] ? inputJson['relatedReadmeMdFiles'] : inputJson['relatedReadmeMdFile']; + const typespecProjectFolder: string[] | string | undefined = inputJson['relatedTypeSpecProjectFolder']; + const gitCommitId: string = inputJson['headSha']; + const repoHttpsUrl: string = inputJson['repoHttpsUrl']; + const autorestConfig: string | undefined = inputJson['autorestConfig']; + const downloadUrlPrefix: string | undefined = inputJson.installInstructionInput?.downloadUrlPrefix; + // TODO: consider remove it, since it's not defined in inputJson schema + const skipGeneration: boolean | undefined = inputJson['skipGeneration']; + + if (!readmeFiles && !typespecProjectFolder) { + throw new Error(`readme files and typespec project info are both undefined`); + } + + if (typespecProjectFolder && typeof typespecProjectFolder !== 'string' && typespecProjectFolder.length !== 1) { + throw new Error(`get ${typespecProjectFolder.length} typespec project`); + } + + const isTypeSpecProject = !!typespecProjectFolder; + + const packages: any[] = []; + const outputJson = { + packages: packages, + language: 'JavaScript', + }; + const readmeMd = isTypeSpecProject ? undefined : typeof readmeFiles === 'string' ? readmeFiles : readmeFiles![0]; + const typespecProject = isTypeSpecProject ? typeof typespecProjectFolder === 'string' ? typespecProjectFolder : typespecProjectFolder![0] : undefined; + const runningEnvironment = typeof readmeFiles === 'string' || typeof typespecProjectFolder === 'string' ? RunningEnvironment.SdkGeneration : RunningEnvironment.SwaggerSdkAutomation; + + const isMgmtWithHLC = isTypeSpecProject ? false : readmeMd!.includes('resource-manager'); + const isMgmtWithModular = await isManagementPlaneModularClient(specFolder, typespecProjectFolder); + const sdkType = getSDKType(isMgmtWithHLC, isMgmtWithModular); + return { + sdkType, + specFolder, + gitCommitId, + repoHttpsUrl, + autorestConfig, + downloadUrlPrefix, + readmeMd, + outputJson, + skipGeneration, + runningEnvironment, + typespecProject + }; +} \ No newline at end of file diff --git a/tools/js-sdk-release-tools/src/utils/getOutputPackageInfo.ts b/tools/js-sdk-release-tools/src/utils/getOutputPackageInfo.ts index c2f0a7603dc..f12e8cd7de1 100644 --- a/tools/js-sdk-release-tools/src/utils/getOutputPackageInfo.ts +++ b/tools/js-sdk-release-tools/src/utils/getOutputPackageInfo.ts @@ -4,7 +4,8 @@ export function getOutputPackageInfo(runningEnvironment: RunningEnvironment | un let outputPackageInfo: any; if (runningEnvironment === RunningEnvironment.SwaggerSdkAutomation) { outputPackageInfo = { - "packageName": "", + // pipeline framework limit, it cannot handle result with empty string + "packageName": "default", "path": [ 'rush.json', 'common/config/rush/pnpm-lock.yaml' @@ -23,7 +24,8 @@ export function getOutputPackageInfo(runningEnvironment: RunningEnvironment | un } } else if (runningEnvironment === RunningEnvironment.SdkGeneration) { outputPackageInfo = { - "packageName": "", + // pipeline framework limit, it cannot handle result with empty string + "packageName": "default", "path": [ 'rush.json', 'common/config/rush/pnpm-lock.yaml' diff --git a/tools/js-sdk-release-tools/src/xlc/apiVersion/apiVersionTypeExtractor.ts b/tools/js-sdk-release-tools/src/xlc/apiVersion/apiVersionTypeExtractor.ts index 88301468953..4d43d1be233 100644 --- a/tools/js-sdk-release-tools/src/xlc/apiVersion/apiVersionTypeExtractor.ts +++ b/tools/js-sdk-release-tools/src/xlc/apiVersion/apiVersionTypeExtractor.ts @@ -5,7 +5,6 @@ import * as mlcApi from '../../mlc/apiVersion/apiVersionTypeExtractor' import * as hlcApi from '../../hlc/apiVersion/apiVersionTypeExtractor' import * as rlcApi from '../../llc/apiVersion/apiVersionTypeExtractor' -// TODO: move to x-level-client folder export const getApiVersionType: IApiVersionTypeExtractor = async (packageRoot: string): Promise => { const sdkType = getSDKType(packageRoot); switch (sdkType) { @@ -19,4 +18,4 @@ export const getApiVersionType: IApiVersionTypeExtractor = async (packageRoot: s console.warn(`Unsupported SDK type ${sdkType} to get detact api version`); return ApiVersionType.None; } -} \ No newline at end of file +} diff --git a/tools/js-sdk-release-tools/src/xlc/apiVersion/utils.ts b/tools/js-sdk-release-tools/src/xlc/apiVersion/utils.ts index 14e197a92ec..69726f41f3e 100644 --- a/tools/js-sdk-release-tools/src/xlc/apiVersion/utils.ts +++ b/tools/js-sdk-release-tools/src/xlc/apiVersion/utils.ts @@ -1,107 +1,118 @@ -import { readFileSync } from "node:fs"; -import { getTsSourceFile } from "../../common/utils"; -import { ApiVersionType, SDKType } from "../../common/types"; -import ts from "typescript"; -import path from "node:path"; -import shell from "shelljs"; -import { SourceFile, SyntaxKind } from "ts-morph"; -import { logger } from "../../utils/logger"; -import { glob } from 'glob' +import { getTsSourceFile } from '../../common/utils'; +import { ApiVersionType } from '../../common/types'; +import path, { basename } from 'node:path'; +import shell from 'shelljs'; +import { FunctionDeclaration, SourceFile, SyntaxKind } from 'ts-morph'; +import { logger } from '../../utils/logger'; +import { glob } from 'glob'; +import { exists } from 'fs-extra'; var unixify = require('unixify'); -const findApiVersionInRestClientV1 = ( - clientPath: string -): string | undefined => { - const sourceFile = getTsSourceFile(clientPath); - const createClientFunction = sourceFile?.getFunction("createClient"); - if (!createClientFunction) - throw new Error("Function 'createClient' not found."); - - const apiVersionStatements = createClientFunction - .getStatements() - .filter((s) => s.getText().includes("options.apiVersion")); +function tryFindVersionInFunctionBody(func: FunctionDeclaration): string | undefined { + const apiVersionStatements = func.getStatements().filter((s) => s.getText().includes('options.apiVersion')); if (apiVersionStatements.length === 0) { return undefined; } - const text = - apiVersionStatements[apiVersionStatements.length - 1].getText(); + const text = apiVersionStatements[apiVersionStatements.length - 1].getText(); return extractApiVersionFromText(text); -}; +} + +function tryFindFunctionWithApiVersion(clientPath: string, functionName: string): FunctionDeclaration | undefined { + const sourceFile = getTsSourceFile(clientPath); + const createClientFunction = sourceFile?.getFunction(functionName); + return createClientFunction; +} const extractApiVersionFromText = (text: string): string | undefined => { const begin = text.indexOf('"'); const end = text.lastIndexOf('"'); - return text.substring(begin + 1, end); + return text.substring(begin + 1, end); +}; + +const tryFindApiVersionInRestClientV1 = (clientPath: string): string | undefined => { + const createClientFunction = tryFindFunctionWithApiVersion(clientPath, 'createClient'); + if (!createClientFunction) return undefined; + return tryFindVersionInFunctionBody(createClientFunction); }; -// new ways in @autorest/typespec-ts emitter to set up api-version -const findApiVersionInRestClientV2 = (clientPath: string): string | undefined => { - const sourceCode= readFileSync(clientPath, {encoding: 'utf-8'}) - const sourceFile = ts.createSourceFile("example.ts", sourceCode, ts.ScriptTarget.Latest, true); - const createClientFunction = sourceFile.statements.filter(s => (s as ts.FunctionDeclaration)?.name?.escapedText === 'createClient').map(s => (s as ts.FunctionDeclaration))[0]; +// new way in @autorest/typespec-ts emitter to set up api-version +const tryFindApiVersionInRestClientV2 = (clientPath: string): string | undefined => { + const createClientFunction = tryFindFunctionWithApiVersion(clientPath, 'createClient'); + if (!createClientFunction) return undefined; let apiVersion: string | undefined = undefined; - createClientFunction.parameters.forEach(p => { - const isBindingPattern = node => node && typeof node === "object" && "elements" in node && "parent" in node && "kind" in node; - if (!isBindingPattern(p.name)) { - return; - } - const binding = p.name as ts.ObjectBindingPattern; - const apiVersionTexts = binding.elements?.filter(e => (e.name as ts.Identifier)?.escapedText === "apiVersion").map(e => e.initializer?.getText()); - // apiVersionTexts.length must be 0 or 1, otherwise the binding pattern contains the same keys, which causes a ts error - if (apiVersionTexts.length === 1 && apiVersionTexts[0]) { - apiVersion = extractApiVersionFromText(apiVersionTexts[0]); - } - }); + const bindingParameters = createClientFunction + .getParameters() + .filter((p) => p.getNameNode().getKind() === SyntaxKind.ObjectBindingPattern); + if (bindingParameters.length !== 1) return undefined; + const bindingPatterns = bindingParameters[0].getNameNode().asKind(SyntaxKind.ObjectBindingPattern); + if (!bindingPatterns) return undefined; + bindingPatterns + .getElements() + .filter((e) => e.getName() === 'apiVersion') + .map((e) => { + const text = e.getInitializer()?.getText(); + if (!text) return; + apiVersion = extractApiVersionFromText(text); + }); return apiVersion; }; -const findApiVersionsInOperations = ( - sourceFile: SourceFile | undefined -): Array | undefined => { +// another new way in @autorest/typespec-ts emitter to set up api-version +const tryFindApiVersionInRestClientV3 = (clientPath: string): string | undefined => { + const suffix = basename(clientPath).replace('Context.ts', ''); + const functionName = `create${suffix[0].toUpperCase()}${suffix.slice(1)}`; + const createClientFunction = tryFindFunctionWithApiVersion(clientPath, functionName); + if (!createClientFunction) return undefined; + return tryFindVersionInFunctionBody(createClientFunction); +}; + +const findApiVersionsInOperations = (sourceFile: SourceFile | undefined): Array | undefined => { const interfaces = sourceFile?.getInterfaces(); - const interfacesWithApiVersion = interfaces?.filter((itf) => - itf.getProperty('"api-version"') - ); + const interfacesWithApiVersion = interfaces?.filter((itf) => itf.getProperty('"api-version"')); const apiVersions = interfacesWithApiVersion?.map((itf) => { const property = itf.getMembers().filter((m) => { - const defaultValue = m.getChildrenOfKind( - SyntaxKind.StringLiteral - )[0]; + const defaultValue = m.getChildrenOfKind(SyntaxKind.StringLiteral)[0]; return defaultValue && defaultValue.getText() === '"api-version"'; })[0]; - const apiVersion = property - .getChildrenOfKind(SyntaxKind.LiteralType)[0] - .getText(); + const apiVersion = property.getChildrenOfKind(SyntaxKind.LiteralType)[0].getText(); return apiVersion; }); return apiVersions; }; // workaround for createClient function changes it's way to setup api-version -export const findApiVersionInRestClient = (clientPath: string): string | undefined => { - const version2 = findApiVersionInRestClientV2(clientPath); - if (version2) { - return version2; - } - const version1 = findApiVersionInRestClientV1(clientPath); +export const tryFindApiVersionInRestClient = (clientPath: string): string | undefined => { + const version3 = tryFindApiVersionInRestClientV3(clientPath); + if (version3) return version3; + const version2 = tryFindApiVersionInRestClientV2(clientPath); + if (version2) return version2; + const version1 = tryFindApiVersionInRestClientV1(clientPath); return version1; }; -export const tryFindRestClientPath = async (packageRoot: string, clientPattern: string): Promise => { +export const tryFindRestClientPath = async ( + packageRoot: string, + clientPattern: string +): Promise => { const pattern = unixify(path.join(packageRoot, clientPattern)); const clientFiles = await glob(pattern); if (clientFiles.length !== 1) { logger.warn(`Failed to find extactly one REST client in pattern '${pattern}', got '${clientFiles}'.`); return undefined; } + const filePath = clientFiles[0]; + if (!(await exists(filePath))) { + logger.warn(`Client file '${filePath}' does not exist.`); + return undefined; + } return clientFiles[0]; -}; +}; export const findParametersPath = (packageRoot: string, relativeParametersFolder: string): string => { const parametersPath = path.join(packageRoot, relativeParametersFolder); const fileNames = shell.ls(parametersPath); - const clientFiles = fileNames.filter((f) => f === "parameters.ts"); + const clientFiles = fileNames.filter((f) => f === 'parameters.ts'); if (clientFiles.length !== 1) throw new Error(`Expected 1 'parameters.ts' file, but found '${clientFiles}' in '${parametersPath}'.`); @@ -116,11 +127,9 @@ export const getApiVersionTypeFromRestClient = async ( ): Promise => { const clientPath = await findRestClientPath(packageRoot, clientPattern); if (!clientPath) return ApiVersionType.None; - const apiVersion = findApiVersionInRestClient(clientPath); - if (apiVersion && apiVersion.indexOf("-preview") >= 0) - return ApiVersionType.Preview; - if (apiVersion && apiVersion.indexOf("-preview") < 0) - return ApiVersionType.Stable; + const apiVersion = tryFindApiVersionInRestClient(clientPath); + if (apiVersion && apiVersion.indexOf('-preview') >= 0) return ApiVersionType.Preview; + if (apiVersion && apiVersion.indexOf('-preview') < 0) return ApiVersionType.Stable; return ApiVersionType.None; }; @@ -133,10 +142,6 @@ export const getApiVersionTypeFromOperations = ( const sourceFile = getTsSourceFile(paraPath); const apiVersions = findApiVersionsInOperations(sourceFile); if (!apiVersions) return ApiVersionType.None; - const previewVersions = apiVersions.filter( - (v) => v.indexOf("-preview") >= 0 - ); - return previewVersions.length > 0 - ? ApiVersionType.Preview - : ApiVersionType.Stable; -}; \ No newline at end of file + const previewVersions = apiVersions.filter((v) => v.indexOf('-preview') >= 0); + return previewVersions.length > 0 ? ApiVersionType.Preview : ApiVersionType.Stable; +};