Support RLC Changelog Generation Based on Inline Usage (#8751)

This commit is contained in:
Wanpeng Li 2024-08-19 21:20:08 +08:00 коммит произвёл GitHub
Родитель ee00555fb6
Коммит a37817cc57
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
55 изменённых файлов: 13432 добавлений и 6553 удалений

4
tools/js-sdk-release-tools/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,4 @@
# JS SDK Release Tools
!packages/*
.tmp/*
.tmp-detect/

Просмотреть файл

@ -65,11 +65,6 @@ extends:
npm run test
displayName: 'npm run test'
workingDirectory: $(System.DefaultWorkingDirectory)/tools/js-sdk-release-tools
- script: |
npm install
displayName: 'npm install'
workingDirectory: $(System.DefaultWorkingDirectory)/tools/js-sdk-release-tools
- script: |
npm pack

6608
tools/js-sdk-release-tools/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -3,14 +3,13 @@
"version": "2.7.11",
"description": "",
"scripts": {
"dev": "nodemon src/changelogToolCli.ts",
"dev": "tsx watch src/changelogToolCli.ts",
"start": "node dist/changelogToolCli.js",
"debug": "node --inspect-brk dist/changelogToolCli.js",
"build": "rimraf dist && tsc -p .",
"prepack": "npm run build",
"test": "vitest --run",
"test:watch": "vitest",
"test:ci": "node dist/changelogToolCli.js src/test/testCases/ci/arm-networkanalytics"
"test:watch": "vitest"
},
"bin": {
"changelog-tool": "./dist/changelogToolCli.js",
@ -28,23 +27,45 @@
"command-line-args": "^5.1.1",
"comment-json": "^4.1.0",
"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": "^12.0.0",
"ts-morph": "^23.0.0",
"tslib": "^1.9.3",
"typescript-codegen-breaking-change-detector": "0.4.11",
"unixify": "^1.0.0",
"winston": "^3.13.1",
"yaml": "^1.10.2"
},
"devDependencies": {
"@types/node": "^20.12.12",
"@types/shelljs": "^0.8.15",
"nodemon": "^3.1.0",
"@types/unixify": "^1.0.2",
"rimraf": "^3.0.2",
"ts-node": "^10.9.2",
"typescript": "^5.4.5",
"vitest": "^1.6.0"
"tsx": "^4.16.5",
"typescript": "5.5.3",
"vitest": "2.0.5"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.20.0",
"@rollup/rollup-android-arm64": "4.20.0",
"@rollup/rollup-darwin-arm64": "4.20.0",
"@rollup/rollup-darwin-x64": "4.20.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.20.0",
"@rollup/rollup-linux-arm-musleabihf": "4.20.0",
"@rollup/rollup-linux-arm64-gnu": "4.20.0",
"@rollup/rollup-linux-arm64-musl": "4.20.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.20.0",
"@rollup/rollup-linux-riscv64-gnu": "4.20.0",
"@rollup/rollup-linux-s390x-gnu": "4.20.0",
"@rollup/rollup-linux-x64-gnu": "4.20.0",
"@rollup/rollup-linux-x64-musl": "4.20.0",
"@rollup/rollup-win32-arm64-msvc": "4.20.0",
"@rollup/rollup-win32-ia32-msvc": "4.20.0",
"@rollup/rollup-win32-x64-msvc": "4.20.0"
}
}

Просмотреть файл

@ -0,0 +1,4 @@
node_modules
build
coverage
src/tests/e2e/cases

Просмотреть файл

@ -0,0 +1,17 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"prettier"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
"prettier/prettier": 2
}
}

Просмотреть файл

@ -0,0 +1,132 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.tmp/

Просмотреть файл

@ -0,0 +1,5 @@
{
"extends": [
"development"
]
}

Просмотреть файл

@ -0,0 +1 @@
registry=https://registry.npmjs.org/

Просмотреть файл

@ -0,0 +1,4 @@
node_modules
build
coverage
src/tests/e2e/cases

Просмотреть файл

@ -0,0 +1,9 @@
{
"arrowParens": "always",
"semi": true,
"endOfLine": "lf",
"tabWidth": 2,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 120
}

Просмотреть файл

@ -0,0 +1,11 @@
# typescript-codegen-breaking-change-detector
> 🏗 Work in progress...
Detect breaking changes to your TypeScript client generated by open api, typespec and more...
## Contribution
### sort-imports
Install `amatiasq.sort-imports` VS code extension to sort imports automatically.

Просмотреть файл

@ -0,0 +1,20 @@
# Azure REST Level Breaking Change Rules
## Special Rule Set
Special rule set for Azure REST level client: [rule set](../src/azure/rule-sets/rest-level-client-rule-sets.ts).
### Non-Breaking Changes
- Operation name changes: [rule](../src/azure/common/rules/ignore-operation-interface-name-changes.ts)
- Request parameter model name changes: [rule](../src/azure/common/rules/ignore-request-parameter-model-name-changes.ts)
- Response model name changes:
-
### Breaking Changes
- Path value changes
### TODO
- Request parameter type changes with content type

Просмотреть файл

@ -0,0 +1,127 @@
{
"name": "@azure-rest/arm-servicefabric",
"sdk-type": "client",
"author": "Microsoft Corporation",
"version": "1.0.0-beta.1",
"description": "",
"keywords": [
"node",
"azure",
"cloud",
"typescript",
"browser",
"isomorphic"
],
"license": "MIT",
"main": "dist/index.js",
"module": "./dist-esm/src/index.js",
"types": "./types/arm-servicefabric.d.ts",
"homepage": "https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/servicefabric/arm-servicefabric-rest/README.md",
"repository": "github:Azure/azure-sdk-for-js",
"bugs": {
"url": "https://github.com/Azure/azure-sdk-for-js/issues"
},
"files": [
"dist/",
"dist-esm/src/",
"types/arm-servicefabric.d.ts",
"README.md",
"LICENSE",
"review/*"
],
"//metadata": {
"constantPaths": [
{
"path": "swagger/README.md",
"prefix": "package-version"
}
]
},
"engines": {
"node": ">=12.0.0"
},
"scripts": {
"audit": "node ../../../common/scripts/rush-audit.js && rimraf node_modules package-lock.json && npm i --package-lock-only 2>&1 && npm audit",
"build:browser": "tsc -p . && cross-env ONLY_BROWSER=true rollup -c 2>&1",
"build:node": "tsc -p . && cross-env ONLY_NODE=true rollup -c 2>&1",
"build:samples": "echo skipped.",
"build:test": "tsc -p . && dev-tool run bundle",
"build:debug": "tsc -p . && dev-tool run bundle && api-extractor run --local",
"check-format": "prettier --list-different --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"samples-dev/**/*.ts\" \"*.{js,json}\"",
"clean": "rimraf dist dist-browser dist-esm test-dist temp types *.tgz *.log",
"execute:samples": "dev-tool samples run samples-dev",
"extract-api": "rimraf review && mkdirp ./review && api-extractor run --local",
"format": "prettier --write --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"samples-dev/**/*.ts\" \"*.{js,json}\"",
"generate:client": "autorest --typescript swagger/README.md && npm run format",
"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 src test --ext .ts --fix --fix-type [problem,suggestion]",
"lint": "eslint package.json api-extractor.json src test --ext .ts",
"pack": "npm pack 2>&1",
"test:browser": "npm run clean && npm run build:test && npm run unit-test:browser",
"test:node": "npm run clean && npm run build:test && npm run unit-test:node",
"test": "npm run clean && npm run build:test && npm run unit-test",
"unit-test": "npm run unit-test:node && npm run unit-test:browser",
"unit-test:node": "dev-tool run test:node-ts-input -- --timeout 1200000 --exclude 'test/**/browser/*.spec.ts' 'test/**/*.spec.ts'",
"unit-test:browser": "dev-tool run test:browser",
"build": "npm run clean && tsc -p . && dev-tool run bundle && mkdirp ./review && api-extractor run --local"
},
"sideEffects": false,
"autoPublish": false,
"dependencies": {
"@azure/core-auth": "^1.3.0",
"@azure-rest/core-client": "1.0.0-beta.10",
"@azure/core-rest-pipeline": "^1.8.0",
"@azure/logger": "^1.0.0",
"tslib": "^2.2.0",
"@azure/core-paging": "^1.2.0",
"@azure/core-lro": "^2.2.0"
},
"devDependencies": {
"@microsoft/api-extractor": "7.18.11",
"autorest": "latest",
"@types/node": "^12.0.0",
"dotenv": "^8.2.0",
"eslint": "^7.15.0",
"mkdirp": "^1.0.4",
"prettier": "2.2.1",
"rimraf": "^3.0.0",
"source-map-support": "^0.5.9",
"typescript": "~4.2.0",
"@azure/dev-tool": "^1.0.0",
"@azure/eslint-plugin-azure-sdk": "^3.0.0",
"@azure-tools/test-credential": "^1.0.0",
"@azure/identity": "^2.0.1",
"@azure-tools/test-recorder": "^2.0.0",
"mocha": "^7.1.1",
"mocha-junit-reporter": "^1.18.0",
"cross-env": "^7.0.2",
"@types/chai": "^4.2.8",
"chai": "^4.2.0",
"karma-chrome-launcher": "^3.0.0",
"karma-coverage": "^2.0.0",
"karma-edge-launcher": "^0.4.2",
"karma-env-preprocessor": "^0.1.1",
"karma-firefox-launcher": "^1.1.0",
"karma-ie-launcher": "^1.0.0",
"karma-junit-reporter": "^2.0.1",
"karma-mocha-reporter": "^2.2.5",
"karma-mocha": "^2.0.1",
"karma-source-map-support": "~1.4.0",
"karma-sourcemap-loader": "^0.3.8",
"karma": "^6.2.0",
"nyc": "^14.0.0"
},
"browser": {
"./dist-esm/test/public/utils/env.js": "./dist-esm/test/public/utils/env.browser.js"
},
"//sampleConfiguration": {
"productName": "",
"productSlugs": [
"azure"
],
"disableDocsMs": true,
"apiRefLink": "https://docs.microsoft.com/javascript/api/@azure-rest/arm-servicefabric?view=azure-node-preview"
}
}

