Skip to content

Commit

Permalink
Added emitter support to init template (microsoft#5415)
Browse files Browse the repository at this point in the history
Init template now supports a list of emitters for user to choose from to
be included in the project.

Artifacts affected:
- `package.json` Emitters with optional package version are added
- `tspconfig.yaml` emitters with options are added to `emit` and
`options` section. **Currently options do not support token
replacement**
- optional messages are display after scaffolding for any additional
environment setup instructions

Here is a sample template. We will evaluate whether to make it a
built-in template:

```JSON
{
  "azure-core": {
    "title": "REST API with client or server project",
    "description": "Create a project representing a generic REST API with options to include client or server project",
    "compilerVersion": "0.63.0",
    "libraries": ["@typespec/http", "@typespec/rest", "@typespec/openapi3"],
    "config": {},
    "emitters": {
      "@typespec/openapi3": {
        "selected": true,
        "description": "Generate OpenAPI 3.0 or 3.1 specification",
        "options": {
          "emitter-output-dir": "{project-root}/openapi/"
        },
        "version": "0.49.0"
      },
      "@typespec/http-client-csharp": {
        "description": "Client: generate CSharp client",
        "options": {
          "emitter-output-dir": "{project-root}/client/csharp/generated"
        },
        "message": "Please ensure dotnet SDK has been installed. Download from https://dotnet.microsoft.com/download"
      },
      "@typespec/http-server-csharp": {
        "description": "Server: generate ASP.NET Core server",
        "options": {
          "emitter-output-dir": "{project-root}/servers/aspnet/generated"
        },
        "message": "Please ensure dotnet SDK has been installed. Download from https://dotnet.microsoft.com/download"
      }
    }
  }
}

```

---------

Co-authored-by: Mark Cowlishaw <markcowl@microsoft.com>
  • Loading branch information
allenjzhang and markcowl authored Dec 20, 2024
1 parent 8214ed6 commit 70f8ea5
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: internal
packages:
- typespec-vscode
---

Updated the init project template to include the new emitter definition
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

Added support for emitter selections for init template.
37 changes: 37 additions & 0 deletions packages/compiler/src/init/init-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export interface InitTemplate {
*/
libraries?: InitTemplateLibrary[];

/**
* List of emitters to include
*/
emitters?: Record<string, EmitterTemplate>;

/**
* Config
*/
Expand All @@ -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.
*/
Expand Down Expand Up @@ -99,6 +120,22 @@ export const InitTemplateSchema: JSONSchemaType<InitTemplate> = {
},
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: {
Expand Down
43 changes: 42 additions & 1 deletion packages/compiler/src/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -75,6 +76,7 @@ export async function initTypeSpecProject(
folderName,
parameters,
includeGitignore,
emitters,
});

await scaffoldNewProject(host, scaffoldingConfig);
Expand All @@ -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<Record<string, any>> {
Expand Down Expand Up @@ -235,6 +249,33 @@ async function validateTemplate(template: any, loaded: LoadedTemplate): Promise<
return true;
}

async function selectEmitters(template: InitTemplate): Promise<Record<string, EmitterTemplate>> {
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<InitTemplateLibrarySpec[]> {
if (template.libraries === undefined || template.libraries.length === 0) {
return [];
Expand Down
36 changes: 30 additions & 6 deletions packages/compiler/src/init/scaffold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export interface ScaffoldingConfig {
* Custom parameters provided in the tempalates.
*/
parameters: Record<string, any>;

/**
* Selected emitters the tempalates.
*/
emitters: Record<string, any>;
}

export function normalizeLibrary(library: InitTemplateLibrary): InitTemplateLibrarySpec {
Expand All @@ -73,6 +78,7 @@ export function makeScaffoldingConfig(
folderName: config.folderName ?? "",
parameters: config.parameters ?? {},
includeGitignore: config.includeGitignore ?? true,
emitters: config.emitters ?? {},
...config,
};
}
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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);
}

Expand All @@ -165,7 +189,7 @@ async function writeMain(host: CompilerHost, config: ScaffoldingConfig) {
const dependencies: Record<string, string> = {};

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}";`), ""];
Expand Down Expand Up @@ -220,7 +244,7 @@ async function writeFile(
return host.writeFile(joinPaths(config.directory, file.destination), content);
}

async function getLibraryVersion(library: InitTemplateLibrarySpec): Promise<string> {
async function getPackageVersion(packageInfo: { version?: string }): Promise<string> {
// TODO: Resolve 'latest' version from npm, issue #1919
return library.version ?? "latest";
return packageInfo.version ?? "latest";
}
2 changes: 2 additions & 0 deletions packages/typespec-vscode/src/vscode-cmd/create-tsp-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) {
Expand Down

0 comments on commit 70f8ea5

Please sign in to comment.