diff --git a/.chronus/changes/azhang_InitTemplateWithEmitterSupport-2024-11-19-14-44-8.md b/.chronus/changes/azhang_InitTemplateWithEmitterSupport-2024-11-19-14-44-8.md new file mode 100644 index 0000000000..d20c667143 --- /dev/null +++ b/.chronus/changes/azhang_InitTemplateWithEmitterSupport-2024-11-19-14-44-8.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - typespec-vscode +--- + +Updated the init project template to include the new emitter definition \ No newline at end of file diff --git a/.chronus/changes/azhang_InitTemplateWithEmitterSupport-2024-11-19-9-37-2.md b/.chronus/changes/azhang_InitTemplateWithEmitterSupport-2024-11-19-9-37-2.md new file mode 100644 index 0000000000..0886902b4d --- /dev/null +++ b/.chronus/changes/azhang_InitTemplateWithEmitterSupport-2024-11-19-9-37-2.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Added support for emitter selections for init template. \ No newline at end of file diff --git a/packages/compiler/src/init/init-template.ts b/packages/compiler/src/init/init-template.ts index 973c627b4c..6a234bdda2 100644 --- a/packages/compiler/src/init/init-template.ts +++ b/packages/compiler/src/init/init-template.ts @@ -33,6 +33,11 @@ export interface InitTemplate { */ libraries?: InitTemplateLibrary[]; + /** + * List of emitters to include + */ + emitters?: Record; + /** * Config */ @@ -55,6 +60,22 @@ export interface InitTemplate { files?: InitTemplateFile[]; } +/** + * Describes emitter dependencies that will be added to the generated project. + */ +export interface EmitterTemplate { + /** Emitter Selection Description */ + description?: string; + /** Whether emitter is selected by default in the list */ + selected?: boolean; + /** Optional emitter Options to populate the tspconfig.yaml */ + options?: any; + /** Optional message to display to the user post creation */ + message?: string; + /** Optional specific emitter version. `latest` if not specified */ + version?: string; +} + /** * Describes a library dependency that will be added to the generated project. */ @@ -99,6 +120,22 @@ export const InitTemplateSchema: JSONSchemaType = { }, nullable: true, }, + emitters: { + type: "object", + nullable: true, + additionalProperties: { + type: "object", + properties: { + description: { type: "string", nullable: true }, + selected: { type: "boolean", nullable: true }, + options: {} as any, + message: { type: "string", nullable: true }, + version: { type: "string", nullable: true }, + }, + required: [], + }, + required: [], + }, skipCompilerPackage: { type: "boolean", nullable: true }, config: { nullable: true, ...TypeSpecConfigJsonSchema }, inputs: { diff --git a/packages/compiler/src/init/init.ts b/packages/compiler/src/init/init.ts index efa4b843ef..4af843d430 100644 --- a/packages/compiler/src/init/init.ts +++ b/packages/compiler/src/init/init.ts @@ -9,7 +9,7 @@ import { MANIFEST } from "../manifest.js"; import { readUrlOrPath } from "../utils/misc.js"; import { getTypeSpecCoreTemplates } from "./core-templates.js"; import { validateTemplateDefinitions, ValidationResult } from "./init-template-validate.js"; -import { InitTemplate, InitTemplateLibrarySpec } from "./init-template.js"; +import { EmitterTemplate, InitTemplate, InitTemplateLibrarySpec } from "./init-template.js"; import { makeScaffoldingConfig, normalizeLibrary, scaffoldNewProject } from "./scaffold.js"; export interface InitTypeSpecProjectOptions { @@ -66,6 +66,7 @@ export async function initTypeSpecProject( ]); const libraries = await selectLibraries(template); + const emitters = await selectEmitters(template); const parameters = await promptCustomParameters(template); const scaffoldingConfig = makeScaffoldingConfig(template, { baseUri: result.baseUri, @@ -75,6 +76,7 @@ export async function initTypeSpecProject( folderName, parameters, includeGitignore, + emitters, }); await scaffoldNewProject(host, scaffoldingConfig); @@ -86,6 +88,18 @@ export async function initTypeSpecProject( // eslint-disable-next-line no-console console.log(pc.green("Project created successfully.")); + + if (Object.values(emitters).some((emitter) => emitter.message !== undefined)) { + // eslint-disable-next-line no-console + console.log(pc.yellow("\nPlease review the following messages from emitters:")); + + for (const key of Object.keys(emitters)) { + if (emitters[key].message) { + // eslint-disable-next-line no-console + console.log(` ${key}: \n\t${emitters[key].message}`); + } + } + } } async function promptCustomParameters(template: InitTemplate): Promise> { @@ -235,6 +249,33 @@ async function validateTemplate(template: any, loaded: LoadedTemplate): Promise< return true; } +async function selectEmitters(template: InitTemplate): Promise> { + if (!template.emitters) { + return {}; + } + + const promptList = [...Object.entries(template.emitters)].map(([name, emitter]) => { + return { + title: name, + description: emitter.description, + selected: emitter.selected ?? false, + }; + }); + + const { emitters } = await prompts({ + type: "multiselect", + name: "emitters", + message: "Select emitters?", + choices: promptList, + }); + + const selectedEmitters = [...Object.entries(template.emitters)].filter((_, index) => + emitters.includes(index), + ); + + return Object.fromEntries(selectedEmitters); +} + async function selectLibraries(template: InitTemplate): Promise { if (template.libraries === undefined || template.libraries.length === 0) { return []; diff --git a/packages/compiler/src/init/scaffold.ts b/packages/compiler/src/init/scaffold.ts index 0a39d2c018..4eb49c83df 100644 --- a/packages/compiler/src/init/scaffold.ts +++ b/packages/compiler/src/init/scaffold.ts @@ -51,6 +51,11 @@ export interface ScaffoldingConfig { * Custom parameters provided in the tempalates. */ parameters: Record; + + /** + * Selected emitters the tempalates. + */ + emitters: Record; } export function normalizeLibrary(library: InitTemplateLibrary): InitTemplateLibrarySpec { @@ -73,6 +78,7 @@ export function makeScaffoldingConfig( folderName: config.folderName ?? "", parameters: config.parameters ?? {}, includeGitignore: config.includeGitignore ?? true, + emitters: config.emitters ?? {}, ...config, }; } @@ -113,8 +119,13 @@ async function writePackageJson(host: CompilerHost, config: ScaffoldingConfig) { } for (const library of config.libraries) { - peerDependencies[library.name] = await getLibraryVersion(library); - devDependencies[library.name] = await getLibraryVersion(library); + peerDependencies[library.name] = await getPackageVersion(library); + devDependencies[library.name] = await getPackageVersion(library); + } + + for (const key of Object.keys(config.emitters)) { + peerDependencies[key] = await getPackageVersion(config.emitters[key]); + devDependencies[key] = await getPackageVersion(config.emitters[key]); } const packageJson: PackageJson = { @@ -154,7 +165,20 @@ async function writeConfig(host: CompilerHost, config: ScaffoldingConfig) { if (isFileSkipGeneration(TypeSpecConfigFilename, config.template.files ?? [])) { return; } - const content = config.template.config ? stringify(config.template.config) : placeholderConfig; + + let content: string = placeholderConfig; + if (config.template.config !== undefined && Object.keys(config.template.config).length > 0) { + content = stringify(config.template.config); + } else if (Object.keys(config.emitters).length > 0) { + const emitters = Object.keys(config.emitters); + const options = Object.fromEntries( + Object.entries(config.emitters).map(([key, emitter]) => [key, emitter.options]), + ); + content = stringify({ + emit: emitters, + options: Object.keys(options).length > 0 ? options : undefined, + }); + } return host.writeFile(joinPaths(config.directory, TypeSpecConfigFilename), content); } @@ -165,7 +189,7 @@ async function writeMain(host: CompilerHost, config: ScaffoldingConfig) { const dependencies: Record = {}; for (const library of config.libraries) { - dependencies[library.name] = await getLibraryVersion(library); + dependencies[library.name] = await getPackageVersion(library); } const lines = [...config.libraries.map((x) => `import "${x.name}";`), ""]; @@ -220,7 +244,7 @@ async function writeFile( return host.writeFile(joinPaths(config.directory, file.destination), content); } -async function getLibraryVersion(library: InitTemplateLibrarySpec): Promise { +async function getPackageVersion(packageInfo: { version?: string }): Promise { // TODO: Resolve 'latest' version from npm, issue #1919 - return library.version ?? "latest"; + return packageInfo.version ?? "latest"; } diff --git a/packages/typespec-vscode/src/vscode-cmd/create-tsp-project.ts b/packages/typespec-vscode/src/vscode-cmd/create-tsp-project.ts index 30ecdce49c..deb3327e44 100644 --- a/packages/typespec-vscode/src/vscode-cmd/create-tsp-project.ts +++ b/packages/typespec-vscode/src/vscode-cmd/create-tsp-project.ts @@ -162,6 +162,7 @@ export async function createTypeSpecProject(client: TspLanguageClient | undefine return; } + // TODO: add support for emitters picking const initTemplateConfig: InitProjectConfig = { template: info.template!, directory: selectedRootFolder, @@ -171,6 +172,7 @@ export async function createTypeSpecProject(client: TspLanguageClient | undefine parameters: inputs ?? {}, includeGitignore: includeGitignore, libraries: librariesToInclude, + emitters: {}, }; const initResult = await initProject(client, initTemplateConfig); if (!initResult) {