Просмотреть файл

@ -0,0 +1,17 @@
# Release History
## 1.0.0 (2024-08-16)
### Features Added
- Added operation group ClustersGet_DDD
- Added Interface DiagnosticsStorageAccountConfigOutput_FFF
### Breaking Changes
- Removed operation group ClustersGet
- Operation ClusterVersionsGet.get has a new signature
- Interface DiagnosticsStorageAccountConfig no longer has parameter protectedAccountKeyName
- Interface DiagnosticsStorageAccountConfig has a new required parameter protectedAccountKeyName_GGG
- Type of parameter diagnosticsStorageAccountConfig of interface ClusterPropertiesOutput is changed from DiagnosticsStorageAccountConfigOutput to DiagnosticsStorageAccountConfigOutput_FFF

Просмотреть файл

@ -0,0 +1,125 @@
{
"name": "@azure-rest/arm-servicefabric",
"sdk-type": "client",
"author": "Microsoft Corporation",
"version": "1.0.0",
"description": "",
"keywords": [
"node",
"azure",
"cloud",
"typescript",
"browser",
"isomorphic"
],
"license": "MIT",
"main": "dist/index.js",
"module": "./dist-esm/src/index.js",
"types": "./types/arm-servicefabric.d.ts",
"homepage": "https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/servicefabric/arm-servicefabric-rest/README.md",
"repository": "github:Azure/azure-sdk-for-js",
"bugs": {
"url": "https://github.com/Azure/azure-sdk-for-js/issues"
},
"files": [
"dist/",
"dist-esm/src/",
"types/arm-servicefabric.d.ts",
"README.md",
"LICENSE",
"review/*"
],
"//metadata": {
"constantPaths": [
{
"path": "swagger/README.md",
"prefix": "package-version"
}
]
},
"engines": {
"node": ">=18.0.0"
},
"scripts": {
"audit": "node ../../../common/scripts/rush-audit.js && rimraf node_modules package-lock.json && npm i --package-lock-only 2>&1 && npm audit",
"build:browser": "tsc -p . && dev-tool run bundle",
"build:node": "tsc -p . && dev-tool run bundle --browser-test false",
"build:samples": "echo skipped.",
"build:test": "tsc -p . && dev-tool run bundle",
"build:debug": "tsc -p . && dev-tool 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-browser dist-esm test-dist temp types *.tgz *.log",
"execute:samples": "dev-tool samples run samples-dev",
"extract-api": "rimraf review && mkdirp ./review && 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}\"",
"generate:client": "autorest --typescript swagger/README.md && npm run format",
"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 src test --ext .ts --fix --fix-type [problem,suggestion]",
"lint": "eslint package.json api-extractor.json src test --ext .ts",
"pack": "npm pack 2>&1",
"test:browser": "npm run clean && npm run build:test && npm run unit-test:browser",
"test:node": "npm run clean && npm run build:test && npm run unit-test:node",
"test": "npm run clean && npm run build:test && npm run unit-test",
"unit-test": "npm run unit-test:node && npm run unit-test:browser",
"unit-test:node": "dev-tool run test:node-ts-input -- --timeout 1200000 --exclude 'test/**/browser/*.spec.ts' 'test/**/*.spec.ts'",
"unit-test:browser": "dev-tool run test:browser",
"build": "npm run clean && tsc -p . && dev-tool run bundle && mkdirp ./review && dev-tool run extract-api"
},
"sideEffects": false,
"autoPublish": false,
"dependencies": {
"@azure/core-auth": "^1.3.0",
"@azure-rest/core-client": "^1.0.0",
"@azure/core-rest-pipeline": "^1.8.0",
"@azure/logger": "^1.0.0",
"tslib": "^2.2.0",
"@azure/core-paging": "^1.2.0",
"@azure/core-lro": "^2.2.0"
},
"devDependencies": {
"@microsoft/api-extractor": "^7.31.1",
"autorest": "latest",
"@types/node": "^18.0.0",
"dotenv": "^16.0.0",
"eslint": "^8.0.0",
"mkdirp": "^3.0.1",
"rimraf": "^5.0.5",
"source-map-support": "^0.5.9",
"typescript": "~5.5.3",
"@azure/dev-tool": "^1.0.0",
"@azure/eslint-plugin-azure-sdk": "^3.0.0",
"@azure-tools/test-credential": "^1.0.0",
"@azure/identity": "^4.0.1",
"@azure-tools/test-recorder": "^3.0.0",
"mocha": "^10.0.0",
"cross-env": "^7.0.2",
"@types/chai": "^4.2.8",
"chai": "^4.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-junit-reporter": "^2.0.1",
"karma-mocha-reporter": "^2.2.5",
"karma-mocha": "^2.0.1",
"karma-source-map-support": "~1.4.0",
"karma-sourcemap-loader": "^0.3.8",
"karma": "^6.2.0",
"nyc": "^17.0.0",
"ts-node": "^10.0.0",
"@types/mocha": "^10.0.0"
},
"browser": {
"./dist-esm/test/public/utils/env.js": "./dist-esm/test/public/utils/env.browser.js"
},
"//sampleConfiguration": {
"productName": "",
"productSlugs": [
"azure"
],
"disableDocsMs": true,
"apiRefLink": "https://docs.microsoft.com/javascript/api/@azure-rest/arm-servicefabric?view=azure-node-preview"
}
}

Просмотреть файл

@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { ClientOptions, getClient } from "@azure-rest/core-client";
import { TokenCredential } from "@azure/core-auth";
import { ServiceFabricClient } from "./clientDefinitions";
import { customizedApiVersionPolicy } from "./customizedApiVersionPolicy";
export default function createClient(
credentials: TokenCredential,
options: ClientOptions = {},
): ServiceFabricClient {
const baseUrl = options.baseUrl ?? "https://management.azure.com";
options.apiVersion = options.apiVersion ?? "2021-06-01";
options = {
...options,
credentials: {
scopes: ["https://management.azure.com/.default"],
},
};
const userAgentInfo = `azsdk-js-arm-servicefabric-rest/1.0.0-beta.1`;
const userAgentPrefix =
options.userAgentOptions && options.userAgentOptions.userAgentPrefix
? `${options.userAgentOptions.userAgentPrefix} ${userAgentInfo}`
: `${userAgentInfo}`;
options = {
...options,
userAgentOptions: {
userAgentPrefix,
},
};
const client = getClient(baseUrl, credentials, options) as ServiceFabricClient;
// Considering ApiVersionPolicy in core has bugs so we replace with our customized one
client.pipeline.removePolicy({
name: "ApiVersionPolicy",
});
client.pipeline.addPolicy(customizedApiVersionPolicy(options));
return client;
}

