microsoft/typespec

Public

mirrored from https://github.com/microsoft/typespecAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
8e2e5c72cd7acfb2ca667d5c4e7c45d93bc98f02

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

packages/compiler/init/init.ts

282lines · modecode

1import { readdir } from "fs/promises";
2import jsyaml from "js-yaml";
3import Mustache from "mustache";
4import prompts from "prompts";
5import { CadlConfigFilename } from "../config/config-loader.js";
6import { logDiagnostics } from "../core/diagnostics.js";
7import { formatCadl } from "../core/formatter.js";
8import { NodePackage } from "../core/module-resolver.js";
9import { getBaseFileName, joinPaths } from "../core/path-utils.js";
10import { createJSONSchemaValidator } from "../core/schema-validator.js";
11import { CompilerHost, SourceFile } from "../core/types.js";
12import { readUrlOrPath, resolveRelativeUrlOrPath } from "../core/util.js";
13import { InitTemplate, InitTemplateDefinitionsSchema, InitTemplateFile } from "./init-template.js";
14
15interface ScaffoldingConfig extends InitTemplate {
16 /**
17 * Path where this template was loaded from.
18 */
19 templateUri: string;
20
21 /**
22 * Directory where the project should be initialized.
23 */
24 directory: string;
25
26 /**
27 * Name of the project.
28 */
29 name: string;
30
31 /**
32 * List of libraries to include
33 */
34 libraries: string[];
35
36 /**
37 * Custom parameters provided in the tempalates.
38 */
39 parameters: Record<string, any>;
40}
41
42export async function initCadlProject(
43 host: CompilerHost,
44 directory: string,
45 templatesUrl?: string
46) {
47 if (!(await confirmDirectoryEmpty(directory))) {
48 return;
49 }
50 const folderName = getBaseFileName(directory);
51
52 const template = await selectTemplate(host, templatesUrl);
53 const { name } = await prompts([
54 {
55 type: "text",
56 name: "name",
57 message: `Project name`,
58 initial: folderName,
59 },
60 ]);
61
62 const libraries = await selectLibraries(template);
63 const parameters = await promptCustomParameters(template);
64 const scaffoldingConfig: ScaffoldingConfig = {
65 ...template,
66 templateUri: templatesUrl ?? ".",
67 libraries,
68 name,
69 directory,
70 parameters,
71 };
72 await scaffoldNewProject(host, scaffoldingConfig);
73}
74
75async function promptCustomParameters(template: InitTemplate): Promise<Record<string, any>> {
76 if (!template.inputs) {
77 return {};
78 }
79
80 const promptList = [...Object.entries(template.inputs)].map(([name, input]) => {
81 return {
82 name,
83 type: input.type,
84 message: input.description,
85 initial: input.initialValue,
86 };
87 });
88 return await prompts(promptList);
89}
90
91async function isDirectoryEmpty(directory: string) {
92 try {
93 const files = await readdir(directory);
94 return files.length === 0;
95 } catch {
96 return true;
97 }
98}
99
100async function confirmDirectoryEmpty(directory: string) {
101 if (await isDirectoryEmpty(directory)) {
102 return true;
103 }
104
105 return confirm(
106 `Folder '${directory}' is not empty. Are you sure you want to initialize a new project here?`
107 );
108}
109
110const builtInTemplates: Record<string, InitTemplate> = {
111 empty: {
112 title: "Empty project",
113 description: "Create an empty project.",
114 libraries: [],
115 },
116 rest: {
117 title: "Generic Rest API",
118 description: "Create a project representing a generic Rest API",
119 libraries: ["@cadl-lang/rest", "@cadl-lang/openapi3"],
120 config: {
121 emit: ["@cadl-lang/openapi3"],
122 },
123 },
124};
125
126async function confirm(message: string): Promise<boolean> {
127 const { confirm } = await prompts({
128 name: "confirm",
129 type: "confirm",
130 message,
131 initial: true,
132 });
133 return confirm;
134}
135
136async function downloadTemplates(
137 host: CompilerHost,
138 templatesUrl: string
139): Promise<Record<string, InitTemplate>> {
140 const file = await readUrlOrPath(host, templatesUrl);
141
142 const json = JSON.parse(file.text);
143 validateTemplateDefinitions(host, json, file);
144 return json;
145}
146
147async function selectTemplate(
148 host: CompilerHost,
149 templatesUrl: string | undefined
150): Promise<InitTemplate> {
151 const templates =
152 templatesUrl === undefined ? builtInTemplates : await downloadTemplates(host, templatesUrl);
153 return promptTemplateSelection(templates);
154}
155
156async function promptTemplateSelection(
157 templates: Record<string, InitTemplate>
158): Promise<InitTemplate> {
159 const { templateName } = await prompts({
160 type: "select",
161 name: "templateName",
162 message: "Please select a template",
163 choices: Object.entries(templates).map(([id, template]) => {
164 return { value: id, description: template.description, title: template.title };
165 }),
166 });
167 const template = templates[templateName];
168 if (!template) {
169 throw new Error(`Unexpected error: Cannot find template ${templateName}`);
170 }
171 return template;
172}
173
174async function selectLibraries(template: InitTemplate): Promise<string[]> {
175 if (template.libraries.length === 0) {
176 return [];
177 }
178
179 const libraryChoices = template.libraries.map((x) => ({ name: x, description: "" }));
180
181 const { libraries } = await prompts({
182 type: "multiselect",
183 name: "libraries",
184 message: "Update the libraries?",
185 choices: libraryChoices.map((x) => {
186 return {
187 title: x.name,
188 description: x.description,
189 value: x.name,
190 selected: true,
191 };
192 }),
193 initial: template.libraries as any,
194 });
195
196 return libraries;
197}
198
199export async function scaffoldNewProject(host: CompilerHost, config: ScaffoldingConfig) {
200 await writePackageJson(host, config);
201 await writeConfig(host, config);
202 await writeMain(host, config);
203 await writeFiles(host, config);
204
205 // eslint-disable-next-line no-console
206 console.log("Cadl init completed. You can run `cadl install` now to install dependencies.");
207}
208
209async function writePackageJson(host: CompilerHost, config: ScaffoldingConfig) {
210 const dependencies: Record<string, string> = {
211 "@cadl-lang/compiler": "latest",
212 };
213
214 for (const library of config.libraries) {
215 dependencies[library] = "latest";
216 }
217
218 const packageJson: NodePackage = {
219 name: config.name,
220 version: "0.1.0",
221 type: "module",
222 dependencies,
223 private: true,
224 };
225
226 return host.writeFile(
227 joinPaths(config.directory, "package.json"),
228 JSON.stringify(packageJson, null, 2)
229 );
230}
231
232async function writeConfig(host: CompilerHost, config: ScaffoldingConfig) {
233 if (!config.config) {
234 return;
235 }
236 const content = jsyaml.dump(config.config);
237 return host.writeFile(joinPaths(config.directory, CadlConfigFilename), content);
238}
239
240async function writeMain(host: CompilerHost, config: ScaffoldingConfig) {
241 const dependencies: Record<string, string> = {};
242
243 for (const library of config.libraries) {
244 dependencies[library] = "latest";
245 }
246
247 const lines = [...config.libraries.map((x) => `import "${x}";`), ""];
248 const content = lines.join("\n");
249
250 return host.writeFile(joinPaths(config.directory, "main.cadl"), formatCadl(content));
251}
252
253async function writeFiles(host: CompilerHost, config: ScaffoldingConfig) {
254 if (!config.files) {
255 return;
256 }
257 for (const file of config.files) {
258 await writeFile(host, config, file);
259 }
260}
261
262async function writeFile(host: CompilerHost, config: ScaffoldingConfig, file: InitTemplateFile) {
263 const template = await readUrlOrPath(
264 host,
265 resolveRelativeUrlOrPath(config.templateUri, file.path)
266 );
267 const content = Mustache.render(template.text, config);
268 return host.writeFile(joinPaths(config.directory, file.destination), content);
269}
270
271function validateTemplateDefinitions(
272 host: CompilerHost,
273 templates: unknown,
274 file: SourceFile
275): asserts templates is Record<string, InitTemplate> {
276 const validator = createJSONSchemaValidator(InitTemplateDefinitionsSchema);
277 const diagnostics = validator.validate(templates, file);
278 if (diagnostics.length > 0) {
279 logDiagnostics(diagnostics, host.logSink);
280 throw new Error("Template contained error.");
281 }
282}
283