-
Notifications
You must be signed in to change notification settings - Fork 183
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* create tool tsp-client * [tsp-client] Refactor tsp-client (#6887) * update options * add additional fs helper methods * update options and usage text * refactor + update commands * add additional directories support * add function to compile from shell * use additional dirs, call tsp compile * return empty list for additional dirs * add init option * use service as project name * improve directory naming * allow raw github content url for init, include additional dirs in yaml * rename file * use cp with promises * fix remove dir * remove existssync * clean up * more clean up * rename to syncAndGenerate * remove then from fs * remove then use * remove var * improve loop * remove shell usage * refactor url parser * refactor to use compile method from typespec lib * Rename function * add more init functionality * update log message * add support for local spec repo * add emitter options flag * reorganize code * support commit and repo update * support config url update * simplify emitter search in emitter-package.json * update package.json * prompt user for correct output dir * improve error message * clean up * add initial local spec support * fix author * update chaining format * fix resolveTspConfigUrl * simplify tspconfig variable search * fix getEmitterFromRepoConfig * fs updates * update package.json --------- Co-authored-by: catalinaperalta <[email protected]>
- Loading branch information
1 parent
cbd469a
commit 865b861
Showing
18 changed files
with
1,310 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
node_modules | ||
*.env | ||
dist | ||
types | ||
temp |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"arrowParens": "always", | ||
"bracketSpacing": true, | ||
"endOfLine": "lf", | ||
"printWidth": 100, | ||
"semi": true, | ||
"singleQuote": false, | ||
"tabWidth": 2 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
# tsp-client | ||
|
||
A simple command line tool for generating TypeSpec clients. | ||
|
||
### Usage | ||
``` | ||
tsp-client [options] <outputDir> | ||
``` | ||
|
||
## Options | ||
|
||
### <outputDir> (required) | ||
|
||
The only positional parameter. Specifies the directory to pass to the language emitter. | ||
|
||
### --emitter, -e (required) | ||
|
||
Specifies which language emitter to use. Current choices are "csharp", "java", "javascript", "python", "openapi". | ||
|
||
Aliases are also available, such as cs, js, py, and ts. | ||
|
||
### --mainFile, -m | ||
|
||
Used when specifying a URL to a TSP file directly. Not required if using a `tsp-location.yaml` | ||
|
||
### --debug, -d | ||
|
||
Enables verbose debug logging to the console. | ||
|
||
### --no-cleanup | ||
|
||
Disables automatic cleanup of the temporary directory where the TSP is written and referenced npm modules are installed. | ||
|
||
## Examples | ||
|
||
Generating from a TSP file to a particular directory: | ||
|
||
``` | ||
tsp-client -e openapi -m https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cognitiveservices/OpenAI.Inference/main.tsp ./temp | ||
``` | ||
|
||
Generating in a directory that contains a `tsp-location.yaml`: | ||
|
||
``` | ||
tsp-client sdk/openai/openai | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
#!/usr/bin/env node | ||
await import("../dist/index.js"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
{ | ||
"name": "@azure-tools/tsp-client", | ||
"version": "0.0.1", | ||
"private": "true", | ||
"description": "A tool to generate Azure SDKs from TypeSpec", | ||
"main": "dist/index.js", | ||
"scripts": { | ||
"build": "npm run clean && npm run build:tsc", | ||
"build:tsc": "tsc", | ||
"clean": "rimraf ./dist ./types", | ||
"example": "npx ts-node src/index.ts -e openapi -m https://raw.githubusercontent.com/Azure/azure-rest-api-specs/main/specification/cognitiveservices/OpenAI.Inference/main.tsp ./temp", | ||
"test": "mocha" | ||
}, | ||
"author": "Microsoft Corporation", | ||
"license": "MIT", | ||
"type": "module", | ||
"engines": { | ||
"node": ">=18.0.0" | ||
}, | ||
"bin": { | ||
"tsp-client": "cmd/tsp-client.js" | ||
}, | ||
"files": [ | ||
"dist", | ||
"cmd/tsp-client.js" | ||
], | ||
"devDependencies": { | ||
"@types/chai": "^4.3.5", | ||
"@types/mocha": "^10.0.1", | ||
"@types/node": "^20.4.8", | ||
"@types/prompt-sync": "^4.2.1", | ||
"chai": "^4.3.7", | ||
"mocha": "^10.2.0", | ||
"prettier": "^3.0.1", | ||
"rimraf": "^5.0.1", | ||
"ts-node": "^10.9.1", | ||
"typescript": "^5.1.6" | ||
}, | ||
"dependencies": { | ||
"@azure/core-rest-pipeline": "^1.12.0", | ||
"chalk": "^5.3.0", | ||
"prompt-sync": "^4.2.0", | ||
"yaml": "^2.3.1" | ||
}, | ||
"peerDependencies": { | ||
"@typespec/compiler": ">=0.48.1 <1.0.0" | ||
}, | ||
"mocha": { | ||
"extension": [ | ||
"ts" | ||
], | ||
"spec": "test/**/*.spec.ts", | ||
"loader": "ts-node/esm" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
export function createFileTree(url: string): FileTree { | ||
const rootUrl = url; | ||
const fileMap = new Map<string, string>(); | ||
|
||
function longestCommonPrefix(a: string, b: string): string { | ||
if (a === b) { | ||
return a; | ||
} | ||
let lastCommonSlash = -1; | ||
for (let i = 0; i < Math.min(a.length, b.length); i++) { | ||
if (a[i] === b[i]) { | ||
if (a[i] === "/") { | ||
lastCommonSlash = i; | ||
} | ||
} else { | ||
break; | ||
} | ||
} | ||
if (lastCommonSlash === -1) { | ||
throw new Error("no common prefix found"); | ||
} | ||
return a.slice(0, lastCommonSlash + 1); | ||
} | ||
|
||
function findCommonRoot(): string { | ||
let candidate = ""; | ||
for (const fileUrl of fileMap.keys()) { | ||
const lastSlashIndex = fileUrl.lastIndexOf("/"); | ||
const dirUrl = fileUrl.slice(0, lastSlashIndex + 1); | ||
if (!candidate) { | ||
candidate = dirUrl; | ||
} else { | ||
candidate = longestCommonPrefix(candidate, dirUrl); | ||
} | ||
} | ||
return candidate; | ||
} | ||
|
||
return { | ||
addFile(url: string, contents: string): void { | ||
if (fileMap.has(url)) { | ||
throw new Error(`file already parsed: ${url}`); | ||
} | ||
fileMap.set(url, contents); | ||
}, | ||
async createTree(): Promise<FileTreeResult> { | ||
const outputFiles = new Map<string, string>(); | ||
// calculate the highest common root | ||
const root = findCommonRoot(); | ||
let mainFilePath = ""; | ||
for (const [url, contents] of fileMap.entries()) { | ||
const relativePath = url.slice(root.length); | ||
outputFiles.set(relativePath, contents); | ||
if (url === rootUrl) { | ||
mainFilePath = relativePath; | ||
} | ||
} | ||
if (!mainFilePath) { | ||
throw new RangeError( | ||
`Main file ${rootUrl} not added to FileTree. Did you forget to add it?`, | ||
); | ||
} | ||
return { | ||
mainFilePath, | ||
files: outputFiles, | ||
}; | ||
}, | ||
}; | ||
} | ||
|
||
export interface FileTreeResult { | ||
mainFilePath: string; | ||
files: Map<string, string>; | ||
} | ||
|
||
export interface FileTree { | ||
addFile(url: string, contents: string): void; | ||
createTree(): Promise<FileTreeResult>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { mkdir, rm, writeFile, stat, readFile, access } from "node:fs/promises"; | ||
import { FileTreeResult } from "./fileTree.js"; | ||
import * as path from "node:path"; | ||
import { Logger } from "./log.js"; | ||
import { parse as parseYaml } from "yaml"; | ||
|
||
export async function ensureDirectory(path: string) { | ||
await mkdir(path, { recursive: true }); | ||
} | ||
|
||
export async function removeDirectory(path: string) { | ||
await rm(path, { recursive: true, force: true }); | ||
} | ||
|
||
export async function createTempDirectory(outputDir: string): Promise<string> { | ||
const tempRoot = path.join(outputDir, "TempTypeSpecFiles"); | ||
await mkdir(tempRoot, { recursive: true }); | ||
Logger.debug(`Created temporary working directory ${tempRoot}`); | ||
return tempRoot; | ||
} | ||
|
||
export async function writeFileTree(rootDir: string, files: FileTreeResult["files"]) { | ||
for (const [relativeFilePath, contents] of files) { | ||
const filePath = path.join(rootDir, relativeFilePath); | ||
await ensureDirectory(path.dirname(filePath)); | ||
Logger.debug(`writing ${filePath}`); | ||
await writeFile(filePath, contents); | ||
} | ||
} | ||
|
||
export async function tryReadTspLocation(rootDir: string): Promise<string | undefined> { | ||
try { | ||
const yamlPath = path.resolve(rootDir, "tsp-location.yaml"); | ||
const fileStat = await stat(yamlPath); | ||
if (fileStat.isFile()) { | ||
const fileContents = await readFile(yamlPath, "utf8"); | ||
const locationYaml = parseYaml(fileContents); | ||
const { directory, commit, repo } = locationYaml; | ||
if (!directory || !commit || !repo) { | ||
throw new Error("Invalid tsp-location.yaml"); | ||
} | ||
// make GitHub URL | ||
return `https://raw.githubusercontent.com/${repo}/${commit}/${directory}/`; | ||
} | ||
} catch (e) { | ||
Logger.error(`Error reading tsp-location.yaml: ${e}`); | ||
} | ||
return undefined; | ||
} | ||
|
||
export async function readTspLocation(rootDir: string): Promise<[string, string, string, string[]]> { | ||
try { | ||
const yamlPath = path.resolve(rootDir, "tsp-location.yaml"); | ||
const fileStat = await stat(yamlPath); | ||
if (fileStat.isFile()) { | ||
const fileContents = await readFile(yamlPath, "utf8"); | ||
const locationYaml = parseYaml(fileContents); | ||
let { directory, commit, repo, additionalDirectories } = locationYaml; | ||
if (!directory || !commit || !repo) { | ||
throw new Error("Invalid tsp-location.yaml"); | ||
} | ||
Logger.info(`Additional directories: ${additionalDirectories}`) | ||
if (!additionalDirectories) { | ||
additionalDirectories = []; | ||
} | ||
return [ directory, commit, repo, additionalDirectories ]; | ||
} | ||
throw new Error("Could not find tsp-location.yaml"); | ||
} catch (e) { | ||
Logger.error(`Error reading tsp-location.yaml: ${e}`); | ||
throw e; | ||
} | ||
} | ||
|
||
|
||
export async function getEmitterFromRepoConfig(emitterPath: string): Promise<string> { | ||
await access(emitterPath); | ||
const data = await readFile(emitterPath, 'utf8'); | ||
const obj = JSON.parse(data); | ||
if (!obj || !obj.dependencies) { | ||
throw new Error("Invalid emitter-package.json"); | ||
} | ||
const languages: string[] = ["@azure-tools/typespec-", "@typespec/openapi3"]; | ||
for (const lang of languages) { | ||
const emitter = Object.keys(obj.dependencies).find((dep: string) => dep.startsWith(lang)); | ||
if (emitter) { | ||
Logger.info(`Found emitter package ${emitter}`); | ||
return emitter; | ||
} | ||
} | ||
throw new Error("Could not find emitter package"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { execSync, spawn } from "child_process"; | ||
|
||
export function getRepoRoot(): string { | ||
return execSync('git rev-parse --show-toplevel').toString().trim(); | ||
} | ||
|
||
export async function cloneRepo(rootUrl: string, cloneDir: string, repo: string): Promise<void> { | ||
return new Promise((resolve, reject) => { | ||
const git = spawn("git", ["clone", "--no-checkout", "--filter=tree:0", repo, cloneDir], { | ||
cwd: rootUrl, | ||
stdio: "inherit", | ||
}); | ||
git.once("exit", (code) => { | ||
if (code === 0) { | ||
resolve(); | ||
} else { | ||
reject(new Error(`git clone failed exited with code ${code}`)); | ||
} | ||
}); | ||
git.once("error", (err) => { | ||
reject(new Error(`git clone failed with error: ${err}`)); | ||
}); | ||
}); | ||
} | ||
|
||
|
||
export async function sparseCheckout(cloneDir: string): Promise<void> { | ||
return new Promise((resolve, reject) => { | ||
const git = spawn("git", ["sparse-checkout", "init"], { | ||
cwd: cloneDir, | ||
stdio: "inherit", | ||
}); | ||
git.once("exit", (code) => { | ||
if (code === 0) { | ||
resolve(); | ||
} else { | ||
reject(new Error(`git sparse-checkout failed exited with code ${code}`)); | ||
} | ||
}); | ||
git.once("error", (err) => { | ||
reject(new Error(`git sparse-checkout failed with error: ${err}`)); | ||
}); | ||
}); | ||
} | ||
|
||
export async function addSpecFiles(cloneDir: string, subDir: string): Promise<void> { | ||
return new Promise((resolve, reject) => { | ||
const git = spawn("git", ["sparse-checkout", "add", subDir], { | ||
cwd: cloneDir, | ||
stdio: "inherit", | ||
}); | ||
git.once("exit", (code) => { | ||
if (code === 0) { | ||
resolve(); | ||
} else { | ||
reject(new Error(`git sparse-checkout add failed exited with code ${code}`)); | ||
} | ||
}); | ||
git.once("error", (err) => { | ||
reject(new Error(`git sparse-checkout add failed with error: ${err}`)); | ||
}); | ||
}); | ||
} | ||
|
||
export async function checkoutCommit(cloneDir: string, commit: string): Promise<void> { | ||
return new Promise((resolve, reject) => { | ||
const git = spawn("git", ["checkout", commit], { | ||
cwd: cloneDir, | ||
stdio: "inherit", | ||
}); | ||
git.once("exit", (code) => { | ||
if (code === 0) { | ||
resolve(); | ||
} else { | ||
reject(new Error(`git checkout failed exited with code ${code}`)); | ||
} | ||
}); | ||
git.once("error", (err) => { | ||
reject(new Error(`git checkout failed with error: ${err}`)); | ||
}); | ||
}); | ||
} |
Oops, something went wrong.