Просмотреть файл

@ -0,0 +1,9 @@
{
"extends": "../../../tsconfig",
"compilerOptions": {
"outDir": "./dist-esm",
"declarationDir": "./types",
"paths": { "@azure-rest/arm-servicefabric": ["./src/index"] }
},
"include": ["src/**/*.ts", "./test/**/*.ts", "samples-dev/**/*.ts"]
}

Просмотреть файл

@ -0,0 +1,64 @@
{
"name": "typescript-codegen-breaking-change-detector",
"version": "0.4.11",
"description": "Detect breaking changes to your TypeScript client generated by open api, typespec and more...",
"main": "dist/src/index.js",
"types": "./dist/src/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "rimraf ./dist && tsc",
"start:dev": "tsx watch src/cli.ts",
"start": "npm run build && node dist/index.js",
"test": "vitest --run",
"test:watch": "vitest",
"lint": "eslint . --ext .ts --max-warnings=0",
"prettier-format": "run-script-os",
"prettier-format:win32": "prettier --config .prettierrc \"./src/**/*.ts\" --write",
"prettier-format:darwin:linux": "prettier --config .prettierrc 'src/**/*.ts' --write",
"prettier-format:default": "prettier --config .prettierrc 'src/**/*.ts' --write",
"prettier-watch": "run-script-os",
"prettier-watch:win32": "onchange \"src/**/*.ts\" -- prettier --write {{changed}}",
"prettier-watch:darwin:linux": "onchange 'src/**/*.ts' -- prettier --write {{changed}}",
"prettier-watch:default": "onchange 'src/**/*.ts' -- prettier --write {{changed}}"
},
"license": "MIT",
"dependencies": {
"@azure-tools/openapi-tools-common": "^1.2.2",
"@typescript-eslint/scope-manager": "^7.17.0",
"@typescript-eslint/types": "^7.17.0",
"@typescript-eslint/typescript-estree": "^7.17.0",
"@typescript-eslint/utils": "^7.17.0",
"@typescript-eslint/visitor-keys": "^7.17.0",
"commander": "^12.1.0",
"fs-extra": "^11.2.0",
"glob": "^11.0.0",
"marked": "^13.0.2",
"pino": "^9.3.1",
"pino-pretty": "^11.2.1",
"ts-morph": "^23.0.0",
"typescript": "5.5.3",
"typescript-eslint": "^7.17.0"
},
"devDependencies": {
"@types/eslint": "^8.56.10",
"@types/eslint-scope": "^3.7.7",
"@types/fs-extra": "^11.0.4",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"@typescript-eslint/parser": "^7.17.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"husky": "^9.0.11",
"onchange": "^7.1.0",
"prettier": "3.3.2",
"rimraf": "^5.0.7",
"tsx": "^4.16.2",
"vitest": "^2.0.4"
},
"husky": {
"hooks": {
"pre-commit": "npm run prettier-format && npm run lint"
}
}
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,63 @@
import {
CreateOperationRule,
InlineDeclarationNameSetMessage,
NodeContext,
ParseForESLintResult,
RuleMessageKind,
} from '../types';
import { RuleListener, getParserServices } from '@typescript-eslint/utils/eslint-utils';
import {
convertToMorphNode,
findAllRenameAbleDeclarationsInNode,
findDeclaration,
getGlobalScope,
isInterfaceDeclarationNode,
isParseServiceWithTypeInfo,
} from '../../../utils/ast-utils';
import { ParserServicesWithTypeInformation } from '@typescript-eslint/typescript-estree';
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
import { Scope } from '@typescript-eslint/scope-manager';
import { createOperationRuleListener } from '../../utils/azure-rule-utils';
import { getSettings } from '../../../utils/common-utils';
import { ignoreInlineDeclarationsInOperationGroup } from '../../../common/models/rules/rule-ids';
function getInlineDeclarationNameSet(service: ParserServicesWithTypeInformation, scope: Scope) {
const inlineDeclarationMap = new Map<string, NodeContext>();
const routes = findDeclaration('Routes', scope, isInterfaceDeclarationNode);
const moNode = convertToMorphNode(routes, service);
const result = findAllRenameAbleDeclarationsInNode(moNode, scope, service);
result.interfaces.forEach((i) => inlineDeclarationMap.set(i.getName(), { node: i, used: false }));
result.typeAliases.forEach((t) => inlineDeclarationMap.set(t.getName(), { node: t, used: false }));
result.enums.forEach((e) => inlineDeclarationMap.set(e.getName(), { node: e, used: false }));
return inlineDeclarationMap;
}
const rule: CreateOperationRule = (baselineParsedResult: ParseForESLintResult | undefined) => {
if (!baselineParsedResult)
throw new Error(`ParseForESLintResult is required in ${ignoreInlineDeclarationsInOperationGroup} rule`);
const baselineService = baselineParsedResult.services;
if (!isParseServiceWithTypeInfo(baselineService)) {
throw new Error(`Failed to get ParserServicesWithTypeInformation. It indicates the parser configuration is wrong.`);
}
const baselineGlobalScope = getGlobalScope(baselineParsedResult.scopeManager);
const baselineInlineDeclarationNameSet = getInlineDeclarationNameSet(baselineService, baselineGlobalScope);
return createOperationRuleListener(
ignoreInlineDeclarationsInOperationGroup,
(context: RuleContext<string, readonly unknown[]>): RuleListener => {
const currentService = getParserServices(context);
const currentGlobalScope = getGlobalScope(context.sourceCode.scopeManager);
const currentInlineDeclarationMap = getInlineDeclarationNameSet(currentService, currentGlobalScope);
const message: InlineDeclarationNameSetMessage = {
id: ignoreInlineDeclarationsInOperationGroup,
baseline: baselineInlineDeclarationNameSet,
current: currentInlineDeclarationMap,
kind: RuleMessageKind.InlineDeclarationNameSetMessage,
};
getSettings(context).reportInlineDeclarationNameSetMessage(message);
return {};
}
);
};
export default rule;

Просмотреть файл

@ -0,0 +1,52 @@
import { RuleListener, RuleModule } from '@typescript-eslint/utils/eslint-utils';
import { ParserServices } from '@typescript-eslint/parser';
import type { ScopeManager } from '@typescript-eslint/scope-manager';
import { TSESTree } from '@typescript-eslint/utils';
import type { VisitorKeys } from '@typescript-eslint/visitor-keys';
import { EnumDeclaration, InterfaceDeclaration, Node, TypeAliasDeclaration } from 'ts-morph';
export interface ParseForESLintResult {
ast: TSESTree.Program & {
range?: [number, number];
tokens?: TSESTree.Token[];
comments?: TSESTree.Comment[];
};
services: ParserServices;
visitorKeys: VisitorKeys;
scopeManager: ScopeManager;
}
export interface CreateOperationRule {
(baselineParsedResult: ParseForESLintResult | undefined): RuleModule<'default', readonly unknown[], RuleListener>;
}
export interface RuleMessage {
id: string;
kind: RuleMessageKind;
}
export enum RuleMessageKind {
InlineDeclarationNameSetMessage = 'InlineDeclarationNameSetMessage',
}
export interface InlineDeclarationNameSetMessage extends RuleMessage {
baseline: Map<string, NodeContext>;
current: Map<string, NodeContext>;
kind: RuleMessageKind.InlineDeclarationNameSetMessage;
}
export interface LinterSettings {
reportInlineDeclarationNameSetMessage(message: InlineDeclarationNameSetMessage): void;
}
export interface NodeContext {
node: InterfaceDeclaration | TypeAliasDeclaration | EnumDeclaration;
used: boolean;
}
export interface RenameAbleDeclarations {
interfaces: InterfaceDeclaration[];
typeAliases: TypeAliasDeclaration[];
enums: EnumDeclaration[];
}

