Skip to content

Commit

Permalink
[tsp-client] Add command to generate config files (#9566)
Browse files Browse the repository at this point in the history
* add new command

* update docs

* update command

* tests + fixes

* update tests

* update azure-sdk/emitter-package-json-pinning logic

* add azure-sdk/emitter-package-json-pinning test

* update example file

* fixes

* increase timeout in case npm install takes longer

* update readme

---------

Co-authored-by: catalinaperalta <caperal@microsoft.com>
  • Loading branch information
catalinaperalta and cperaltah authored Jan 17, 2025
1 parent 6303e26 commit d3b34a0
Show file tree
Hide file tree
Showing 7 changed files with 312 additions and 2 deletions.
6 changes: 5 additions & 1 deletion tools/tsp-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ A simple command line tool to facilitate generating client libraries from TypeSp
## Installation

```
npm install @azure-tools/typespec-client-generator-cli
npm install -g @azure-tools/typespec-client-generator-cli
```

## Prerequisites
Expand Down Expand Up @@ -63,6 +63,10 @@ correspond to differences in the service, allowing you to focus only on differen

Sort an existing swagger specification to be the same content order with TypeSpec generated swagger. This will allow you to easily compare and identify differences between the existing swagger and TypeSpec generated one. You should run this command on existing swagger files and check them in prior to creating converted TypeSpec PRs.

### generate-config-files

Generate the `emitter-package.json` and `emitter-package-lock.json` used for tsp-client commands under the eng/ directory of your current repository. Run this command from the target repository and pass in the path to the package.json file of the emitter you want to use generate the configuration files.

### generate-lock-file

Generate an emitter-package-lock.json under the eng/ directory based on existing `<repo-root>/eng/emitter-package.json`.
Expand Down
55 changes: 55 additions & 0 deletions tools/tsp-client/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,61 @@ export async function convertCommand(argv: any): Promise<void> {
}
}

export async function generateConfigFilesCommand(argv: any) {
const outputDir = argv["output-dir"];
const repoRoot = await getRepoRoot(outputDir);
const packageJsonPath = normalizePath(resolve(argv["package-json"]));
const overridePath = argv["overrides"] ?? undefined;

if (packageJsonPath === undefined || !(await doesFileExist(packageJsonPath))) {
throw new Error(`package.json not found in: ${packageJsonPath ?? "[Not Specified]"}`);
}
Logger.info("Generating emitter-package.json file...");
const content = await readFile(packageJsonPath);
const packageJson: Record<string, any> = JSON.parse(content.toString());
const emitterPackageJson: Record<string, any> = {
name: "dist/src/index.js",
dependencies: {},
};

let overrideJson: Record<string, any> = {};
if (overridePath) {
overrideJson = JSON.parse((await readFile(overridePath)).toString()) ?? {};
}

// Add emitter as dependency
emitterPackageJson["dependencies"][packageJson["name"]] =
overrideJson[packageJson["name"]] ?? packageJson["version"];

delete overrideJson[packageJson["name"]];
const devDependencies: Record<string, any> = {};
const peerDependencies = packageJson["peerDependencies"] ?? {};
const possiblyPinnedPackages: Array<string> =
packageJson["azure-sdk/emitter-package-json-pinning"] ?? Object.keys(peerDependencies);

for (const pinnedPackage of possiblyPinnedPackages) {
const pinnedVersion = packageJson["devDependencies"][pinnedPackage];
if (pinnedVersion && !overrideJson[pinnedPackage]) {
Logger.info(`Pinning ${pinnedPackage} to ${pinnedVersion}`);
devDependencies[pinnedPackage] = pinnedVersion;
}
}

if (Object.keys(devDependencies).length > 0) {
emitterPackageJson["devDependencies"] = devDependencies;
}
if (Object.keys(overrideJson).length > 0) {
emitterPackageJson["overrides"] = overrideJson;
}
await writeFile(
joinPaths(repoRoot, "eng", "emitter-package.json"),
JSON.stringify(emitterPackageJson, null, 2),
);
Logger.info(`emitter-package.json file generated in '${joinPaths(repoRoot, "eng")}' directory`);

await generateLockFileCommand(argv);
}

