diff --git a/.chronus/changes/generateSDK-2024-11-10-10-45-35.md b/.chronus/changes/generateSDK-2024-11-10-10-45-35.md
new file mode 100644
index 0000000000..6c70a9ff96
--- /dev/null
+++ b/.chronus/changes/generateSDK-2024-11-10-10-45-35.md
@@ -0,0 +1,7 @@
+---
+changeKind: feature
+packages:
+ - typespec-vscode
+---
+
+integrate client SDK generation
\ No newline at end of file
diff --git a/.chronus/changes/generateSDK-2025-0-10-0-6-24.md b/.chronus/changes/generateSDK-2025-0-10-0-6-24.md
new file mode 100644
index 0000000000..3f6bd062e8
--- /dev/null
+++ b/.chronus/changes/generateSDK-2025-0-10-0-6-24.md
@@ -0,0 +1,7 @@
+---
+changeKind: fix
+packages:
+ - "@typespec/internal-build-utils"
+---
+
+resolve the program crash when there is no package name in package.json
\ No newline at end of file
diff --git a/packages/internal-build-utils/src/generate-third-party-notice.ts b/packages/internal-build-utils/src/generate-third-party-notice.ts
index 68ecb9123b..4e22acc398 100644
--- a/packages/internal-build-utils/src/generate-third-party-notice.ts
+++ b/packages/internal-build-utils/src/generate-third-party-notice.ts
@@ -52,17 +52,23 @@ async function findThirdPartyPackages() {
const sources = contents.sources;
for (const source of sources) {
const sourcePath = resolve(dirname(map), source);
- const packageRoot = await getPackageRoot(sourcePath);
+ let packageRoot = await getPackageRoot(sourcePath);
if (packageRoot === undefined) {
continue;
}
- const pkg = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf-8"));
-
- if (pkg.name === rootName || /microsoft/i.test(JSON.stringify(pkg.author))) {
- continue;
- }
-
if (!packages.has(packageRoot)) {
+ let pkg = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf-8"));
+
+ if (!pkg.name) {
+ packageRoot = await getPackageRoot(packageRoot);
+ while (!pkg.name && packageRoot) {
+ pkg = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf-8"));
+ packageRoot = await getPackageRoot(packageRoot);
+ }
+ }
+ if (!pkg.name || pkg.name === rootName || /microsoft/i.test(JSON.stringify(pkg.author))) {
+ continue;
+ }
packages.set(packageRoot, pkg);
}
}
diff --git a/packages/typespec-vscode/ThirdPartyNotices.txt b/packages/typespec-vscode/ThirdPartyNotices.txt
index 681acb4e65..301be2d7f7 100644
--- a/packages/typespec-vscode/ThirdPartyNotices.txt
+++ b/packages/typespec-vscode/ThirdPartyNotices.txt
@@ -12,6 +12,7 @@ granted herein, whether by implication, estoppel or otherwise.
2. brace-expansion version 2.0.1 (https://github.com/juliangruber/brace-expansion)
3. minimatch version 5.1.6 (https://github.com/isaacs/minimatch)
4. semver version 7.6.3 (https://github.com/npm/node-semver)
+5. yaml version 2.5.1 (github:eemeli/yaml)
%% balanced-match NOTICES AND INFORMATION BEGIN HERE
@@ -111,4 +112,32 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
=====================================================");
-END OF semver NOTICES AND INFORMATION
\ No newline at end of file
+END OF semver NOTICES AND INFORMATION
+
+
+%% yaml NOTICES AND INFORMATION BEGIN HERE
+=====================================================
+ MIT License
+
+ Copyright (c) Microsoft Corporation. All rights reserved.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE
+
+=====================================================");
+END OF yaml NOTICES AND INFORMATION
\ No newline at end of file
diff --git a/packages/typespec-vscode/icons/client.dark.svg b/packages/typespec-vscode/icons/client.dark.svg
new file mode 100644
index 0000000000..18aa6ebd5d
--- /dev/null
+++ b/packages/typespec-vscode/icons/client.dark.svg
@@ -0,0 +1,12 @@
+
\ No newline at end of file
diff --git a/packages/typespec-vscode/icons/client.light.svg b/packages/typespec-vscode/icons/client.light.svg
new file mode 100644
index 0000000000..b493a458d1
--- /dev/null
+++ b/packages/typespec-vscode/icons/client.light.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/packages/typespec-vscode/icons/dotnet.dark.svg b/packages/typespec-vscode/icons/dotnet.dark.svg
new file mode 100644
index 0000000000..39be6a20c6
--- /dev/null
+++ b/packages/typespec-vscode/icons/dotnet.dark.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/packages/typespec-vscode/icons/dotnet.light.svg b/packages/typespec-vscode/icons/dotnet.light.svg
new file mode 100644
index 0000000000..9c614b11d5
--- /dev/null
+++ b/packages/typespec-vscode/icons/dotnet.light.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/packages/typespec-vscode/icons/java.dark.svg b/packages/typespec-vscode/icons/java.dark.svg
new file mode 100644
index 0000000000..649dcb26c8
--- /dev/null
+++ b/packages/typespec-vscode/icons/java.dark.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/packages/typespec-vscode/icons/java.light.svg b/packages/typespec-vscode/icons/java.light.svg
new file mode 100644
index 0000000000..649dcb26c8
--- /dev/null
+++ b/packages/typespec-vscode/icons/java.light.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/packages/typespec-vscode/icons/javascript.dark.svg b/packages/typespec-vscode/icons/javascript.dark.svg
new file mode 100644
index 0000000000..978d95f5cd
--- /dev/null
+++ b/packages/typespec-vscode/icons/javascript.dark.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/packages/typespec-vscode/icons/javascript.light.svg b/packages/typespec-vscode/icons/javascript.light.svg
new file mode 100644
index 0000000000..a400bccfd0
--- /dev/null
+++ b/packages/typespec-vscode/icons/javascript.light.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/packages/typespec-vscode/icons/openapi.dark.svg b/packages/typespec-vscode/icons/openapi.dark.svg
new file mode 100644
index 0000000000..3f05bbddf6
--- /dev/null
+++ b/packages/typespec-vscode/icons/openapi.dark.svg
@@ -0,0 +1,16 @@
+
\ No newline at end of file
diff --git a/packages/typespec-vscode/icons/openapi.light.svg b/packages/typespec-vscode/icons/openapi.light.svg
new file mode 100644
index 0000000000..3f05bbddf6
--- /dev/null
+++ b/packages/typespec-vscode/icons/openapi.light.svg
@@ -0,0 +1,16 @@
+
\ No newline at end of file
diff --git a/packages/typespec-vscode/icons/openapi3.dark.svg b/packages/typespec-vscode/icons/openapi3.dark.svg
new file mode 100644
index 0000000000..22b781fe93
--- /dev/null
+++ b/packages/typespec-vscode/icons/openapi3.dark.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/packages/typespec-vscode/icons/openapi3.light.svg b/packages/typespec-vscode/icons/openapi3.light.svg
new file mode 100644
index 0000000000..22b781fe93
--- /dev/null
+++ b/packages/typespec-vscode/icons/openapi3.light.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/packages/typespec-vscode/icons/python.dark.svg b/packages/typespec-vscode/icons/python.dark.svg
new file mode 100644
index 0000000000..5e792117a7
--- /dev/null
+++ b/packages/typespec-vscode/icons/python.dark.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/packages/typespec-vscode/icons/python.light.svg b/packages/typespec-vscode/icons/python.light.svg
new file mode 100644
index 0000000000..3490261bed
--- /dev/null
+++ b/packages/typespec-vscode/icons/python.light.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/packages/typespec-vscode/icons/server.dark.svg b/packages/typespec-vscode/icons/server.dark.svg
new file mode 100644
index 0000000000..8289d77d3a
--- /dev/null
+++ b/packages/typespec-vscode/icons/server.dark.svg
@@ -0,0 +1,27 @@
+
\ No newline at end of file
diff --git a/packages/typespec-vscode/icons/server.light.svg b/packages/typespec-vscode/icons/server.light.svg
new file mode 100644
index 0000000000..828b762e17
--- /dev/null
+++ b/packages/typespec-vscode/icons/server.light.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/packages/typespec-vscode/package.json b/packages/typespec-vscode/package.json
index a0e1feb1bc..ac437f9cab 100644
--- a/packages/typespec-vscode/package.json
+++ b/packages/typespec-vscode/package.json
@@ -108,6 +108,104 @@
],
"default": "off",
"description": "Define whether/how the TypeSpec language server should send traces to client. For the traces to show properly in vscode Output, make sure 'Log Level' is also set to 'Trace' so that they won't be filtered at client side, which can be set through 'Developer: Set Log Level...' command."
+ },
+ "typespec.generateCode.emitters": {
+ "scope": "window",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "language": {
+ "type": "string",
+ "enum": [
+ ".NET",
+ "Java",
+ "JavaScript",
+ "Python",
+ "Go",
+ "OpenAPI3",
+ "ProtoBuf",
+ "JsonSchema"
+ ],
+ "description": "Define the language the emitter will emit."
+ },
+ "package": {
+ "type": "string",
+ "description": "Define the emitter package.\n\nExample (with version): @typespec/http-client-csharp@1.0.0\n\nExample (without version): @typespec/http-client-csharp"
+ },
+ "sourceRepo": {
+ "type": "string",
+ "description": "Define the source repository of the emitter package."
+ },
+ "requisites": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Define the requisites of the emitter package."
+ },
+ "kind": {
+ "type": "string",
+ "enum": [
+ "client",
+ "server",
+ "openapi"
+ ],
+ "description": "Define the emitter kind."
+ }
+ }
+ },
+ "default": [
+ {
+ "language": ".NET",
+ "package": "@typespec/http-client-csharp",
+ "sourceRepo": "https://github.com/microsoft/typespec/tree/main/packages/http-client-csharp",
+ "requisites": [
+ ".NET 8.0 SDK"
+ ],
+ "kind": "client"
+ },
+ {
+ "language": "Java",
+ "package": "@typespec/http-client-java",
+ "sourceRepo": "https://github.com/microsoft/typespec/tree/main/packages/http-client-java",
+ "requisites": [
+ "Java 17 or above",
+ "Maven"
+ ],
+ "kind": "client"
+ },
+ {
+ "language": "JavaScript",
+ "package": "@azure-tools/typespec-ts",
+ "sourceRepo": "https://github.com/Azure/autorest.typescript/tree/main/packages/typespec-ts",
+ "kind": "client"
+ },
+ {
+ "language": "Python",
+ "package": "@typespec/http-client-python",
+ "sourceRepo": "https://github.com/microsoft/typespec/tree/main/packages/http-client-python",
+ "kind": "client"
+ },
+ {
+ "language": ".NET",
+ "package": "@typespec/http-server-csharp",
+ "sourceRepo": "https://github.com/microsoft/typespec/tree/main/packages/http-server-csharp",
+ "kind": "server"
+ },
+ {
+ "language": "JavaScript",
+ "package": "@typespec/http-server-javascript",
+ "sourceRepo": "https://github.com/microsoft/typespec/tree/main/packages/http-server-javascript",
+ "kind": "server"
+ },
+ {
+ "language": "OpenAPI3",
+ "package": "@typespec/openapi3",
+ "sourceRepo": "https://github.com/microsoft/typespec/tree/main/packages/openapi3",
+ "kind": "openapi"
+ }
+ ]
}
}
}
@@ -141,6 +239,11 @@
"title": "Show Output Channel",
"category": "TypeSpec"
},
+ {
+ "command": "typespec.generateCode",
+ "title": "Generate from TypeSpec",
+ "category": "TypeSpec"
+ },
{
"command": "typespec.createProject",
"title": "Create TypeSpec Project",
@@ -152,6 +255,22 @@
"category": "TypeSpec"
}
],
+ "menus": {
+ "explorer/context": [
+ {
+ "command": "typespec.generateCode",
+ "when": "explorerResourceIsFolder || resourceLangId == typespec",
+ "group": "code_generation"
+ }
+ ],
+ "editor/context": [
+ {
+ "command": "typespec.generateCode",
+ "when": "resourceLangId == typespec",
+ "group": "code_generation"
+ }
+ ]
+ },
"semanticTokenScopes": [
{
"scopes": {
@@ -229,6 +348,7 @@
"typescript": "~5.6.3",
"vitest": "^2.1.5",
"vscode-languageclient": "~9.0.1",
- "semver": "^7.6.3"
+ "semver": "^7.6.3",
+ "yaml": "~2.5.1"
}
}
diff --git a/packages/typespec-vscode/src/const.ts b/packages/typespec-vscode/src/const.ts
new file mode 100644
index 0000000000..8a5d3e4b8f
--- /dev/null
+++ b/packages/typespec-vscode/src/const.ts
@@ -0,0 +1,2 @@
+export const StartFileName = "main.tsp";
+export const TspConfigFileName = "tspconfig.yaml";
diff --git a/packages/typespec-vscode/src/extension.ts b/packages/typespec-vscode/src/extension.ts
index da1b73a8ae..2d0f0cc45a 100644
--- a/packages/typespec-vscode/src/extension.ts
+++ b/packages/typespec-vscode/src/extension.ts
@@ -13,6 +13,7 @@ import {
SettingName,
} from "./types.js";
import { createTypeSpecProject } from "./vscode-cmd/create-tsp-project.js";
+import { emitCode } from "./vscode-cmd/emit-code/emit-code.js";
import { installCompilerGlobally } from "./vscode-cmd/install-tsp-compiler.js";
let client: TspLanguageClient | undefined;
@@ -44,6 +45,20 @@ export async function activate(context: ExtensionContext) {
}),
);
+ /* emit command. */
+ context.subscriptions.push(
+ commands.registerCommand(CommandName.GenerateCode, async (uri: vscode.Uri) => {
+ await vscode.window.withProgress(
+ {
+ location: vscode.ProgressLocation.Window,
+ title: "Generate from TypeSpec...",
+ cancellable: false,
+ },
+ async () => await emitCode(context, uri),
+ );
+ }),
+ );
+
context.subscriptions.push(
commands.registerCommand(
CommandName.RestartServer,
diff --git a/packages/typespec-vscode/src/npm-utils.ts b/packages/typespec-vscode/src/npm-utils.ts
new file mode 100644
index 0000000000..d3b5f61867
--- /dev/null
+++ b/packages/typespec-vscode/src/npm-utils.ts
@@ -0,0 +1,163 @@
+import { readFile } from "fs/promises";
+import path from "path";
+import semver from "semver";
+import logger from "./log/logger.js";
+import { ExecOutput, loadModule, spawnExecutionAndLogToOutput } from "./utils.js";
+
+export enum InstallAction {
+ Install = "Install",
+ Upgrade = "Upgrade",
+ Skip = "Skip",
+ Cancel = "Cancel",
+}
+
+export enum npmDependencyType {
+ dependencies = "dependencies",
+ peerDependencies = "peerDependencies",
+ devDependencies = "devDependencies",
+}
+
+export interface NpmPackageInfo {
+ name: string;
+ version?: string;
+ resolved?: string;
+ overridden?: string;
+ dependencies?: Record;
+}
+
+export class NpmUtil {
+ private cwd: string;
+
+ constructor(cwd: string) {
+ this.cwd = cwd;
+ }
+
+ public async npmInstallPackages(packages: string[] = [], options: any = {}): Promise {
+ return spawnExecutionAndLogToOutput("npm", ["install", ...packages], this.cwd);
+ }
+
+ /* identify the action to take for a package. install or skip or cancel or upgrade */
+ public async calculateNpmPackageInstallAction(
+ packageName: string,
+ version?: string,
+ ): Promise<{ action: InstallAction; version?: string }> {
+ const { installed: isPackageInstalled, version: installedVersion } =
+ await this.isPackageInstalled(packageName);
+ if (isPackageInstalled) {
+ if (version && installedVersion !== version) {
+ if (semver.gt(version, installedVersion!)) {
+ return { action: InstallAction.Upgrade, version: version };
+ } else {
+ logger.info(
+ "The version to intall is less than the installed version. Skip installation.",
+ );
+ return { action: InstallAction.Skip, version: installedVersion };
+ }
+ }
+ return { action: InstallAction.Skip, version: installedVersion };
+ } else {
+ return { action: InstallAction.Install, version: version };
+ }
+ }
+
+ /* identify the dependency packages need to be upgraded */
+ public async calculateNpmPackageDependencyToUpgrade(
+ packageName: string,
+ version?: string,
+ dependencyTypes: npmDependencyType[] = [npmDependencyType.dependencies],
+ ): Promise<{ name: string; version: string }[]> {
+ const dependenciesToInstall: { name: string; version: string }[] = [];
+ let packageFullName = packageName;
+ if (version) {
+ packageFullName = `${packageName}@${version}`;
+ }
+
+ /* get dependencies. */
+ if (dependencyTypes.length === 0) {
+ logger.info("No dependency to check.");
+ return dependenciesToInstall;
+ }
+
+ try {
+ const dependenciesResult = await spawnExecutionAndLogToOutput(
+ "npm",
+ ["view", packageFullName, ...dependencyTypes, "--json"],
+ this.cwd,
+ );
+
+ if (dependenciesResult.exitCode === 0) {
+ const json = JSON.parse(dependenciesResult.stdout);
+ const jsonDependencies: any[] = [];
+ if (dependencyTypes.length > 1) {
+ jsonDependencies.push(...Object.values(json));
+ } else {
+ jsonDependencies.push(json);
+ }
+ const dependencies = parseDependency(jsonDependencies);
+ for (const [key, value] of Object.entries(dependencies)) {
+ const { installed, version: installedVersion } = await this.isPackageInstalled(key);
+ if (installed && installedVersion) {
+ if (!this.isValidVersion(installedVersion, value.join("||"))) {
+ dependenciesToInstall.push({ name: key, version: "latest" });
+ }
+ }
+ }
+ } else {
+ logger.error("Error getting dependencies.", [dependenciesResult.stderr]);
+ }
+ } catch (err) {
+ logger.error("Error getting dependencies.", [err]);
+ }
+
+ function parseDependency(jsonDependencies: any[]): Record {
+ const dependencies: Record = {};
+ for (const dependency of jsonDependencies) {
+ for (const [key, value] of Object.entries(dependency)) {
+ if (dependencies[key]) {
+ dependencies[key].push(value as string);
+ } else {
+ dependencies[key] = [value as string];
+ }
+ }
+ }
+ return dependencies;
+ }
+
+ return dependenciesToInstall;
+ }
+
+ private isValidVersion(version: string, range: string): boolean {
+ return semver.satisfies(version, range);
+ }
+
+ private async isPackageInstalled(
+ packageName: string,
+ ): Promise<{ installed: boolean; version: string | undefined }> {
+ const packageInfo = await this.loadNpmPackage(packageName);
+ if (packageInfo) return { installed: true, version: packageInfo.version };
+ return { installed: false, version: undefined };
+ }
+
+ private async loadNpmPackage(packageName: string): Promise {
+ const executable = await loadModule(this.cwd, packageName);
+ if (executable) {
+ const packageJsonPath = path.resolve(executable.path, "package.json");
+
+ /* get the package version. */
+ let version;
+ try {
+ const data = await readFile(packageJsonPath, { encoding: "utf-8" });
+ const packageJson = JSON.parse(data);
+ version = packageJson.version;
+ } catch (error) {
+ logger.error("Error reading package.json.", [error]);
+ }
+ return {
+ name: packageName,
+ version: version,
+ };
+ }
+
+ return undefined;
+ }
+}
diff --git a/packages/typespec-vscode/src/task-provider.ts b/packages/typespec-vscode/src/task-provider.ts
index 39d8da558e..c0f0c16ee5 100644
--- a/packages/typespec-vscode/src/task-provider.ts
+++ b/packages/typespec-vscode/src/task-provider.ts
@@ -1,6 +1,7 @@
import { resolve } from "path";
import vscode, { workspace } from "vscode";
import { Executable } from "vscode-languageclient/node.js";
+import { StartFileName } from "./const.js";
import logger from "./log/logger.js";
import { normalizeSlashes } from "./path-utils.js";
import { resolveTypeSpecCli } from "./tsp-executable-resolver.js";
@@ -11,13 +12,13 @@ export function createTaskProvider() {
provideTasks: async () => {
logger.info("Providing tsp tasks");
const targetPathes = await vscode.workspace
- .findFiles("**/main.tsp", "**/node_modules/**")
+ .findFiles(`**/${StartFileName}`, "**/node_modules/**")
.then((uris) =>
uris
.filter((uri) => uri.scheme === "file" && !uri.fsPath.includes("node_modules"))
.map((uri) => normalizeSlashes(uri.fsPath)),
);
- logger.info(`Found ${targetPathes.length} main.tsp files`);
+ logger.info(`Found ${targetPathes.length} ${StartFileName} files`);
const tasks: vscode.Task[] = [];
for (const targetPath of targetPathes) {
tasks.push(...(await createBuiltInTasks(targetPath)));
diff --git a/packages/typespec-vscode/src/tsp-language-client.ts b/packages/typespec-vscode/src/tsp-language-client.ts
index 43b24433fb..e72b263a8d 100644
--- a/packages/typespec-vscode/src/tsp-language-client.ts
+++ b/packages/typespec-vscode/src/tsp-language-client.ts
@@ -7,6 +7,7 @@ import type {
} from "@typespec/compiler";
import { ExtensionContext, LogOutputChannel, RelativePattern, workspace } from "vscode";
import { Executable, LanguageClient, LanguageClientOptions } from "vscode-languageclient/node.js";
+import { TspConfigFileName } from "./const.js";
import logger from "./log/logger.js";
import { resolveTypeSpecServer } from "./tsp-executable-resolver.js";
import {
@@ -191,7 +192,7 @@ export class TspLanguageClient {
workspace.createFileSystemWatcher("**/*.cadl"),
workspace.createFileSystemWatcher("**/cadl-project.yaml"),
workspace.createFileSystemWatcher("**/*.tsp"),
- workspace.createFileSystemWatcher("**/tspconfig.yaml"),
+ workspace.createFileSystemWatcher(`**/${TspConfigFileName}`),
// please be aware that the vscode watch with '**' will honer the files.watcherExclude settings
// so we won't get notification for those package.json under node_modules
// if our customers exclude the node_modules folder in files.watcherExclude settings.
@@ -213,7 +214,7 @@ export class TspLanguageClient {
documentSelector: [
{ scheme: "file", language: "typespec" },
{ scheme: "untitled", language: "typespec" },
- { scheme: "file", language: "yaml", pattern: "**/tspconfig.yaml" },
+ { scheme: "file", language: "yaml", pattern: `**/${TspConfigFileName}` },
],
outputChannel,
};
diff --git a/packages/typespec-vscode/src/types.ts b/packages/typespec-vscode/src/types.ts
index 99af0a7fa0..9abd25fb26 100644
--- a/packages/typespec-vscode/src/types.ts
+++ b/packages/typespec-vscode/src/types.ts
@@ -1,6 +1,7 @@
export const enum SettingName {
TspServerPath = "typespec.tsp-server.path",
InitTemplatesUrls = "typespec.initTemplatesUrls",
+ GenerateCodeEmitters = "typespec.generateCode.emitters",
}
export const enum CommandName {
@@ -9,6 +10,7 @@ export const enum CommandName {
InstallGlobalCompilerCli = "typespec.installGlobalCompilerCli",
CreateProject = "typespec.createProject",
OpenUrl = "typespec.openUrl",
+ GenerateCode = "typespec.generateCode",
}
export interface InstallGlobalCliCommandArgs {
diff --git a/packages/typespec-vscode/src/typespec-utils.ts b/packages/typespec-vscode/src/typespec-utils.ts
new file mode 100644
index 0000000000..fc112a6cc5
--- /dev/null
+++ b/packages/typespec-vscode/src/typespec-utils.ts
@@ -0,0 +1,55 @@
+import { readFile } from "fs/promises";
+import path from "path";
+import vscode from "vscode";
+import { StartFileName } from "./const.js";
+import logger from "./log/logger.js";
+import { getDirectoryPath, normalizeSlashes } from "./path-utils.js";
+import { isFile } from "./utils.js";
+
+export async function getEntrypointTspFile(tspPath: string): Promise {
+ const isFilePath = await isFile(tspPath);
+ let baseDir = isFilePath ? getDirectoryPath(tspPath) : tspPath;
+
+ while (true) {
+ const pkgPath = path.resolve(baseDir, "package.json");
+ if (await isFile(pkgPath)) {
+ /* get the tspMain from package.json. */
+ try {
+ const data = await readFile(pkgPath, { encoding: "utf-8" });
+ const packageJson = JSON.parse(data);
+ const tspMain = packageJson.tspMain;
+ if (typeof tspMain === "string") {
+ const tspMainFile = path.resolve(baseDir, tspMain);
+ if (await isFile(tspMainFile)) {
+ logger.debug(`tspMain file ${tspMainFile} selected as entrypoint file.`);
+ return tspMainFile;
+ }
+ }
+ } catch (error) {
+ logger.error(`An error occurred while reading the package.json file ${pkgPath}`, [error]);
+ }
+ }
+
+ const mainTspFile = path.resolve(baseDir, StartFileName);
+ if (await isFile(mainTspFile)) {
+ return mainTspFile;
+ }
+ const parentDir = getDirectoryPath(baseDir);
+ if (parentDir === baseDir) {
+ break;
+ }
+ baseDir = parentDir;
+ }
+
+ return undefined;
+}
+
+export async function TraverseMainTspFileInWorkspace() {
+ return vscode.workspace
+ .findFiles(`**/${StartFileName}`, "**/node_modules/**")
+ .then((uris) =>
+ uris
+ .filter((uri) => uri.scheme === "file" && !uri.fsPath.includes("node_modules"))
+ .map((uri) => normalizeSlashes(uri.fsPath)),
+ );
+}
diff --git a/packages/typespec-vscode/src/vscode-cmd/emit-code/emit-code.ts b/packages/typespec-vscode/src/vscode-cmd/emit-code/emit-code.ts
new file mode 100644
index 0000000000..a57114288e
--- /dev/null
+++ b/packages/typespec-vscode/src/vscode-cmd/emit-code/emit-code.ts
@@ -0,0 +1,617 @@
+import { readFile, writeFile } from "fs/promises";
+import path from "path";
+import vscode, { QuickInputButton, Uri } from "vscode";
+import { Executable } from "vscode-languageclient/node.js";
+import { isScalar, isSeq, parseDocument } from "yaml";
+import { StartFileName, TspConfigFileName } from "../../const.js";
+import logger from "../../log/logger.js";
+import { InstallAction, npmDependencyType, NpmUtil } from "../../npm-utils.js";
+import { getDirectoryPath } from "../../path-utils.js";
+import { resolveTypeSpecCli } from "../../tsp-executable-resolver.js";
+import { getEntrypointTspFile, TraverseMainTspFileInWorkspace } from "../../typespec-utils.js";
+import { ExecOutput, isFile, spawnExecutionAndLogToOutput } from "../../utils.js";
+import { EmitQuickPickItem } from "./emit-quick-pick-item.js";
+import {
+ Emitter,
+ EmitterKind,
+ getLanguageAlias,
+ getRegisterEmitters,
+ getRegisterEmittersByPackage,
+ getRegisterEmitterTypes,
+ PreDefinedEmitterPickItems,
+} from "./emitter.js";
+
+interface EmitQuickPickButton extends QuickInputButton {
+ uri: string;
+}
+
+async function configureEmitter(
+ context: vscode.ExtensionContext,
+ existingEmitters: string[],
+): Promise {
+ const emitterKinds = getRegisterEmitterTypes();
+ const toEmitterTypeQuickPickItem = (kind: EmitterKind): any => {
+ const registerEmitters = getRegisterEmitters(kind);
+ const supportedLanguages = registerEmitters.map((e) => e.language).join(", ");
+ return {
+ label: PreDefinedEmitterPickItems[kind]?.label ?? kind,
+ detail:
+ PreDefinedEmitterPickItems[kind]?.detail ??
+ `Generate ${kind} code from TypeSpec files. Supported languages are ${supportedLanguages}.`,
+ emitterKind: kind,
+ iconPath: {
+ light: Uri.file(context.asAbsolutePath(`./icons/${kind.toLowerCase()}.light.svg`)),
+ dark: Uri.file(context.asAbsolutePath(`./icons/${kind.toLowerCase()}.dark.svg`)),
+ },
+ };
+ };
+ const codesToEmit = emitterKinds.map((kind) => toEmitterTypeQuickPickItem(kind));
+ const codeType = await vscode.window.showQuickPick(codesToEmit, {
+ title: "Generate from TypeSpec",
+ canPickMany: false,
+ placeHolder: "Select an emitter type",
+ ignoreFocusOut: true,
+ });
+ if (!codeType) {
+ logger.info("No emitter Type selected. Generating Cancelled.");
+ return undefined;
+ }
+
+ const toQuickPickItem = (e: Emitter): EmitQuickPickItem => {
+ const buttons = e.sourceRepo
+ ? [
+ {
+ iconPath: new vscode.ThemeIcon("link-external"),
+ tooltip: "More details",
+ uri: e.sourceRepo,
+ },
+ ]
+ : undefined;
+ return {
+ language: e.language,
+ package: e.package,
+ version: e.version,
+ requisites: e.requisites,
+ sourceRepo: e.sourceRepo,
+ emitterKind: e.kind,
+ label: e.language,
+ detail: `Generate ${e.kind} code for ${e.language} by TypeSpec library ${e.package}.`,
+ picked: false,
+ fromConfig: false,
+ buttons: buttons,
+ iconPath: {
+ light: Uri.file(
+ context.asAbsolutePath(`./icons/${getLanguageAlias(e.language).toLowerCase()}.light.svg`),
+ ),
+ dark: Uri.file(
+ context.asAbsolutePath(`./icons/${getLanguageAlias(e.language).toLowerCase()}.dark.svg`),
+ ),
+ },
+ };
+ };
+
+ /* filter out already existing emitters. */
+ const registerEmitters = getRegisterEmitters(codeType.emitterKind).filter(
+ (emitter) => !existingEmitters.includes(emitter.package),
+ );
+ const all: EmitQuickPickItem[] = [...registerEmitters].map((e) => toQuickPickItem(e));
+
+ const emitterSelector = vscode.window.createQuickPick();
+ emitterSelector.items = all;
+ emitterSelector.title = `Generate from TypeSpec`;
+ emitterSelector.canSelectMany = false;
+ emitterSelector.placeholder = `Select a Language for ${codeType} code generation`;
+ emitterSelector.ignoreFocusOut = true;
+ emitterSelector.onDidTriggerItemButton(async (e) => {
+ if (e.button.tooltip === "More details") {
+ const url = (e.button as EmitQuickPickButton).uri;
+ await vscode.env.openExternal(vscode.Uri.parse(url));
+ }
+ });
+ emitterSelector.show();
+ const selectedEmitter = await new Promise((resolve) => {
+ emitterSelector.onDidAccept(() => {
+ resolve(emitterSelector.selectedItems[0]);
+ emitterSelector.dispose();
+ });
+ });
+
+ if (!selectedEmitter) {
+ logger.info("No language selected. Generating Cancelled.");
+ return undefined;
+ }
+ return {
+ language: selectedEmitter.language,
+ package: selectedEmitter.package,
+ version: selectedEmitter.version,
+ sourceRepo: selectedEmitter.sourceRepo,
+ requisites: selectedEmitter.requisites,
+ kind: selectedEmitter.emitterKind,
+ };
+}
+async function doEmit(mainTspFile: string, emitter: Emitter) {
+ if (!mainTspFile || !(await isFile(mainTspFile))) {
+ logger.error(
+ "Invalid typespec project. There is no main tsp file in the project. Generating Cancelled.",
+ [],
+ { showOutput: false, showPopup: true },
+ );
+ return;
+ }
+
+ const baseDir = getDirectoryPath(mainTspFile);
+
+ const npmUtil = new NpmUtil(baseDir);
+ const packagesToInstall: string[] = [];
+
+ /* install emitter package. */
+ logger.info(`select ${emitter.package}`);
+ const { action, version } = await npmUtil.calculateNpmPackageInstallAction(
+ emitter.package,
+ emitter.version,
+ );
+
+ const installPackageQuickPickItems = await vscode.window.withProgress(
+ {
+ location: vscode.ProgressLocation.Notification,
+ title: "Calculating packages to install or upgrade ...",
+ cancellable: false,
+ },
+ async () => {
+ const packageQuickPickItems = [];
+ if (action === InstallAction.Upgrade || action === InstallAction.Install) {
+ const minimumRequisites = emitter.requisites
+ ? ` Minimum requisites: install ${emitter.requisites?.join(", ")}`
+ : "";
+ let packageFullName = emitter.package;
+ if (version) {
+ packageFullName = `${emitter.package}@${version}`;
+ }
+ packageQuickPickItems.push({
+ label: `${emitter.package}`,
+ description: `TypeSpec library for emitting ${emitter.language} from TypeSpec files.`,
+ detail: minimumRequisites,
+ packageFullName: packageFullName,
+ sourceRepo: emitter.sourceRepo,
+ picked: true,
+ buttons: emitter.sourceRepo
+ ? [
+ {
+ iconPath: new vscode.ThemeIcon("link-external"),
+ tooltip: "More details",
+ uri: emitter.sourceRepo,
+ },
+ ]
+ : undefined,
+ });
+ packagesToInstall.push(packageFullName);
+ }
+
+ for (const p of packagesToInstall) {
+ /* verify dependency packages. */
+ try {
+ const dependenciesToInstall = await npmUtil.calculateNpmPackageDependencyToUpgrade(
+ p,
+ version,
+ [npmDependencyType.dependencies, npmDependencyType.peerDependencies],
+ );
+ if (dependenciesToInstall.length > 0) {
+ packagesToInstall.push(...dependenciesToInstall.map((d) => `${d.name}@${d.version}`));
+ for (const dep of dependenciesToInstall) {
+ const packageFullName = `${dep.name}@${version ?? "latest"}`;
+ packageQuickPickItems.push({
+ label: `${dep.name}`,
+ description: "Required for enabling the emitter library.",
+ packageFullName: packageFullName,
+ picked: true,
+ });
+ }
+ }
+ } catch (err) {
+ logger.error(`Exception occurred when check dependency packages for ${p}.`);
+ }
+ }
+ return packageQuickPickItems;
+ },
+ );
+
+ if (installPackageQuickPickItems.length > 0) {
+ const installPackagesSelector = vscode.window.createQuickPick();
+ installPackagesSelector.items = installPackageQuickPickItems;
+ installPackagesSelector.title = `Generate from TypeSpec`;
+ installPackagesSelector.canSelectMany = true;
+ installPackagesSelector.placeholder = "Here are libraries to install or update.";
+ installPackagesSelector.ignoreFocusOut = true;
+ installPackagesSelector.selectedItems = [...installPackageQuickPickItems];
+ installPackagesSelector.onDidTriggerItemButton(async (e) => {
+ if (e.button.tooltip === "More details") {
+ const url = (e.button as EmitQuickPickButton).uri;
+ await vscode.env.openExternal(vscode.Uri.parse(url));
+ }
+ });
+ installPackagesSelector.show();
+ const selectedPackages = await new Promise((resolve) => {
+ installPackagesSelector.onDidAccept(() => {
+ resolve(installPackagesSelector.selectedItems);
+ installPackagesSelector.dispose();
+ });
+ });
+ if (!selectedPackages || selectedPackages.length === 0) {
+ logger.info("No package selected. Generating Cancelled.", [], {
+ showOutput: true,
+ showPopup: true,
+ });
+ return;
+ }
+ /* npm install packages. */
+ if (selectedPackages.length > 0) {
+ const installPackages = selectedPackages.map((p) => p.packageFullName);
+ logger.info(`Install ${installPackages.join(",")} under directory ${baseDir}`);
+ const installResult = await vscode.window.withProgress(
+ {
+ location: vscode.ProgressLocation.Notification,
+ title: "Installing packages...",
+ cancellable: false,
+ },
+ async () => {
+ try {
+ const npmInstallResult = await npmUtil.npmInstallPackages(installPackages, undefined);
+ if (npmInstallResult.exitCode !== 0) {
+ return false;
+ } else {
+ return true;
+ }
+ } catch (err: any) {
+ return false;
+ }
+ },
+ );
+ if (!installResult) {
+ logger.error(`Error occurred when installing packages. Generating Cancelled.`, [], {
+ showOutput: false,
+ showPopup: true,
+ });
+ return;
+ }
+ }
+ }
+
+ /* emit */
+ const cli = await resolveTypeSpecCli(baseDir);
+ if (!cli) {
+ logger.error(
+ "Cannot find TypeSpec CLI. Please install @typespec/compiler. Generating Cancelled.",
+ [],
+ {
+ showOutput: true,
+ showPopup: true,
+ },
+ );
+ return;
+ }
+ /*Config emitter output dir and emit in tspconfig.yaml. */
+ const defaultEmitOutputDirInConfig = `{project-root}/${emitter.kind}/${getLanguageAlias(emitter.language)}`;
+ const tspConfigFile = path.join(baseDir, TspConfigFileName);
+ let configYaml = parseDocument(""); //generate a empty yaml
+ if (await isFile(tspConfigFile)) {
+ const content = await readFile(tspConfigFile);
+ configYaml = parseDocument(content.toString());
+ }
+ let outputDir = defaultEmitOutputDirInConfig;
+ try {
+ /*update emitter in config.yaml. */
+ const emitNode = configYaml.get("emit");
+ if (emitNode) {
+ if (isSeq(emitNode)) {
+ if (Array.isArray(emitNode.items)) {
+ const index = emitNode.items.findIndex((item) => {
+ if (isScalar(item)) {
+ return item.value === emitter.package;
+ }
+ return false;
+ });
+ if (index === -1) {
+ emitNode.items.push(emitter.package);
+ }
+ }
+ }
+ } else {
+ configYaml.set("emit", [emitter.package]);
+ }
+ const emitOutputDir = configYaml.getIn(["options", emitter.package, "emitter-output-dir"]);
+ if (!emitOutputDir) {
+ configYaml.setIn(
+ ["options", emitter.package, "emitter-output-dir"],
+ defaultEmitOutputDirInConfig,
+ );
+ } else {
+ outputDir = emitOutputDir as string;
+ }
+ const newYamlContent = configYaml.toString();
+ await writeFile(tspConfigFile, newYamlContent);
+ } catch (error: any) {
+ logger.error(error);
+ }
+
+ outputDir = outputDir.replace("{project-root}", baseDir);
+ const options: Record = {};
+ logger.info(
+ `Start to generate ${emitter.language} ${emitter.kind} code under directory ${outputDir}`,
+ );
+ await vscode.window.withProgress(
+ {
+ location: vscode.ProgressLocation.Notification,
+ title: `Generating ${emitter.kind} code for ${emitter.language}...`,
+ cancellable: false,
+ },
+ async () => {
+ try {
+ const compileResult = await compile(cli, mainTspFile, emitter.package, options, false);
+ if (compileResult.exitCode !== 0) {
+ logger.error(`Generating ${emitter.kind} code for ${emitter.language}...Failed`, [], {
+ showOutput: true,
+ showPopup: true,
+ });
+ } else {
+ logger.info(`Generating ${emitter.kind} code for ${emitter.language}...Succeeded`, [], {
+ showOutput: true,
+ showPopup: true,
+ });
+ }
+ } catch (err: any) {
+ if (typeof err === "object" && "stdout" in err && "stderr" in err && `error` in err) {
+ const execOutput = err as ExecOutput;
+ const details = [];
+ if (execOutput.stdout !== "") details.push(execOutput.stdout);
+ if (execOutput.stderr !== "") details.push(execOutput.stderr);
+ if (execOutput.error) details.push(execOutput.error);
+ logger.error(
+ `Generating ${emitter.kind} code for ${emitter.language}...Failed.`,
+ details,
+ {
+ showOutput: true,
+ showPopup: true,
+ },
+ );
+ } else {
+ logger.error(`Generating ${emitter.kind} code for ${emitter.language}...Failed.`, [err], {
+ showOutput: true,
+ showPopup: true,
+ });
+ }
+ }
+ },
+ );
+}
+
+export async function emitCode(context: vscode.ExtensionContext, uri: vscode.Uri) {
+ let tspProjectFile: string = "";
+ if (!uri) {
+ const targetPathes = await TraverseMainTspFileInWorkspace();
+ logger.info(`Found ${targetPathes.length} ${StartFileName} files`);
+ if (targetPathes.length === 0) {
+ logger.info(`No entrypoint file (${StartFileName}) found. Generating Cancelled.`, [], {
+ showOutput: true,
+ showPopup: true,
+ });
+ return;
+ } else if (targetPathes.length === 1) {
+ tspProjectFile = targetPathes[0];
+ } else {
+ const toProjectPickItem = (filePath: string): any => {
+ return {
+ label: `Project: ${filePath}`,
+ path: filePath,
+ iconPath: {
+ light: Uri.file(context.asAbsolutePath(`./icons/tsp-file.light.svg`)),
+ dark: Uri.file(context.asAbsolutePath(`./icons/tsp-file.dark.svg`)),
+ },
+ };
+ };
+ const typespecProjectQuickPickItems: any[] = targetPathes.map((filePath) =>
+ toProjectPickItem(filePath),
+ );
+ const selectedProjectFile = await vscode.window.showQuickPick(typespecProjectQuickPickItems, {
+ title: "Generate from TypeSpec",
+ canPickMany: false,
+ placeHolder: "Select a project",
+ ignoreFocusOut: true,
+ });
+ if (!selectedProjectFile) {
+ logger.info("No project selected. Generating Cancelled.", [], {
+ showOutput: true,
+ showPopup: true,
+ });
+ return;
+ }
+ tspProjectFile = selectedProjectFile.path;
+ }
+ } else {
+ const tspStartFile = await getEntrypointTspFile(uri.fsPath);
+ if (!tspStartFile) {
+ logger.info(`No entrypoint file (${StartFileName}). Invalid typespec project.`, [], {
+ showOutput: true,
+ showPopup: true,
+ });
+ return;
+ }
+ tspProjectFile = tspStartFile;
+ }
+
+ logger.info(`Generate from entrypoint file: ${tspProjectFile}`);
+ const baseDir = getDirectoryPath(tspProjectFile);
+ const tspConfigFile = path.join(baseDir, TspConfigFileName);
+ let configYaml = parseDocument(""); //generate a empty yaml
+ if (await isFile(tspConfigFile)) {
+ const content = await readFile(tspConfigFile);
+ configYaml = parseDocument(content.toString());
+ }
+
+ let existingEmitters;
+ try {
+ const emitNode = configYaml.get("emit");
+ if (emitNode) {
+ if (isSeq(emitNode)) {
+ if (Array.isArray(emitNode.items)) {
+ existingEmitters = emitNode.items.map((item) => {
+ if (isScalar(item)) {
+ return item.value as string;
+ }
+ return "";
+ });
+ }
+ }
+ }
+ } catch (error: any) {
+ logger.error(error);
+ }
+
+ const toEmitterQuickPickItem = (e: Emitter): EmitQuickPickItem => {
+ const buttons = e.sourceRepo
+ ? [
+ {
+ iconPath: new vscode.ThemeIcon("link-external"),
+ tooltip: "More details",
+ uri: e.sourceRepo,
+ },
+ ]
+ : undefined;
+ return {
+ language: e.language,
+ package: e.package,
+ version: e.version,
+ requisites: e.requisites,
+ sourceRepo: e.sourceRepo,
+ emitterKind: e.kind,
+ label: `${e.language} ${e.kind} code emitter`,
+ description: `${e.package}.`,
+ // detail: `Generate ${e.kind} code for ${e.language} by TypeSpec library ${e.package}.`,
+ picked: false,
+ fromConfig: false,
+ buttons: buttons,
+ iconPath: {
+ light: Uri.file(
+ context.asAbsolutePath(`./icons/${getLanguageAlias(e.language).toLowerCase()}.light.svg`),
+ ),
+ dark: Uri.file(
+ context.asAbsolutePath(`./icons/${getLanguageAlias(e.language).toLowerCase()}.dark.svg`),
+ ),
+ },
+ };
+ };
+ /* display existing emitters in config.yaml. */
+ if (existingEmitters && existingEmitters.length > 0) {
+ const existingEmitterQuickPickItems = existingEmitters.map((e) => {
+ const emitter = getRegisterEmittersByPackage(e);
+ if (emitter) {
+ return toEmitterQuickPickItem(emitter);
+ } else {
+ return {
+ label: e,
+ description: "Emit code by the emitter library.",
+ picked: true,
+ fromConfig: true,
+ package: e,
+ kind: vscode.QuickPickItemKind.Default,
+ };
+ }
+ });
+ const separatorItem = {
+ label: "Settings",
+ description: "settings",
+ kind: vscode.QuickPickItemKind.Separator,
+ info: undefined,
+ package: "",
+ fromConfig: false,
+ picked: false,
+ };
+ const newEmitterQuickPickItem = {
+ label: "Configure a new emitter",
+ description: "Configure a new emitter for code generation",
+ picked: false,
+ fromConfig: false,
+ package: "",
+ kind: vscode.QuickPickItemKind.Default,
+ buttons: [
+ {
+ iconPath: new vscode.ThemeIcon("settings-gear"),
+ tooltip: "Configure a new emitter for code generation",
+ },
+ ],
+ };
+
+ const allPickItems = [];
+ allPickItems.push(...existingEmitterQuickPickItems, separatorItem, newEmitterQuickPickItem);
+ const existingEmittersSelector = vscode.window.createQuickPick();
+ existingEmittersSelector.items = allPickItems;
+ existingEmittersSelector.title = `Generate from TypeSpec`;
+ existingEmittersSelector.canSelectMany = false;
+ existingEmittersSelector.placeholder = "Select an emitter for code generation";
+ existingEmittersSelector.ignoreFocusOut = true;
+ existingEmittersSelector.show();
+ const selectedExistingEmitter = await new Promise((resolve) => {
+ existingEmittersSelector.onDidAccept(async () => {
+ const selectedItem = existingEmittersSelector.selectedItems[0];
+ if (selectedItem === newEmitterQuickPickItem) {
+ const newEmitter = await configureEmitter(context, existingEmitters);
+ const allPickItems = [];
+ if (newEmitter) {
+ allPickItems.push(toEmitterQuickPickItem(newEmitter));
+ }
+ allPickItems.push(...existingEmitterQuickPickItems);
+ allPickItems.push(separatorItem, newEmitterQuickPickItem);
+ existingEmittersSelector.items = allPickItems;
+ existingEmittersSelector.show();
+ } else {
+ resolve(existingEmittersSelector.selectedItems[0]);
+ existingEmittersSelector.dispose();
+ }
+ });
+ });
+ if (!selectedExistingEmitter) {
+ logger.info("No emitter selected. Generating Cancelled.");
+ return;
+ }
+ await doEmit(
+ tspProjectFile,
+ getRegisterEmittersByPackage(selectedExistingEmitter.package) ?? {
+ package: selectedExistingEmitter.package,
+ language: "Unknown",
+ kind: EmitterKind.Unknown,
+ },
+ );
+ } else {
+ const newEmitter = await configureEmitter(context, existingEmitters ?? []);
+ if (newEmitter) {
+ await doEmit(tspProjectFile, newEmitter);
+ } else {
+ logger.info("No emitter selected. Generating Cancelled.");
+ return;
+ }
+ }
+}
+
+async function compile(
+ cli: Executable,
+ startFile: string,
+ emitter: string,
+ options: Record,
+ logPretty?: boolean,
+): Promise {
+ const args: string[] = cli.args ?? [];
+ args.push("compile");
+ args.push(startFile);
+ if (emitter) {
+ args.push("--emit", emitter);
+ }
+ if (logPretty !== undefined) {
+ args.push("--pretty");
+ args.push(logPretty ? "true" : "false");
+ }
+
+ for (const [key, value] of Object.entries(options)) {
+ args.push("--option", `${emitter}.${key}=${value}`);
+ }
+
+ return await spawnExecutionAndLogToOutput(cli.command, args, getDirectoryPath(startFile));
+}
diff --git a/packages/typespec-vscode/src/vscode-cmd/emit-code/emit-quick-pick-item.ts b/packages/typespec-vscode/src/vscode-cmd/emit-code/emit-quick-pick-item.ts
new file mode 100644
index 0000000000..e98fc48a9a
--- /dev/null
+++ b/packages/typespec-vscode/src/vscode-cmd/emit-code/emit-quick-pick-item.ts
@@ -0,0 +1,13 @@
+import vscode from "vscode";
+import { EmitterKind } from "./emitter.js";
+
+export interface EmitQuickPickItem extends vscode.QuickPickItem {
+ language: string;
+ package: string;
+ version?: string;
+ sourceRepo?: string;
+ requisites?: string[];
+ fromConfig: boolean;
+ outputDir?: string;
+ emitterKind: EmitterKind;
+}
diff --git a/packages/typespec-vscode/src/vscode-cmd/emit-code/emitter.ts b/packages/typespec-vscode/src/vscode-cmd/emit-code/emitter.ts
new file mode 100644
index 0000000000..3fe1fb4137
--- /dev/null
+++ b/packages/typespec-vscode/src/vscode-cmd/emit-code/emitter.ts
@@ -0,0 +1,95 @@
+import vscode from "vscode";
+import logger from "../../log/logger.js";
+import { SettingName } from "../../types.js";
+
+export enum EmitterKind {
+ Schema = "openapi",
+ Client = "client",
+ Server = "server",
+ Unknown = "unknown",
+}
+
+export interface Emitter {
+ language: string;
+ package: string;
+ version?: string;
+ sourceRepo?: string;
+ requisites?: string[];
+ kind: EmitterKind;
+}
+
+export const PreDefinedEmitterPickItems: Record = {
+ openapi: {
+ label: "OpenAPI Document",
+ detail: "Generating OpenAPI3 Document from TypeSpec files.",
+ },
+ client: {
+ label: "Client Code",
+ detail:
+ "Generating Client Code from TypeSpec files. Supported languages are .NET, Python, Java, JavaScript.",
+ },
+ server: {
+ label: " Server Stub",
+ detail: "Generating Server Stub from TypeSpec files. Supported languages are .NET, JavaScript.",
+ },
+};
+
+function getEmitter(kind: EmitterKind, emitter: Emitter): Emitter | undefined {
+ let packageFullName: string = emitter.package;
+ if (!packageFullName) {
+ logger.error("Emitter package name is required.");
+ return undefined;
+ }
+ packageFullName = packageFullName.trim();
+ const index = packageFullName.lastIndexOf("@");
+ let version = undefined;
+ let packageName = packageFullName;
+ if (index !== -1 && index !== 0) {
+ version = packageFullName.substring(index + 1);
+ packageName = packageFullName.substring(0, index);
+ }
+
+ return {
+ language: emitter.language,
+ package: packageName,
+ version: version,
+ sourceRepo: emitter.sourceRepo,
+ requisites: emitter.requisites,
+ kind: kind,
+ };
+}
+
+export function getRegisterEmitters(kind: EmitterKind): ReadonlyArray {
+ const extensionConfig = vscode.workspace.getConfiguration();
+ const emitters: ReadonlyArray =
+ extensionConfig.get(SettingName.GenerateCodeEmitters) ?? [];
+ return emitters
+ .filter((emitter) => emitter.kind === kind)
+ .map((emitter) => getEmitter(kind, emitter))
+ .filter((emitter) => emitter !== undefined) as Emitter[];
+}
+
+export function getRegisterEmitterTypes(): ReadonlyArray {
+ const extensionConfig = vscode.workspace.getConfiguration();
+ const emitters: ReadonlyArray =
+ extensionConfig.get(SettingName.GenerateCodeEmitters) ?? [];
+ return Array.from(new Set(emitters.map((emitter) => emitter.kind)));
+}
+
+export function getRegisterEmittersByPackage(packageName: string): Emitter | undefined {
+ const extensionConfig = vscode.workspace.getConfiguration();
+ const emitters: ReadonlyArray =
+ extensionConfig.get(SettingName.GenerateCodeEmitters) ?? [];
+ return emitters.find(
+ (emitter) => emitter.package === packageName || emitter.package.startsWith(packageName + "@"),
+ );
+}
+
+const languageAlias: Record = {
+ ".NET": "dotnet",
+};
+
+/*return the alias of the language if it exists, otherwise return the original language. */
+export function getLanguageAlias(language: string): string {
+ return languageAlias[language] ?? language;
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b3453a5853..afe94daeef 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1939,6 +1939,9 @@ importers:
vscode-languageclient:
specifier: ~9.0.1
version: 9.0.1
+ yaml:
+ specifier: ~2.5.1
+ version: 2.5.1
packages/versioning:
devDependencies:
@@ -11215,8 +11218,8 @@ packages:
terser:
optional: true
- vitefu@1.0.4:
- resolution: {integrity: sha512-y6zEE3PQf6uu/Mt6DTJ9ih+kyJLr4XcSgHR2zUkM8SWDhuixEJxfJ6CZGMHh1Ec3vPLoEA0IHU5oWzVqw8ulow==}
+ vitefu@1.0.5:
+ resolution: {integrity: sha512-h4Vflt9gxODPFNGPwp4zAMZRpZR7eslzwH2c5hn5kNZ5rhnKyRJ50U+yGCdc2IRaBs8O4haIgLNGrV5CrpMsCA==}
peerDependencies:
vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
peerDependenciesMeta:
@@ -16425,7 +16428,7 @@ snapshots:
dependencies:
'@vitest/spy': 2.1.5
estree-walker: 3.0.3
- magic-string: 0.30.17
+ magic-string: 0.30.13
optionalDependencies:
vite: 5.4.11(@types/node@22.7.9)
@@ -16445,7 +16448,7 @@ snapshots:
'@vitest/snapshot@2.1.5':
dependencies:
'@vitest/pretty-format': 2.1.5
- magic-string: 0.30.17
+ magic-string: 0.30.13
pathe: 1.1.2
'@vitest/spy@2.0.5':
@@ -16974,7 +16977,7 @@ snapshots:
unist-util-visit: 5.0.0
vfile: 6.0.3
vite: 5.4.11(@types/node@22.7.9)
- vitefu: 1.0.4(vite@5.4.11(@types/node@22.7.9))
+ vitefu: 1.0.5(vite@5.4.11(@types/node@22.7.9))
which-pm: 3.0.0
xxhash-wasm: 1.1.0
yargs-parser: 21.1.1
@@ -23234,7 +23237,7 @@ snapshots:
'@types/node': 22.7.9
fsevents: 2.3.3
- vitefu@1.0.4(vite@5.4.11(@types/node@22.7.9)):
+ vitefu@1.0.5(vite@5.4.11(@types/node@22.7.9)):
optionalDependencies:
vite: 5.4.11(@types/node@22.7.9)