Просмотреть файл

@ -0,0 +1,195 @@
import * as parser from '@typescript-eslint/parser';
import * as ruleIds from '../common/models/rules/rule-ids';
import {
InlineDeclarationNameSetMessage,
LinterSettings,
ParseForESLintResult,
RuleMessage,
} from './common/types';
import { Renderer, marked } from 'marked';
import { basename, join, posix, relative } from 'node:path';
import { devConsolelog, toPosixPath } from '../utils/common-utils';
import { exists, outputFile, readFile, remove } from 'fs-extra';
import { TSESLint } from '@typescript-eslint/utils';
import ignoreInlineDeclarationsInOperationGroup from './common/rules/ignore-inline-declarations-in-operation-group';
import { glob } from 'glob';
import { logger } from '../logging/logger';
const tsconfig = `
{
"compilerOptions": {
"jsx": "preserve",
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"lib": ["es2015", "es2017", "esnext"],
"experimentalDecorators": true,
"rootDir": "."
},
"include": [
"**/*.ts",
],
"exclude": ["**/node_modules/**/*.*"]
}
`;
interface SubProjectContext {
code: string;
relativeFilePath: string;
}
interface ProjectContext {
root: string;
baseline: SubProjectContext;
current: SubProjectContext;
}
async function loadCodeFromApiView(path: string) {
const content = await readFile(path, { encoding: 'utf-8' });
const markdown = content.toString();
const codeBlocks: string[] = [];
const renderer = new Renderer();
renderer.code = ({ text }) => {
codeBlocks.push(text);
return '';
};
marked(markdown, { renderer });
if (codeBlocks.length !== 1) throw new Error(`Expected 1 code block, got ${codeBlocks.length} in "${path}".`);
return codeBlocks[0];
}
async function prepareProject(
currentPackageFolder: string,
baselinePackageFolder: string,
tempFolder: string
): Promise<ProjectContext> {
const [currentCode, baselineCode] = await Promise.all([
loadCodeFromApiView(currentPackageFolder),
loadCodeFromApiView(baselinePackageFolder),
]);
const relativeCurrentPath = join('current', 'review', 'index.ts');
const relativeBaselinePath = join('baseline', 'review', 'index.ts');
const currentPath = join(tempFolder, relativeCurrentPath);
const baselinePath = join(tempFolder, relativeBaselinePath);
const tsConfigPath = join(tempFolder, 'tsconfig.json');
await Promise.all([
outputFile(tsConfigPath, tsconfig, 'utf-8'),
outputFile(currentPath, currentCode, 'utf-8'),
outputFile(baselinePath, baselineCode, 'utf-8'),
]);
return {
root: tempFolder,
baseline: {
code: baselineCode,
relativeFilePath: relativeBaselinePath,
},
current: {
code: currentCode,
relativeFilePath: relativeCurrentPath,
},
};
}
async function parseBaselinePackage(projectContext: ProjectContext): Promise<ParseForESLintResult> {
const result = parser.parseForESLint(projectContext.baseline.code, {
comment: true,
tokens: true,
range: true,
loc: true,
project: './tsconfig.json',
tsconfigRootDir: projectContext.root,
filePath: projectContext.baseline.relativeFilePath,
});
return result;
}
async function detectBreakingChangesCore(projectContext: ProjectContext): Promise<RuleMessage[] | undefined> {
try {
const breakingChangeResults: RuleMessage[] = [];
const baselineParsed = await parseBaselinePackage(projectContext);
const linter = new TSESLint.Linter({ cwd: projectContext.root });
// linter.defineRule(ruleIds.ignoreOperationGroupNameChanges, ignoreOperationGroupNameChangesRule(baselineParsed));
linter.defineRule(
ruleIds.ignoreInlineDeclarationsInOperationGroup,
ignoreInlineDeclarationsInOperationGroup(baselineParsed)
);
linter.defineParser('@typescript-eslint/parser', parser);
linter.verify(
projectContext.current.code,
{
rules: {
// [ruleIds.ignoreOperationGroupNameChanges]: [2],
[ruleIds.ignoreInlineDeclarationsInOperationGroup]: [2],
},
parser: '@typescript-eslint/parser',
parserOptions: {
filePath: projectContext.current.relativeFilePath,
comment: true,
tokens: true,
range: true,
loc: true,
project: './tsconfig.json',
tsconfigRootDir: projectContext.root,
},
settings: (<LinterSettings>{
reportInlineDeclarationNameSetMessage: (message: InlineDeclarationNameSetMessage) => {
breakingChangeResults.push(message);
},
}) as any,
},
projectContext.current.relativeFilePath
);
return breakingChangeResults;
} catch (err) {
logger.error(`Failed to detect breaking changes due to ${(err as Error).stack ?? err}`);
return undefined;
}
}
export async function detectBreakingChangesBetweenPackages(
baselinePackageFolder: string | undefined,
currentPackageFolder: string | undefined,
tempFolder: string | undefined,
cleanUpAtTheEnd: boolean
): Promise<Map<string, RuleMessage[] | undefined>> {
if (!baselinePackageFolder) throw new Error(`Failed to use undefined or null baseline package folder`);
if (!currentPackageFolder) throw new Error(`Failed to use undefined or null current package folder`);
if (!tempFolder) throw new Error(`Failed to use undefined or null temp folder`);
try {
baselinePackageFolder = toPosixPath(baselinePackageFolder);
currentPackageFolder = toPosixPath(currentPackageFolder);
tempFolder = toPosixPath(tempFolder);
const apiViewPathPattern = posix.join(baselinePackageFolder, 'review/*.api.md');
const baselineApiViewPaths = await glob(apiViewPathPattern);
const messsagesPromises = baselineApiViewPaths.map(async (baselineApiViewPath) => {
const relativeApiViewPath = relative(baselinePackageFolder!, baselineApiViewPath);
const apiViewBasename = basename(relativeApiViewPath);
const currentApiViewPath = join(currentPackageFolder!, relativeApiViewPath);
if (!(await exists(currentApiViewPath))) throw new Error(`Failed to find API view: ${currentApiViewPath}`);
const projectContext = await prepareProject(currentApiViewPath, baselineApiViewPath, tempFolder!);
const messages = await detectBreakingChangesCore(projectContext);
return { name: apiViewBasename, messages };
});
const messagesMap = new Map<string, RuleMessage[] | undefined>();
const promises = messsagesPromises.map(async (p) => {
const result = await p;
messagesMap.set(result.name, result.messages);
});
await Promise.all(promises);
return messagesMap;
} finally {
if (cleanUpAtTheEnd) {
if (await exists(tempFolder)) {
await remove(tempFolder);
}
}
}
}

Просмотреть файл

@ -0,0 +1,33 @@
import { ESLintUtils } from '@typescript-eslint/utils';
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
import { RuleListener } from '@typescript-eslint/utils/eslint-utils';
import { ruleDescriptions } from '../../common/models/rules/rule-descriptions';
// TODO: provide correct endpint
const endpoint = 'https://a.b.c/rules';
const createRule = ESLintUtils.RuleCreator((name) => `${endpoint}/${name}`);
export function createOperationRuleListener(
id: string,
createListener: (context: RuleContext<string, readonly unknown[]>) => RuleListener
) {
const messages = { default: '' };
const defaultOptions: ReadonlyArray<unknown> = [];
const rule = createRule({
name: id,
meta: {
docs: {
description: ruleDescriptions[id],
},
messages,
schema: [],
type: 'problem',
},
defaultOptions,
create(context) {
const listener = createListener(context);
return listener;
},
});
return rule;
}

Просмотреть файл

@ -0,0 +1,7 @@
import {
ignoreInlineDeclarationsInOperationGroup
} from './rule-ids';
export const ruleDescriptions: { [ruleId: string]: string } = {
[ignoreInlineDeclarationsInOperationGroup]: 'Ignore inline types in routes',
};

Просмотреть файл

@ -0,0 +1 @@
export const ignoreInlineDeclarationsInOperationGroup = 'ignore-inline-declarations-in-operation-group'

Просмотреть файл

@ -0,0 +1,2 @@
export * from './azure/detect-breaking-changes';
export * from './azure/common/types';

Просмотреть файл