export async function generateLockFileCommand(argv: any) {
const outputDir = argv["output-dir"];
const repoRoot = await getRepoRoot(outputDir);
Expand Down
21 changes: 21 additions & 0 deletions tools/tsp-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
compareCommand,
convertCommand,
generateCommand,
generateConfigFilesCommand,
generateLockFileCommand,
initCommand,
sortSwaggerCommand,
Expand Down Expand Up @@ -210,6 +211,26 @@ const parser = yargs(hideBin(process.argv))
await convertCommand(argv);
},
)
.command(
"generate-config-files",
"Generate emitter-package.json and emitter-package-lock.json files from a TypeSpec emitter's package.json",
(yargs: any) => {
return yargs
.option("package-json", {
type: "string",
description: "Path to the emitter's package.json file",
demandOption: true,
})
.option("overrides", {
type: "string",
description: "Path to an override config file for pinning specific dependencies",
});
},
async (argv: any) => {
argv["output-dir"] = resolveOutputDir(argv);
await generateConfigFilesCommand(argv);
},
)
.command(
"generate-lock-file",
"Generate a lock file under the eng/ directory from an existing emitter-package.json",
Expand Down
65 changes: 64 additions & 1 deletion tools/tsp-client/test/commands.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { cp, stat, rm } from "node:fs/promises";
import { cp, stat, rm, readFile } from "node:fs/promises";
import {
initCommand,
generateCommand,
syncCommand,
updateCommand,
generateLockFileCommand,
generateConfigFilesCommand,
} from "../src/commands.js";
import { afterAll, beforeAll, describe, it } from "vitest";
import { assert } from "chai";
import { getRepoRoot } from "../src/git.js";
import { cwd } from "node:process";
import { joinPaths } from "@typespec/compiler";
import { readTspLocation, removeDirectory } from "../src/fs.js";
import { doesFileExist } from "../src/network.js";

describe.sequential("Verify commands", () => {
let repoRoot;
Expand Down Expand Up @@ -238,4 +240,65 @@ describe.sequential("Verify commands", () => {
assert.fail("Failed to init. Error: " + error);
}
});

it("Generate config files", async () => {
try {
const args = {
"package-json": joinPaths(cwd(), "test", "examples", "package.json"),
};
repoRoot = await getRepoRoot(cwd());
await generateConfigFilesCommand(args);
assert.isTrue(await doesFileExist(joinPaths(repoRoot, "eng", "emitter-package.json")));
const emitterJson = JSON.parse(
await readFile(joinPaths(repoRoot, "eng", "emitter-package.json"), "utf8"),
);
assert.equal(emitterJson["dependencies"]["@azure-tools/typespec-python"], "0.37.3");
assert.equal(emitterJson["devDependencies"]["@typespec/compiler"], "~0.63.0");
assert.isUndefined(emitterJson["overrides"]);
assert.isTrue(await doesFileExist(joinPaths(repoRoot, "eng", "emitter-package-lock.json")));
} catch (error: any) {
assert.fail("Failed to generate tsp-client config files. Error: " + error);
}
}, 120000);

it("Generate config files with overrides", async () => {
try {
const args = {
"package-json": joinPaths(cwd(), "test", "examples", "package.json"),
overrides: joinPaths(cwd(), "test", "examples", "overrides.json"),
};
repoRoot = await getRepoRoot(cwd());
await generateConfigFilesCommand(args);
assert.isTrue(await doesFileExist(joinPaths(repoRoot, "eng", "emitter-package.json")));
const emitterJson = JSON.parse(
await readFile(joinPaths(repoRoot, "eng", "emitter-package.json"), "utf8"),
);
assert.equal(emitterJson["dependencies"]["@azure-tools/typespec-python"], "0.36.0");
assert.exists(emitterJson["overrides"]);
assert.equal(emitterJson["overrides"]["@typespec/compiler"], "0.61.0");
assert.isTrue(await doesFileExist(joinPaths(repoRoot, "eng", "emitter-package-lock.json")));
} catch (error: any) {
assert.fail("Failed to generate tsp-client config files. Error: " + error);
}
}, 120000);

it("Generate config files using azure-sdk/emitter-package-json-pinning", async () => {
try {
const args = {
"package-json": joinPaths(cwd(), "test", "examples", "package-sdk-pinning.json"),
};
repoRoot = await getRepoRoot(cwd());
await generateConfigFilesCommand(args);
assert.isTrue(await doesFileExist(joinPaths(repoRoot, "eng", "emitter-package.json")));
const emitterJson = JSON.parse(
await readFile(joinPaths(repoRoot, "eng", "emitter-package.json"), "utf8"),
);
assert.equal(emitterJson["dependencies"]["@azure-tools/typespec-python"], "0.37.3");
assert.equal(Object.keys(emitterJson["devDependencies"]).length, 1);
assert.equal(emitterJson["devDependencies"]["@typespec/compiler"], "~0.64.0");
assert.isTrue(await doesFileExist(joinPaths(repoRoot, "eng", "emitter-package-lock.json")));
} catch (error: any) {
assert.fail("Failed to generate tsp-client config files. Error: " + error);
}
}, 120000);
});
4 changes: 4 additions & 0 deletions tools/tsp-client/test/examples/overrides.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"@azure-tools/typespec-python": "0.36.0",
"@typespec/compiler": "0.61.0"
}
83 changes: 83 additions & 0 deletions tools/tsp-client/test/examples/package-sdk-pinning.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
{
"name": "@azure-tools/typespec-python",
"version": "0.37.3",
"author": "Microsoft Corporation",
"description": "Example package.json for tsp-client",
"license": "MIT",
"type": "module",
"main": "dist/src/index.js",
"exports": {
".": "./dist/src/index.js",
"./testing": "./dist/src/testing/index.js"
},
"tspMain": "dist/src/index.js",
"engines": {
"node": ">=14.0.0"
},
"scripts": {
"clean": "rimraf ./dist ./temp ./venv ./node_modules",
"build": "tsc -p .",
"watch": "tsc -p . --watch",
"install": "tsx ./scripts/run-python3.ts ./scripts/install.py",
"prepare": "tsx ./scripts/run-python3.ts ./scripts/prepare.py",
"lint": "tsx ./scripts/eng/lint.ts",
"lint:fix": "eslint . --fix --ext .ts",
"format": "npx prettier **/*.ts --write && tsx ./scripts/eng/format.ts",
"regenerate": "tsx ./scripts/eng/regenerate.ts",
"test": "tsx ./scripts/eng/run-tests.ts"
},
"files": [
"dist/**",
"!dist/test/**",
"scripts/**",
"generator/**"
],
"peerDependencies": {
"@typespec/compiler": ">=0.63.0 <1.0.0",
"@typespec/http": ">=0.63.0 <1.0.0",
"@typespec/rest": ">=0.63.0 <1.0.0",
"@typespec/versioning": ">=0.63.0 <1.0.0",
"@typespec/openapi": ">=0.63.0 <1.0.0",
"@azure-tools/typespec-azure-core": ">=0.49.0 <1.0.0",
"@azure-tools/typespec-azure-resource-manager": ">=0.49.0 <1.0.0",
"@azure-tools/typespec-autorest": ">=0.49.0 <1.0.0",
"@azure-tools/typespec-azure-rulesets": ">=0.49.0 <3.0.0",
"@azure-tools/typespec-client-generator-core": ">=0.49.1 <1.0.0"
},
"dependencies": {
"js-yaml": "~4.1.0",
"semver": "~7.6.2",
"tsx": "~4.19.1",
"@typespec/http-client-python": "~0.5.1",
"fs-extra": "~11.2.0"
},
"devDependencies": {
"@typespec/compiler": "~0.64.0",
"@typespec/http": "~0.63.0",
"@typespec/rest": "~0.63.0",
"@typespec/versioning": "~0.63.0",
"@typespec/openapi": "~0.63.0",
"@azure-tools/typespec-azure-resource-manager": "~0.49.0",
"@azure-tools/typespec-azure-core": "~0.49.0",
"@azure-tools/typespec-azure-rulesets": "~0.49.0",
"@azure-tools/typespec-autorest": "~0.49.0",
"@azure-tools/typespec-client-generator-core": "~0.49.1",
"@azure-tools/azure-http-specs": "0.1.0-alpha.4",
"@typespec/http-specs": "0.1.0-alpha.5",
"@types/js-yaml": "~4.0.5",
"@types/node": "~22.5.4",
"@types/yargs": "~17.0.33",
"@types/semver": "7.5.8",
"c8": "^10.1.2",
"vitest": "^2.1.2",
"rimraf": "~6.0.1",
"typescript": "~5.6.3",
"typescript-eslint": "^8.8.1",
"yargs": "~17.7.2",
"chalk": "5.3.0",
"@types/fs-extra": "11.0.4"
},
"azure-sdk/emitter-package-json-pinning": [
"@typespec/compiler"
]
}
80 changes: 80 additions & 0 deletions tools/tsp-client/test/examples/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{
"name": "@azure-tools/typespec-python",
"version": "0.37.3",
"author": "Microsoft Corporation",
"description": "Example package.json for tsp-client",
"license": "MIT",
"type": "module",
"main": "dist/src/index.js",
"exports": {
".": "./dist/src/index.js",
"./testing": "./dist/src/testing/index.js"
},
"tspMain": "dist/src/index.js",
"engines": {
"node": ">=14.0.0"
},
"scripts": {
"clean": "rimraf ./dist ./temp ./venv ./node_modules",
"build": "tsc -p .",
"watch": "tsc -p . --watch",
"install": "tsx ./scripts/run-python3.ts ./scripts/install.py",
"prepare": "tsx ./scripts/run-python3.ts ./scripts/prepare.py",
"lint": "tsx ./scripts/eng/lint.ts",
"lint:fix": "eslint . --fix --ext .ts",
"format": "npx prettier **/*.ts --write && tsx ./scripts/eng/format.ts",
"regenerate": "tsx ./scripts/eng/regenerate.ts",
"test": "tsx ./scripts/eng/run-tests.ts"
},
"files": [
"dist/**",
"!dist/test/**",
"scripts/**",
"generator/**"
],
"peerDependencies": {
"@typespec/compiler": ">=0.63.0 <1.0.0",
"@typespec/http": ">=0.63.0 <1.0.0",
"@typespec/rest": ">=0.63.0 <1.0.0",
"@typespec/versioning": ">=0.63.0 <1.0.0",
"@typespec/openapi": ">=0.63.0 <1.0.0",
"@azure-tools/typespec-azure-core": ">=0.49.0 <1.0.0",
"@azure-tools/typespec-azure-resource-manager": ">=0.49.0 <1.0.0",
"@azure-tools/typespec-autorest": ">=0.49.0 <1.0.0",
"@azure-tools/typespec-azure-rulesets": ">=0.49.0 <3.0.0",
"@azure-tools/typespec-client-generator-core": ">=0.49.1 <1.0.0"
},
"dependencies": {
"js-yaml": "~4.1.0",
"semver": "~7.6.2",
"tsx": "~4.19.1",
"@typespec/http-client-python": "~0.5.1",
"fs-extra": "~11.2.0"
},
"devDependencies": {
"@typespec/compiler": "~0.63.0",
"@typespec/http": "~0.63.0",
"@typespec/rest": "~0.63.0",
"@typespec/versioning": "~0.63.0",
"@typespec/openapi": "~0.63.0",
"@azure-tools/typespec-azure-resource-manager": "~0.49.0",
"@azure-tools/typespec-azure-core": "~0.49.0",
"@azure-tools/typespec-azure-rulesets": "~0.49.0",
"@azure-tools/typespec-autorest": "~0.49.0",
"@azure-tools/typespec-client-generator-core": "~0.49.1",
"@azure-tools/azure-http-specs": "0.1.0-alpha.4",
"@typespec/http-specs": "0.1.0-alpha.5",
"@types/js-yaml": "~4.0.5",
"@types/node": "~22.5.4",
"@types/yargs": "~17.0.33",
"@types/semver": "7.5.8",
"c8": "^10.1.2",
"vitest": "^2.1.2",
"rimraf": "~6.0.1",
"typescript": "~5.6.3",
"typescript-eslint": "^8.8.1",
"yargs": "~17.7.2",
"chalk": "5.3.0",
"@types/fs-extra": "11.0.4"
}
}

0 comments on commit d3b34a0

Please sign in to comment.