microsoft/typespec

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
064161d8277a88b3facccca58df87aa332bf9187

Branches

Tags

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

Clone

HTTPS

Download ZIP

packages/compiler/init/init.ts

334lines · modecode

1import { readdir } from "fs/promises";
2import jsyaml from "js-yaml";
3import Mustache from "mustache";
4import prompts from "prompts";
5import { TypeSpecConfigFilename } from "../config/config-loader.js";
6import { logDiagnostics } from "../core/diagnostics.js";
7import { formatTypeSpec } from "../core/formatter.js";
8import { NodePackage } from "../core/module-resolver.js";
9import { getBaseFileName, getDirectoryPath, 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 full path where the project should be initialized.
23 */
24 directory: string;
25
26 /**
27 * folder name where the project should be initialized.
28 */
29 folderName: string;
30
31 /**
32 * Name of the project.
33 */
34 name: string;
35
36 /**
37 * List of libraries to include
38 */
39 libraries: string[];
40
41 /**
42 * A flag to indicate not adding @typespec/compiler package to package.json.
43 * Other libraries may already brought in the dependency such as Azure template.
44 */
45 skipCompilerPackage: boolean;
46
47 /**
48 * Custom parameters provided in the tempalates.
49 */
50 parameters: Record<string, any>;
51
52 /**
53 * NormalizeVersion function replaces `-` with `_`.
54 */
55 normalizeVersion: () => (text: string, render: any) => string;
56
57 /**
58 * toLowerCase function for template replacement
59 */
60 toLowerCase: () => (text: string, render: any) => string;
61
62 /**
63 * Normalize package name for langauges other than C#. It replaces `.` with `-` and toLowerCase
64 */
65 normalizePackageName: () => (text: string, render: any) => string;
66}
67
68const normalizeVersion = function () {
69 return function (text: string, render: any): string {
70 return render(text).replaceAll("-", "_");
71 };
72};
73
74const toLowerCase = function () {
75 return function (text: string, render: any): string {
76 return render(text).toLowerCase();
77 };
78};
79
80const normalizePackageName = function () {
81 return function (text: string, render: any): string {
82 return render(text).replaceAll(".", "-").toLowerCase();
83 };
84};
85
86export async function initTypeSpecProject(
87 host: CompilerHost,
88 directory: string,
89 templatesUrl?: string
90) {
91 if (!(await confirmDirectoryEmpty(directory))) {
92 return;
93 }
94 const folderName = getBaseFileName(directory);
95
96 const template = await selectTemplate(host, templatesUrl);
97 const { name } = await prompts([
98 {
99 type: "text",
100 name: "name",
101 message: `Project name`,
102 initial: folderName,
103 },
104 ]);
105
106 const libraries = await selectLibraries(template);
107 const parameters = await promptCustomParameters(template);
108 const scaffoldingConfig: ScaffoldingConfig = {
109 ...template,
110 templateUri: templatesUrl ?? ".",
111 libraries,
112 name,
113 directory,
114 skipCompilerPackage: template.skipCompilerPackage ?? false,
115 folderName,
116 parameters,
117 normalizeVersion,
118 toLowerCase,
119 normalizePackageName,
120 };
121 await scaffoldNewProject(host, scaffoldingConfig);
122}
123
124async function promptCustomParameters(template: InitTemplate): Promise<Record<string, any>> {
125 if (!template.inputs) {
126 return {};
127 }
128
129 const promptList = [...Object.entries(template.inputs)].map(([name, input]) => {
130 return {
131 name,
132 type: input.type,
133 message: input.description,
134 initial: input.initialValue,
135 };
136 });
137 return await prompts(promptList);
138}
139
140async function isDirectoryEmpty(directory: string) {
141 try {
142 const files = await readdir(directory);
143 return files.length === 0;
144 } catch {
145 return true;
146 }
147}
148
149async function confirmDirectoryEmpty(directory: string) {
150 if (await isDirectoryEmpty(directory)) {
151 return true;
152 }
153
154 return confirm(
155 `Folder '${directory}' is not empty. Are you sure you want to initialize a new project here?`
156 );
157}
158
159const builtInTemplates: Record<string, InitTemplate> = {
160 empty: {
161 title: "Empty project",
162 description: "Create an empty project.",
163 libraries: [],
164 },
165 rest: {
166 title: "Generic Rest API",
167 description: "Create a project representing a generic Rest API",
168 libraries: ["@typespec/rest", "@typespec/openapi3"],
169 config: {
170 emit: ["@typespec/openapi3"],
171 },
172 },
173};
174
175async function confirm(message: string): Promise<boolean> {
176 const { confirm } = await prompts({
177 name: "confirm",
178 type: "confirm",
179 message,
180 initial: true,
181 });
182 return confirm;
183}
184
185async function downloadTemplates(
186 host: CompilerHost,
187 templatesUrl: string
188): Promise<Record<string, InitTemplate>> {
189 const file = await readUrlOrPath(host, templatesUrl);
190
191 const json = JSON.parse(file.text);
192 validateTemplateDefinitions(host, json, file);
193 return json;
194}
195
196async function selectTemplate(
197 host: CompilerHost,
198 templatesUrl: string | undefined
199): Promise<InitTemplate> {
200 const templates =
201 templatesUrl === undefined ? builtInTemplates : await downloadTemplates(host, templatesUrl);
202 return promptTemplateSelection(templates);
203}
204
205async function promptTemplateSelection(
206 templates: Record<string, InitTemplate>
207): Promise<InitTemplate> {
208 const { templateName } = await prompts({
209 type: "select",
210 name: "templateName",
211 message: "Please select a template",
212 choices: Object.entries(templates).map(([id, template]) => {
213 return { value: id, description: template.description, title: template.title };
214 }),
215 });
216 const template = templates[templateName];
217 if (!template) {
218 throw new Error(`Unexpected error: Cannot find template ${templateName}`);
219 }
220 return template;
221}
222
223async function selectLibraries(template: InitTemplate): Promise<string[]> {
224 if (template.libraries.length === 0) {
225 return [];
226 }
227
228 const libraryChoices = template.libraries.map((x) => ({ name: x, description: "" }));
229
230 const { libraries } = await prompts({
231 type: "multiselect",
232 name: "libraries",
233 message: "Update the libraries?",
234 choices: libraryChoices.map((x) => {
235 return {
236 title: x.name,
237 description: x.description,
238 value: x.name,
239 selected: true,
240 };
241 }),
242 initial: template.libraries as any,
243 });
244
245 return libraries;
246}
247
248export async function scaffoldNewProject(host: CompilerHost, config: ScaffoldingConfig) {
249 await writePackageJson(host, config);
250 await writeConfig(host, config);
251 await writeMain(host, config);
252 await writeFiles(host, config);
253
254 // eslint-disable-next-line no-console
255 console.log("TypeSpec init completed. You can run `tsp install` now to install dependencies.");
256}
257
258async function writePackageJson(host: CompilerHost, config: ScaffoldingConfig) {
259 const dependencies: Record<string, string> = {};
260
261 if (!config.skipCompilerPackage) {
262 dependencies["@typespec/compiler"] = "latest";
263 }
264
265 for (const library of config.libraries) {
266 dependencies[library] = "latest";
267 }
268
269 const packageJson: NodePackage = {
270 name: config.name,
271 version: "0.1.0",
272 type: "module",
273 dependencies,
274 private: true,
275 };
276
277 return host.writeFile(
278 joinPaths(config.directory, "package.json"),
279 JSON.stringify(packageJson, null, 2)
280 );
281}
282
283async function writeConfig(host: CompilerHost, config: ScaffoldingConfig) {
284 if (!config.config) {
285 return;
286 }
287 const content = jsyaml.dump(config.config);
288 return host.writeFile(joinPaths(config.directory, TypeSpecConfigFilename), content);
289}
290
291async function writeMain(host: CompilerHost, config: ScaffoldingConfig) {
292 const dependencies: Record<string, string> = {};
293
294 for (const library of config.libraries) {
295 dependencies[library] = "latest";
296 }
297
298 const lines = [...config.libraries.map((x) => `import "${x}";`), ""];
299 const content = lines.join("\n");
300
301 return host.writeFile(joinPaths(config.directory, "main.tsp"), formatTypeSpec(content));
302}
303
304async function writeFiles(host: CompilerHost, config: ScaffoldingConfig) {
305 if (!config.files) {
306 return;
307 }
308 for (const file of config.files) {
309 await writeFile(host, config, file);
310 }
311}
312
313async function writeFile(host: CompilerHost, config: ScaffoldingConfig, file: InitTemplateFile) {
314 const baseDir = getDirectoryPath(config.templateUri) + "/";
315 const template = await readUrlOrPath(host, resolveRelativeUrlOrPath(baseDir, file.path));
316 const content = Mustache.render(template.text, config);
317 const destinationFilePath = joinPaths(config.directory, file.destination);
318 // create folders in case they don't exist
319 await host.mkdirp(getDirectoryPath(destinationFilePath) + "/");
320 return host.writeFile(joinPaths(config.directory, file.destination), content);
321}
322
323function validateTemplateDefinitions(
324 host: CompilerHost,
325 templates: unknown,
326 file: SourceFile
327): asserts templates is Record<string, InitTemplate> {
328 const validator = createJSONSchemaValidator(InitTemplateDefinitionsSchema);
329 const diagnostics = validator.validate(templates, file);
330 if (diagnostics.length > 0) {
331 logDiagnostics(diagnostics, host.logSink);
332 throw new Error("Template contained error.");
333 }
334}