@ -0,0 +1,23 @@
import { pino } from 'pino';
import pretty from 'pino-pretty';
const devStream = pretty({
colorize: false,
levelFirst: true,
translateTime: 'yyyy-dd-mm, h:MM:ss TT',
// destination: 'logs/app.log',
append: false,
});
const stream = pretty({
colorize: false,
levelFirst: true,
translateTime: 'yyyy-dd-mm, h:MM:ss TT',
// destination: 'logs/app.log',
append: true,
});
// TODO: make logger configurable
export const devFileLogger = pino({}, devStream);
export const logger = pino({}, stream);

Просмотреть файл

@ -0,0 +1,46 @@
import { mkdirp, pathExists } from 'fs-extra';
import { describe, expect, test } from 'vitest';
import { join } from 'node:path';
import { detectBreakingChangesBetweenPackages } from '../azure/detect-breaking-changes';
function getFormattedDate(): string {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
async function createTempFolder(tempFolderPrefix: string): Promise<string> {
const maxRetry = 1000;
let tempFolder = '';
for (let i = 0; i < maxRetry; i++) {
tempFolder = `${tempFolderPrefix}-${Math.round(Math.random() * 1000)}`;
if (await pathExists(tempFolder)) continue;
await mkdirp(tempFolder);
return tempFolder;
}
throw new Error(`Failed to create temp folder at "${tempFolder}" for ${maxRetry} times`);
}
describe('detect rest level client breaking changes', async () => {
test('should ignore operation rename', async () => {
const testCaseDir = '../../misc/test-cases/rest-level-client-to-rest-level-client/';
const currentPackageFolder = join(__dirname, testCaseDir, 'current-package');
const baselinePackageFolder = join(__dirname, testCaseDir, 'baseline-package');
const date = getFormattedDate();
const tempFolder = await createTempFolder(`.tmp/temp-${date}`);
const messagesMap = await detectBreakingChangesBetweenPackages(
baselinePackageFolder,
currentPackageFolder,
tempFolder,
true
);
expect(messagesMap.size).toBe(1);
// TODO: add more checks
});
});

Просмотреть файл

@ -0,0 +1,159 @@
import {
EnumDeclaration,
InterfaceDeclaration,
Node,
SyntaxKind,
TypeAliasDeclaration,
TypeReferenceNode,
createWrappedNode,
} from 'ts-morph';
import { ParserServices, ParserServicesWithTypeInformation } from '@typescript-eslint/typescript-estree';
import { Scope, ScopeManager } from '@typescript-eslint/scope-manager';
import { RenameAbleDeclarations } from '../azure/common/types';
import { TSESTree } from '@typescript-eslint/types';
import { findVariable } from '@typescript-eslint/utils/ast-utils';
import { logger } from '../logging/logger';
function tryFindDeclaration<TNode extends TSESTree.Node>(
name: string,
scope: Scope,
typeGuard: ((node: TSESTree.Node) => node is TNode) | undefined,
shouldLog: boolean = true
): TNode | undefined {
const variable = findVariable(scope as Scope, name);
const node = variable?.defs?.[0]?.node;
if (!node) {
if (shouldLog) logger.warn(`Failed to find ${name}'s declaration`);
return undefined;
}
if (typeGuard && !typeGuard(node)) {
if (shouldLog) logger.warn(`Found ${name}'s declaration but with another node type "${node.type}"`);
return undefined;
}
return node as TNode;
}
function getAllTypeReferencesInNode(node: Node, found: Set<string>) {
const types: TypeReferenceNode[] = [];
if (!node) return types;
node.forEachChild((child) => {
if (Node.isTypeReference(child) && !found.has(child.getText())) {
types.push(child);
}
const childTypes = getAllTypeReferencesInNode(child, found);
childTypes.forEach((c) => {
types.push(c);
});
});
return types;
}
function updateRenameAbleDeclarations(
declaration: TypeAliasDeclaration | InterfaceDeclaration | EnumDeclaration,
renameAbleDeclarations: RenameAbleDeclarations,
found: Set<string>
) {
const foundDeclarations = new Set<string>(
[...renameAbleDeclarations.interfaces, ...renameAbleDeclarations.typeAliases, ...renameAbleDeclarations.enums].map(
(i) => i.getName()
)
);
if (foundDeclarations.has(declaration.getName())) return;
switch (declaration.getKind()) {
case SyntaxKind.InterfaceDeclaration:
renameAbleDeclarations.interfaces.push(declaration as InterfaceDeclaration);
break;
case SyntaxKind.TypeAliasDeclaration:
renameAbleDeclarations.typeAliases.push(declaration as TypeAliasDeclaration);
break;
case SyntaxKind.EnumDeclaration:
renameAbleDeclarations.enums.push(declaration as EnumDeclaration);
break;
}
found.add(declaration.getName());
}
function findDeclarationOfTypeReference(
reference: TypeReferenceNode,
scope: Scope,
service: ParserServicesWithTypeInformation
): (TypeAliasDeclaration | InterfaceDeclaration | EnumDeclaration) | undefined {
const esDeclaration = tryFindDeclaration(reference.getText(), scope, undefined, false);
if (!esDeclaration) return;
const msDeclaration = convertToMorphNode(esDeclaration, service);
const msDeclarationKind = msDeclaration.getKind();
if (
msDeclarationKind !== SyntaxKind.InterfaceDeclaration &&
msDeclarationKind !== SyntaxKind.TypeAliasDeclaration &&
msDeclarationKind !== SyntaxKind.EnumDeclaration
)
return;
const declaration = msDeclaration as TypeAliasDeclaration | InterfaceDeclaration | EnumDeclaration;
return declaration;
}
function findAllRenameAbleDeclarationsInNodeCore(
node: Node,
scope: Scope,
service: ParserServicesWithTypeInformation,
renameAbleDeclarations: RenameAbleDeclarations,
found: Set<string>
): void {
if (!node) return;
const references = getAllTypeReferencesInNode(node, found);
references.forEach((reference) => {
const declaration = findDeclarationOfTypeReference(reference, scope, service);
if (!declaration) return;
updateRenameAbleDeclarations(declaration, renameAbleDeclarations, found);
findAllRenameAbleDeclarationsInNodeCore(declaration, scope, service, renameAbleDeclarations, found);
});
}
export function getGlobalScope(scopeManager: ScopeManager | null): Scope {
const globalScope = scopeManager?.globalScope;
if (!globalScope) throw new Error(`Failed to find global scope`);
return globalScope;
}
export function findDeclaration<TNode extends TSESTree.Node>(
name: string,
scope: Scope,
typeGuard: (node: TSESTree.Node) => node is TNode
): TNode {
const node = tryFindDeclaration(name, scope, typeGuard);
if (!node) throw new Error(`Failed to find "${name}"`);
return node;
}
export function isParseServiceWithTypeInfo(service: ParserServices): service is ParserServicesWithTypeInformation {
return service.program !== null;
}
export function isInterfaceDeclarationNode(node: TSESTree.Node): node is TSESTree.TSInterfaceDeclaration {
return node.type === TSESTree.AST_NODE_TYPES.TSInterfaceDeclaration;
}
export function convertToMorphNode(node: TSESTree.Node, service: ParserServicesWithTypeInformation) {
const tsNode = service.esTreeNodeToTSNodeMap.get(node);
const typeChecker = service.program.getTypeChecker();
const moNode = createWrappedNode(tsNode, { typeChecker });
return moNode;
}
export function findAllRenameAbleDeclarationsInNode(
node: Node,
scope: Scope,
service: ParserServicesWithTypeInformation
): { interfaces: InterfaceDeclaration[]; typeAliases: TypeAliasDeclaration[]; enums: EnumDeclaration[] } {
const renameAbleDeclarations: RenameAbleDeclarations = {
interfaces: [],
typeAliases: [],
enums: [],
};
const found = new Set<string>();
findAllRenameAbleDeclarationsInNodeCore(node, scope, service, renameAbleDeclarations, found);
return renameAbleDeclarations;
}

Просмотреть файл

@ -0,0 +1,24 @@
import { LinterSettings } from '../azure/common/types';
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
import path from 'path';
import util from 'util';
// IMPORTANT: dev with chakrounanas.turbo-console-log vscode extension
export function devConsolelog(title?: any, ...optionalParams: any[]): void {
const start = '💎💎💎💎';
const end = '📍📍📍📍';
const body = util.inspect(optionalParams, { depth: null, colors: true });
console.log(start, 'START:', title, start);
console.log(body);
console.log(end, 'END :', title, end);
console.log();
}
export function getSettings(context: RuleContext<string, readonly unknown[]>) {
return (context.settings as any as LinterSettings)!;
}
export function toPosixPath(winPath: string) {
const posixPath = winPath.split(path.sep).join(path.posix.sep);
return posixPath;
}

Просмотреть файл

@ -0,0 +1,105 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "NodeNext", /* Specify what module code is generated. */
"rootDir": ".", /* Specify the root folder within your source files. */
"moduleResolution": "NodeNext", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
"resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
"declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
"typeRoots": ["./node_modules/@types", "./dist/src"],
},
"exclude": ["**/tests/**/cases/**", "dist/**", "packages/**"],
"include": ["src/**/*.ts"]
}

