[TypeSpec] Add @azure-tools/typespec-validation tool

- Replaces bash script used in "specs - typespec - validation" pipeline
- Also intended for spec authors to run on their dev machines
This commit is contained in:
Albert Cheng 2023-06-27 18:59:51 -07:00 коммит произвёл GitHub
Родитель 41a6298aba
Коммит a862f4f217
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 213 добавлений и 52 удалений

4
.gitignore поставляемый
Просмотреть файл

@ -119,3 +119,7 @@ warnings.txt
*.js.map
*.d.ts.map
*.bak
# Eng Tools
eng/tools/TypeSpecValidation/dist
!eng/tools/TypeSpecValidation/cmd/*.js

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

@ -5,49 +5,6 @@ parameters:
type: string
steps:
- script: |
RED='\033[0;31m'
exit_code=0
expected_npm_prefix=$(pwd)
# Log commands before running
set -x
pushd ${{parameters.Folder}} || exit_code=1
actual_npm_prefix=$(npm prefix)
if [ "$expected_npm_prefix" != "$actual_npm_prefix" ]; then
echo -e "\n${RED}ERROR: TypeSpec folders MUST NOT contain a package.json, and instead MUST rely on the package.json at repo root."
echo -e "${RED}Expected npm prefix: $expected_npm_prefix\n${RED}Actual npm prefix: $actual_npm_prefix\n"
exit_code=1
fi
# Pass "--no" to ensure npx does not install anything (TypeSpec compiler should already be installed)
if test -f main.tsp; then
npx --no tsp compile . --warn-as-error || exit_code=1
fi
if test -f client.tsp; then
npx --no tsp compile client.tsp --no-emit --warn-as-error || exit_code=1
fi
# Format parent folder to include shared files
npx tsp format ../**/*.tsp
popd
# If any files are added, changed, or deleted, print diff and return error
if [ -n "$(git status --porcelain)" ]; then
git status
git diff
exit_code=1
fi
# Revert any changes
git restore .
git clean -df
exit $exit_code
- script: npx --no tsv ${{parameters.Folder}}
displayName: ${{parameters.DisplayName}}
condition: succeededOrFailed()

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

@ -0,0 +1,5 @@
#!/usr/bin/env node
import { main } from "../dist/TypeSpecValidation.js"
await main();

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

@ -0,0 +1,16 @@
{
"name": "@azure-tools/typespec-validation",
"private": true,
"version": "0.0.1",
"type": "module",
"main": "dist/TypeSpecValidation.js",
"bin": {
"tsv": "cmd/tsv.js"
},
"dependencies": {
"simple-git": "^3.16.0"
},
"devDependencies": {
"@types/node": "^18.16.18"
}
}

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

@ -0,0 +1,88 @@
import {exec} from "child_process";
import {access} from "fs/promises"
import {parseArgs, ParseArgsConfig} from 'node:util';
import path from "path";
import {simpleGit} from 'simple-git';
async function runCmd(cmd:string, cwd:string) {
console.log(`run command:${cmd}`)
const { err, stdout, stderr } = await new Promise((res) =>
exec(
cmd,
{ encoding: "utf8", maxBuffer: 1024 * 1024 * 64, cwd: cwd},
(err: unknown, stdout: unknown, stderr: unknown) =>
res({ err: err, stdout: stdout, stderr: stderr })
)
) as any;
let resultString = stderr + stdout;
console.log("Stdout output:")
console.log(stdout)
if (stderr) {
console.log("Stderr output:")
console.log(stderr)
}
if (stderr || err) {
throw new Error(err);
}
return resultString as string;
}
async function checkFileExists(file:string) {
return access(file)
.then(() => true)
.catch(() => false)
}
export async function main() {
const args = process.argv.slice(2);
const options = {
folder: {
type: 'string',
short: 'f',
},
};
const parsedArgs = parseArgs({ args, options, allowPositionals: true} as ParseArgsConfig);
const folder = parsedArgs.positionals[0];
console.log("Running TypeSpecValidation on folder:", folder);
// Verify all specs are using root level pacakge.json
let expected_npm_prefix = process.cwd()
const actual_npm_prefix = (await runCmd(`npm prefix`, folder)).trim()
if (expected_npm_prefix !== actual_npm_prefix) {
console.log("ERROR: TypeSpec folders MUST NOT contain a package.json, and instead MUST rely on the package.json at repo root.")
throw new Error ("Expected npm prefix: " + expected_npm_prefix +"\nActual npm prefix: " + actual_npm_prefix)
}
// Spec compilation check
if (await checkFileExists(path.join(folder, "main.tsp"))) {
await runCmd(
`npx --no tsp compile . --warn-as-error`,
folder
);
}
if (await checkFileExists(path.join(folder, "client.tsp"))) {
await runCmd(
`npx --no tsp compile client.tsp --no-emit --warn-as-error`,
folder
);
}
// Format parent folder to include shared files
await runCmd(
`npx tsp format ../**/*.tsp`,
folder
);
// Verify generated swagger file is in sync with one on disk
const git = simpleGit();
let gitStatusIsClean = await (await git.status(['--porcelain'])).isClean()
if (!gitStatusIsClean) {
let gitStatus = await git.status()
let gitDiff = await git.diff()
console.log("git status")
console.log(gitStatus)
console.log("git diff")
console.log(gitDiff)
throw new Error("Generated swagger file does not match swagger file on disk")
}
}

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

