Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[tsp-client] Add command to generate config files #9566

Merged
merged 12 commits into from
Jan 17, 2025
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.

### 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 possiblyPinnedPackages =
packageJson["azure-sdk/emitter-package-json-pinning"] ?? packageJson["peerDependencies"];
catalinaperalta marked this conversation as resolved.
Show resolved Hide resolved

for (const pinnedPackage in 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
44 changes: 43 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,44 @@ describe.sequential("Verify commands", () => {
assert.fail("Failed to init. Error: " + error);
}
});

it("Generate config files", async () => {
try {
const args = {
"package-json": joinPaths(cwd(), "./test/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.isUndefined(emitterJson["overrides"]);
catalinaperalta marked this conversation as resolved.
Show resolved Hide resolved
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);
}
});

it("Generate config files with overrides", async () => {
try {
const args = {
"package-json": joinPaths(cwd(), "test", "package.json"),
overrides: joinPaths(cwd(), "test", "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);
}
});
});
4 changes: 4 additions & 0 deletions tools/tsp-client/test/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"
}
80 changes: 80 additions & 0 deletions tools/tsp-client/test/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"
}
}
Loading