Просмотреть файл

@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
exclude: ['misc/**', 'dist/**', 'node_modules/**'],
},
});

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,118 @@
import { Changelog, ChangelogItem } from './changelogGenerator';
import { InlineDeclarationNameSetMessage, NodeContext } from 'typescript-codegen-breaking-change-detector';
export class RestLevelClientChangelogPostProcessor {
private changelog: Changelog;
private message: InlineDeclarationNameSetMessage;
constructor(changelog: Changelog, message: InlineDeclarationNameSetMessage) {
this.changelog = changelog;
this.message = message;
}
public run() {
this.handleChangelogItems(this.changelog.addedOperationGroup);
this.handleChangelogItems(this.changelog.removedOperationGroup);
this.handleChangelogItems(this.changelog.addedOperation);
this.handleChangelogItems(this.changelog.addedInterface);
this.handleChangelogItems(this.changelog.addedClass);
this.handleChangelogItems(this.changelog.addedTypeAlias);
this.handleChangelogItems(this.changelog.interfaceAddOptionalParam);
this.handleChangelogItems(this.changelog.interfaceParamTypeExtended);
this.handleChangelogItems(this.changelog.typeAliasAddInherit);
this.handleChangelogItems(this.changelog.typeAliasAddParam);
this.handleChangelogItems(this.changelog.addedEnum);
this.handleChangelogItems(this.changelog.addedEnumValue);
this.handleChangelogItems(this.changelog.addedFunction);
this.handleChangelogItems(this.changelog.removedOperation);
this.handleChangelogItems(this.changelog.operationSignatureChange);
this.handleChangelogItems(this.changelog.deletedClass);
this.handleChangelogItems(this.changelog.classSignatureChange);
this.handleChangelogItems(this.changelog.interfaceParamDelete);
this.handleChangelogItems(this.changelog.interfaceParamAddRequired);
this.handleChangelogItems(this.changelog.interfaceParamTypeChanged);
this.handleChangelogItems(this.changelog.interfaceParamChangeRequired);
this.handleChangelogItems(this.changelog.classParamDelete);
this.handleChangelogItems(this.changelog.classParamChangeRequired);
this.handleChangelogItems(this.changelog.typeAliasDeleteInherit);
this.handleChangelogItems(this.changelog.typeAliasParamDelete);
this.handleChangelogItems(this.changelog.typeAliasAddRequiredParam);
this.handleChangelogItems(this.changelog.typeAliasParamChangeRequired);
this.handleChangelogItems(this.changelog.removedEnum);
this.handleChangelogItems(this.changelog.removedEnumValue);
this.handleChangelogItems(this.changelog.removedFunction);
}
private getCurrentNodeContext(name: string | undefined): NodeContext | undefined {
if (!name) return undefined;
return this.message.current.get(name);
}
private getBaselineNodeContext(name: string | undefined): NodeContext | undefined {
if (!name) return undefined;
return this.message.baseline.get(name);
}
private findCompatibleNodeContext(
inputContext: NodeContext,
contextMapToFind: Map<string, NodeContext>,
): NodeContext | undefined {
for (const [_, foundContext] of contextMapToFind) {
const isCompatibleFromInputToFound = inputContext.node.getType().isAssignableTo(foundContext.node.getType())
const isCompatibleFromFoundToInput = foundContext.node.getType().isAssignableTo(inputContext.node.getType())
if (isCompatibleFromInputToFound && isCompatibleFromFoundToInput) return foundContext;
}
return undefined;
}
private tryIgnoreInlineTypeInChangelogItem(
inputContext: NodeContext,
nodeContextMapToFind: Map<string, NodeContext>,
item: ChangelogItem
) {
if (!inputContext) return;
const foundContext = this.findCompatibleNodeContext(
inputContext,
nodeContextMapToFind,
);
if (foundContext) {
inputContext.used = true;
foundContext.used = true;
item.toDelete = true;
}
return;
}
private handleChangelogItems(items: ChangelogItem[]) {
items.forEach((item) => {
if (!item.oldName && !item.newName) return;
if (item.newName && item.oldName) {
const currentContext = this.getCurrentNodeContext(item.newName);
if (!currentContext) return;
const baselineContext = this.getBaselineNodeContext(item.oldName);
if (!baselineContext) return;
const currentType = currentContext.node.getType();
const baselineType = baselineContext.node.getType();
if (currentType.isAssignableTo(baselineType)) {
item.toDelete = true;
currentContext.used = true;
baselineContext.used = true;
}
return;
}
if (item.newName) {
const inputContext = this.getCurrentNodeContext(item.newName);
if (!inputContext) return;
this.tryIgnoreInlineTypeInChangelogItem(inputContext, this.message.baseline, item);
return;
}
// item.oldName exists
const inputContext = this.getBaselineNodeContext(item.oldName);
if (!inputContext) return;
this.tryIgnoreInlineTypeInChangelogItem(inputContext, this.message.current, item);
return;
});
}
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -1,5 +1,5 @@
import { ApiVersionType } from "./types";
export interface IApiVersionTypeExtractor {
(packageRoot: string): ApiVersionType;
(packageRoot: string): Promise<ApiVersionType>;
}

Просмотреть файл

@ -47,6 +47,10 @@ export function getClassicClientParametersPath(packageRoot: string): string {
export function getSDKType(packageRoot: string): SDKType {
const paraPath = getClassicClientParametersPath(packageRoot);
const packageName = getNpmPackageName(packageRoot);
if (packageName.startsWith('@azure-rest/')) {
return SDKType.RestLevelClient;
}
const exist = shell.test('-e', paraPath);
const type = exist ? SDKType.HighLevelClient : SDKType.ModularClient;
logger.info(`SDK type '${type}' is detected in '${packageRoot}'.`);

Просмотреть файл

@ -3,7 +3,7 @@ import { IApiVersionTypeExtractor } from "../../common/interfaces";
import { getClassicClientParametersPath, getTsSourceFile } from "../../common/utils";
// TODO: add unit test
export const getApiVersionType: IApiVersionTypeExtractor = (packageRoot: string): ApiVersionType => {
export const getApiVersionType: IApiVersionTypeExtractor = async (packageRoot: string): Promise<ApiVersionType> => {
const paraPath = getClassicClientParametersPath(packageRoot);
const source = getTsSourceFile(paraPath);
const variableDeclarations = source?.getVariableDeclarations();

Просмотреть файл

@ -20,14 +20,14 @@ import {
} from "../../utils/version";
import { execSync } from "child_process";
import { getversionDate } from "../../utils/version";
import { ApiVersionType } from "../../common/types"
import { ApiVersionType, SDKType } from "../../common/types"
import { getApiVersionType } from '../../xlc/apiVersion/apiVersionTypeExtractor'
import { fixChangelogFormat, getApiReviewPath, getNpmPackageName, getSDKType, tryReadNpmPackageChangelog } from '../../common/utils';
export async function generateChangelogAndBumpVersion(packageFolderPath: string) {
const jsSdkRepoPath = String(shell.pwd());
packageFolderPath = path.join(jsSdkRepoPath, packageFolderPath);
const ApiType = getApiVersionType(packageFolderPath);
const ApiType = await getApiVersionType(packageFolderPath);
const isStableRelease = ApiType != ApiVersionType.Preview;
const packageName = getNpmPackageName(packageFolderPath);
const npm = new NPMScope({ executionFolderPath: packageFolderPath });
@ -57,7 +57,8 @@ export async function generateChangelogAndBumpVersion(packageFolderPath: string)
// only track2 sdk includes sdk-type with value mgmt
const sdkType = JSON.parse(fs.readFileSync(path.join(packageFolderPath, 'changelog-temp', 'package', 'package.json'), {encoding: 'utf-8'}))['sdk-type'];
if (sdkType && sdkType === 'mgmt') {
const clientType = getSDKType(packageFolderPath);
if (sdkType && sdkType === 'mgmt' || clientType === SDKType.RestLevelClient) {
logger.info(`Package ${packageName} released before is track2 sdk.`);
logger.info('Start to generate changelog by comparing api.md.');
const npmPackageRoot = path.join(packageFolderPath, 'changelog-temp', 'package');
@ -100,6 +101,7 @@ export async function generateChangelogAndBumpVersion(packageFolderPath: string)
}
makeChangesForPatchReleasingTrack2(packageFolderPath, newVersion);
} else {
await changelog.postProcess(npmPackageRoot, packageFolderPath, clientType)
const newVersion = getNewVersion(stableVersion, usedVersions, changelog.hasBreakingChange, isStableRelease);
makeChangesForReleasingTrack2(packageFolderPath, newVersion, changelog, originalChangeLogContent,stableVersion);
logger.info('Generated changelogs and set version for track2 release successfully.');

Просмотреть файл

@ -0,0 +1,20 @@
import { findParametersPath, getApiVersionTypeFromOperations, getApiVersionTypeFromRestClient, tryFindRestClientPath } from "../../xlc/apiVersion/utils";
import { ApiVersionType } from "../../common/types";
import { IApiVersionTypeExtractor } from "../../common/interfaces";
export const getApiVersionType: IApiVersionTypeExtractor = async (
packageRoot: string
): Promise<ApiVersionType> => {
let clientPattern = "src/*Context.ts";
let typeFromClient = await getApiVersionTypeFromRestClient(packageRoot, clientPattern, tryFindRestClientPath);
if (typeFromClient !== ApiVersionType.None) return typeFromClient;
clientPattern = "src/*Client.ts";
typeFromClient = await getApiVersionTypeFromRestClient(packageRoot, clientPattern, tryFindRestClientPath);
if (typeFromClient !== ApiVersionType.None) return typeFromClient;
const typeFromOperations = getApiVersionTypeFromOperations(packageRoot, clientPattern, findParametersPath);
if (typeFromOperations !== ApiVersionType.None) return typeFromOperations;
return ApiVersionType.Stable;
};

Просмотреть файл

@ -1,135 +1,19 @@
import { SourceFile, SyntaxKind } from "ts-morph";
import shell from "shelljs";
import path from "path";
import * as ts from "typescript";
import { ApiVersionType } from "../../common/types";
import { IApiVersionTypeExtractor } from "../../common/interfaces";
import { getTsSourceFile } from "../../common/utils";
import { readFileSync } from "fs";
import { findParametersPath, getApiVersionTypeFromOperations, getApiVersionTypeFromRestClient, tryFindRestClientPath } from "../../xlc/apiVersion/utils";
const findRestClientPath = (packageRoot: string): string => {
const restPath = path.join(packageRoot, "src/rest/");
const fileNames = shell.ls(restPath);
const clientFiles = fileNames.filter((f) => f.endsWith("Client.ts"));
if (clientFiles.length !== 1)
throw new Error(`Single client is supported, but found "${clientFiles}" in ${restPath}`);
const clientPath = path.join(restPath, clientFiles[0]);
return clientPath;
};
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"));
if (apiVersionStatements.length === 0) {
return undefined;
}
const text =
apiVersionStatements[apiVersionStatements.length - 1].getText();
return extractApiVersionFromText(text);
};
const extractApiVersionFromText = (text: string): string | undefined => {
const begin = text.indexOf('"');
const end = text.lastIndexOf('"');
return text.substring(begin + 1, end);
};
// 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];
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]);
}
});
return apiVersion;
};
// 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);
return version1;
};
const getApiVersionTypeFromRestClient: IApiVersionTypeExtractor = (
export const getApiVersionType: IApiVersionTypeExtractor = async (
packageRoot: string
): ApiVersionType => {
const clientPath = findRestClientPath(packageRoot);
const apiVersion = findApiVersionInRestClient(clientPath);
if (apiVersion && apiVersion.indexOf("-preview") >= 0)
return ApiVersionType.Preview;
if (apiVersion && apiVersion.indexOf("-preview") < 0)
return ApiVersionType.Stable;
return ApiVersionType.None;
};
const findApiVersionsInOperations = (
sourceFile: SourceFile | undefined
): Array<string> | undefined => {
const interfaces = sourceFile?.getInterfaces();
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];
return defaultValue && defaultValue.getText() === '"api-version"';
})[0];
const apiVersion = property
.getChildrenOfKind(SyntaxKind.LiteralType)[0]
.getText();
return apiVersion;
});
return apiVersions;
};
const getApiVersionTypeFromOperations: IApiVersionTypeExtractor = (
packageRoot: string
): ApiVersionType => {
const paraPath = path.join(packageRoot, "src/rest/parameters.ts");
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;
};
export const getApiVersionType: IApiVersionTypeExtractor = (
packageRoot: string
): ApiVersionType => {
const typeFromClient = getApiVersionTypeFromRestClient(packageRoot);
): Promise<ApiVersionType> => {
let clientPattern = "src/api/*Context.ts";
let typeFromClient = await getApiVersionTypeFromRestClient(packageRoot, clientPattern, tryFindRestClientPath);
if (typeFromClient !== ApiVersionType.None) return typeFromClient;
const typeFromOperations = getApiVersionTypeFromOperations(packageRoot);
clientPattern = "src/rest/*Client.ts";
typeFromClient = await getApiVersionTypeFromRestClient(packageRoot, clientPattern, tryFindRestClientPath);
if (typeFromClient !== ApiVersionType.None) return typeFromClient;
const typeFromOperations = getApiVersionTypeFromOperations(packageRoot, clientPattern, findParametersPath);
if (typeFromOperations !== ApiVersionType.None) return typeFromOperations;
return ApiVersionType.Stable;
};