@ -0,0 +1,8 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"target": "ES6",
"module": "Node16",
"outDir": "./dist"
}
}

93
package-lock.json сгенерированный
Просмотреть файл

@ -5,6 +5,7 @@
"packages": {
"": {
"name": "azure-rest-api-specs",
"hasInstallScript": true,
"devDependencies": {
"@azure-tools/cadl-apiview": "0.3.5",
"@azure-tools/cadl-autorest": "0.26.0",
@ -25,9 +26,30 @@
"@typespec/rest": "0.45.0",
"@typespec/versioning": "0.45.0",
"prettier": "^2.8.8",
"typescript": "~5.0.4"
"typescript": "~5.0.4",
"typespec-validation": "file:eng/tools/TypeSpecValidation"
}
},
"eng/tools/TypeSpecValidation": {
"name": "@azure-tools/typespec-validation",
"version": "0.0.1",
"dev": true,
"dependencies": {
"simple-git": "^3.16.0"
},
"bin": {
"tsv": "cmd/tsv.js"
},
"devDependencies": {
"@types/node": "^18.16.18"
}
},
"eng/tools/TypeSpecValidation/node_modules/@types/node": {
"version": "18.16.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.18.tgz",
"integrity": "sha512-/aNaQZD0+iSBAGnvvN2Cx92HqE5sZCPZtx2TsK+4nvV23fFe09jVDvpArXr2j9DnYlzuU9WuoykDDc6wqvpNcw==",
"dev": true
},
"node_modules/@azure-tools/cadl-apiview": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@azure-tools/cadl-apiview/-/cadl-apiview-0.3.5.tgz",
@ -570,6 +592,21 @@
"node": ">=16.0.0"
}
},
"node_modules/@kwsites/file-exists": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
"integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
"dev": true,
"dependencies": {
"debug": "^4.1.1"
}
},
"node_modules/@kwsites/promise-deferred": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
"integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==",
"dev": true
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -1392,6 +1429,23 @@
"node": ">= 12"
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
@ -1935,6 +1989,12 @@
"node": ">=10"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/mustache": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
@ -2267,9 +2327,9 @@
}
},
"node_modules/semver": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
"integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
"version": "7.5.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
"integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
@ -2304,6 +2364,21 @@
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
"dev": true
},
"node_modules/simple-git": {
"version": "3.19.1",
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.19.1.tgz",
"integrity": "sha512-Ck+rcjVaE1HotraRAS8u/+xgTvToTuoMkT9/l9lvuP5jftwnYUp6DwuJzsKErHgfyRk8IB8pqGHWEbM3tLgV1w==",
"dev": true,
"dependencies": {
"@kwsites/file-exists": "^1.1.1",
"@kwsites/promise-deferred": "^1.1.1",
"debug": "^4.3.4"
},
"funding": {
"type": "github",
"url": "https://github.com/steveukx/git-js?sponsor=1"
}
},
"node_modules/sisteransi": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@ -2401,9 +2476,9 @@
"dev": true
},
"node_modules/tslib": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz",
"integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==",
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz",
"integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==",
"dev": true
},
"node_modules/typescript": {
@ -2419,6 +2494,10 @@
"node": ">=12.20"
}
},
"node_modules/typespec-validation": {
"resolved": "eng/tools/TypeSpecValidation",
"link": true
},
"node_modules/upper-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz",

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

@ -20,7 +20,11 @@
"@azure/avocado": "^0.8.4",
"@types/prettier": "^2.7.2",
"prettier": "^2.8.8",
"typescript": "~5.0.4"
"typescript": "~5.0.4",
"typespec-validation": "file:eng/tools/TypeSpecValidation"
},
"scripts": {
"postinstall": "npx --no tsc -- -p eng/tools/TypeSpecValidation"
},
"private": true
}