Просмотреть файл

@ -1,7 +1,9 @@
import { expect, test } from "vitest";
import { findApiVersionInRestClient, getApiVersionType } from "../../mlc/apiVersion/apiVersionTypeExtractor";
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";
test("MLC api-version Extractor: new createClient function", async () => {
const clientPath = join(__dirname, 'testCases/new/src/rest/newClient.ts');
@ -11,7 +13,7 @@ test("MLC api-version Extractor: new createClient function", async () => {
test("MLC api-version Extractor: get api version type from new createClient function", async () => {
const root = join(__dirname, 'testCases/new/');
const version = getApiVersionType(root);
const version = await getApiVersionType(root);
expect(version).toBe(ApiVersionType.Preview);
});
@ -23,7 +25,7 @@ test("MLC api-version Extractor: old createClient function 1", async () => {
test("MLC api-version Extractor: get api version type from old createClient function 1", async () => {
const root = join(__dirname, 'testCases/old1/');
const version = getApiVersionType(root);
const version = await getApiVersionType(root);
expect(version).toBe(ApiVersionType.Preview);
});
@ -35,6 +37,36 @@ test("MLC api-version Extractor: old createClient function 2", async () => {
test("MLC api-version Extractor: get api version type from old createClient function 2", async () => {
const root = join(__dirname, 'testCases/old2/');
const version = getApiVersionType(root);
const version = await getApiVersionType(root);
expect(version).toBe(ApiVersionType.Stable);
});
describe("Rest client file fallbacks", () => {
// Modular
{
test("Modular: 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 () => {
const root = join(__dirname, 'testCases/new/');
const version = await getApiVersionType(root);
expect(version).toBe(ApiVersionType.Preview);
});
}
// RLC
{
test("RLC: 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 () => {
const root = join(__dirname, 'testCases/rlc-client/');
const version = await getApiVersionTypeInRLC(root);
expect(version).toBe(ApiVersionType.Preview);
});
}
});

Просмотреть файл

@ -0,0 +1,68 @@
// 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;
}

Просмотреть файл

@ -0,0 +1,68 @@
// 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;
}

Просмотреть файл

@ -0,0 +1,68 @@
// 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;
}

Просмотреть файл

@ -3,9 +3,8 @@ import { extractExportAndGenerateChangelog } from "../../changelog/extractMetaDa
import path, { join } from "path";
import { SDKType } from "../../common/types";
import { describe } from "node:test";
import { mkdirSync } from "node:fs";
import { tryReadNpmPackageChangelog } from "../../common/utils";
import { rmdirSync, writeFileSync } from "fs";
import { ensureDirSync, removeSync, outputFileSync } from "fs-extra";
function getRandomInt(max) {
return Math.floor(Math.random() * max);
@ -34,10 +33,10 @@ describe("Breaking change detection", () => {
expect(changelog.addedOperation.length).toBe(1);
expect(changelog.removedOperation.length).toBe(1);
expect(changelog.addedOperation[0]).toBe(
expect(changelog.addedOperation[0].line).toBe(
"Added operation DataProductsCatalogsOperations.listByResourceGroup_NEW"
);
expect(changelog.removedOperation[0]).toBe(
expect(changelog.removedOperation[0].line).toBe(
"Removed operation DataProductsCatalogs.listByResourceGroup"
);
});
@ -64,10 +63,10 @@ describe("Breaking change detection", () => {
expect(changelog.addedOperation.length).toBe(1);
expect(changelog.removedOperation.length).toBe(1);
expect(changelog.addedOperation[0]).toBe(
expect(changelog.addedOperation[0].line).toBe(
"Added operation DataProductsCatalogs.get_NEW"
);
expect(changelog.removedOperation[0]).toBe(
expect(changelog.removedOperation[0].line).toBe(
"Removed operation DataProductsCatalogs.get"
);
});
@ -94,10 +93,10 @@ describe("Breaking change detection", () => {
expect(changelog.addedOperation.length).toBe(1);
expect(changelog.removedOperation.length).toBe(1);
expect(changelog.addedOperation[0]).toBe(
expect(changelog.addedOperation[0].line).toBe(
"Added operation DataProductsCatalogsOperations.listByResourceGroup_NEW"
);
expect(changelog.removedOperation[0]).toBe(
expect(changelog.removedOperation[0].line).toBe(
"Removed operation DataProductsCatalogsOperations.listByResourceGroup"
);
});
@ -126,33 +125,34 @@ describe("Breaking change detection", () => {
expect(changelog.operationSignatureChange.length).toBe(1);
expect(changelog.addedOperationGroup[0]).toBe(
expect(changelog.addedOperationGroup[0].line).toBe(
"Added operation group DataProductsCatalogs_add"
);
expect(changelog.removedOperationGroup[0]).toBe(
expect(changelog.removedOperationGroup[0].line).toBe(
"Removed operation group DataProductsCatalogs_remove"
);
expect(changelog.operationSignatureChange[0]).toBe(
expect(changelog.operationSignatureChange[0].line).toBe(
"Operation DataProductsCatalogs_sig_change.get has a new signature"
);
});
});
describe("Changelog reading", () => {
const tempPackageFolder = `./tmp/package-${getRandomInt(10000)}`;
try {
test("Read changelog that doesn't exist", () => {
const content = tryReadNpmPackageChangelog(tempPackageFolder);
const content = tryReadNpmPackageChangelog('./do/not/exist');
expect(content).toBe("");
});
const changelogPath = join(tempPackageFolder, 'CHANGELOG.md')
writeFileSync(changelogPath, 'aaa', 'utf-8');
test("Read changelog that exists", () => {
const content = tryReadNpmPackageChangelog(tempPackageFolder);
expect(content).toBe("aaa");
test("Read changelog that exists", () => {
const tempPackageFolder = path.join(__dirname, `tmp/package-${getRandomInt(10000)}`);
try {
ensureDirSync(tempPackageFolder);
const changelogPath = path.join(tempPackageFolder, 'changelog-temp', 'package', 'CHANGELOG.md')
outputFileSync(changelogPath, 'aaa', 'utf-8');
const content = tryReadNpmPackageChangelog(tempPackageFolder);
expect(content).toBe("aaa");
} finally {
removeSync(tempPackageFolder);
}
})
} finally {
rmdirSync(tempPackageFolder);
}
});
});

Просмотреть файл

@ -14,7 +14,6 @@ const pipeline = winston.format((info, opts) => {
export const logger = winston.createLogger({
level: "info",
format: winston.format.combine(winston.format.json(), pipeline()),
defaultMeta: { service: "user-service" },
});
if (process.env.NODE_ENV !== "production") {

Просмотреть файл

@ -3,15 +3,18 @@ import { ApiVersionType, SDKType } from "../../common/types";
import { IApiVersionTypeExtractor } from "../../common/interfaces";
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 = (packageRoot: string): ApiVersionType => {
export const getApiVersionType: IApiVersionTypeExtractor = async (packageRoot: string): Promise<ApiVersionType> => {
const sdkType = getSDKType(packageRoot);
switch (sdkType) {
case SDKType.ModularClient:
return mlcApi.getApiVersionType(packageRoot);
return await mlcApi.getApiVersionType(packageRoot);
case SDKType.HighLevelClient:
return hlcApi.getApiVersionType(packageRoot);
return await hlcApi.getApiVersionType(packageRoot);
case SDKType.RestLevelClient:
return await rlcApi.getApiVersionType(packageRoot);
default:
console.warn(`Unsupported SDK type ${sdkType} to get detact api version`);
return ApiVersionType.None;

Просмотреть файл

@ -0,0 +1,142 @@
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'
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"));
if (apiVersionStatements.length === 0) {
return undefined;
}
const text =
apiVersionStatements[apiVersionStatements.length - 1].getText();
return extractApiVersionFromText(text);
};
const extractApiVersionFromText = (text: string): string | undefined => {
const begin = text.indexOf('"');
const end = text.lastIndexOf('"');
return text.substring(begin + 1, end);
};
// 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];
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]);
}
});
return apiVersion;
};
const findApiVersionsInOperations = (
sourceFile: SourceFile | undefined
): Array<string> | undefined => {
const interfaces = sourceFile?.getInterfaces();
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];
return defaultValue && defaultValue.getText() === '"api-version"';
})[0];
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);
return version1;
};
export const tryFindRestClientPath = async (packageRoot: string, clientPattern: string): Promise<string | undefined> => {
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;
}
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");
if (clientFiles.length !== 1)
throw new Error(`Expected 1 'parameters.ts' file, but found '${clientFiles}' in '${parametersPath}'.`);
const clientPath = path.join(parametersPath, clientFiles[0]);
return clientPath;
};
export const getApiVersionTypeFromRestClient = async (
packageRoot: string,
clientPattern: string,
findRestClientPath: (packageRoot: string, clientPattern: string) => Promise<string | undefined>
): Promise<ApiVersionType> => {
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;
return ApiVersionType.None;
};
export const getApiVersionTypeFromOperations = (
packageRoot: string,
relativeParametersFolder: string,
findPararametersPath: (packageRoot: string, relativeParametersFolder: string) => string
): ApiVersionType => {
const paraPath = findPararametersPath(packageRoot, relativeParametersFolder);
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;
};

Просмотреть файл

@ -4,14 +4,13 @@
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
"lib": ["ES6"] /* Specify library files to be included in the compilation. */,
"target": "ES2022" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
"module": "NodeNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
"declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true /* Generates corresponding '.map' file. */,
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./dist" /* Redirect output structure to the directory. */,
@ -43,7 +42,7 @@
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"moduleResolution": "NodeNext", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
@ -68,5 +67,6 @@
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"exclude": ["src/test/**/testCases/**", "vitest.config.ts"]
"exclude": ["src/test/**/testCases/**", "vitest.config.ts"],
"include": ["src/**/*"]
}

Просмотреть файл

@ -2,6 +2,12 @@ import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
exclude: ["src/test/testCases/**", "dist/**", "node_modules/**"],
exclude: [
"src/test/testCases/**",
"dist/**",
"**/node_modules/**",
"**/misc/**",
"packages/**",
],
},
});