UNPKG

11.4 kBPlain TextView Raw
1import chalk from "chalk";
2import fsExtra from "fs-extra";
3import os from "os";
4import path from "path";
5
6import { BUIDLER_NAME } from "../constants";
7import { ExecutionMode, getExecutionMode } from "../core/execution-mode";
8import { getRecommendedGitIgnore } from "../core/project-structure";
9import { getPackageJson, getPackageRoot } from "../util/packageInfo";
10
11import { emoji } from "./emoji";
12
13const CREATE_SAMPLE_PROJECT_ACTION = "Create a sample project";
14const CREATE_EMPTY_BUIDLER_CONFIG_ACTION = "Create an empty buidler.config.js";
15const QUIT_ACTION = "Quit";
16
17const SAMPLE_PROJECT_DEPENDENCIES = {
18 "@nomiclabs/buidler-waffle": "^2.0.0",
19 "ethereum-waffle": "^3.0.0",
20 chai: "^4.2.0",
21 "@nomiclabs/buidler-ethers": "^2.0.0",
22 ethers: "^5.0.0",
23};
24
25async function removeProjectDirIfPresent(projectRoot: string, dirName: string) {
26 const dirPath = path.join(projectRoot, dirName);
27 if (await fsExtra.pathExists(dirPath)) {
28 await fsExtra.remove(dirPath);
29 }
30}
31
32async function removeTempFilesIfPresent(projectRoot: string) {
33 await removeProjectDirIfPresent(projectRoot, "cache");
34 await removeProjectDirIfPresent(projectRoot, "artifacts");
35}
36
37function printAsciiLogo() {
38 console.log(chalk.blue(`888 d8b 888 888`));
39 console.log(chalk.blue(`888 Y8P 888 888`));
40 console.log(chalk.blue("888 888 888"));
41 console.log(
42 chalk.blue("88888b. 888 888 888 .d88888 888 .d88b. 888d888")
43 );
44 console.log(chalk.blue('888 "88b 888 888 888 d88" 888 888 d8P Y8b 888P"'));
45 console.log(chalk.blue("888 888 888 888 888 888 888 888 88888888 888"));
46 console.log(chalk.blue("888 d88P Y88b 888 888 Y88b 888 888 Y8b. 888"));
47 console.log(chalk.blue(`88888P" "Y88888 888 "Y88888 888 "Y8888 888`));
48 console.log("");
49}
50
51async function printWelcomeMessage() {
52 const packageJson = await getPackageJson();
53
54 console.log(
55 chalk.cyan(
56 `${emoji("👷 ")}Welcome to ${BUIDLER_NAME} v${packageJson.version}${emoji(
57 " 👷‍"
58 )}‍\n`
59 )
60 );
61}
62
63async function copySampleProject(projectRoot: string) {
64 const packageRoot = await getPackageRoot();
65
66 await fsExtra.ensureDir(projectRoot);
67 await fsExtra.copy(path.join(packageRoot, "sample-project"), projectRoot);
68
69 // This is just in case we have been using the sample project for dev/testing
70 await removeTempFilesIfPresent(projectRoot);
71
72 await fsExtra.remove(path.join(projectRoot, "LICENSE.md"));
73}
74
75async function addGitIgnore(projectRoot: string) {
76 const gitIgnorePath = path.join(projectRoot, ".gitignore");
77
78 let content = await getRecommendedGitIgnore();
79
80 if (await fsExtra.pathExists(gitIgnorePath)) {
81 const existingContent = await fsExtra.readFile(gitIgnorePath, "utf-8");
82 content = `${existingContent}
83${content}`;
84 }
85
86 await fsExtra.writeFile(gitIgnorePath, content);
87}
88
89async function addGitAttributes(projectRoot: string) {
90 const gitAttributesPath = path.join(projectRoot, ".gitattributes");
91 let content = "*.sol linguist-language=Solidity";
92
93 if (await fsExtra.pathExists(gitAttributesPath)) {
94 const existingContent = await fsExtra.readFile(gitAttributesPath, "utf-8");
95
96 if (existingContent.includes(content)) {
97 return;
98 }
99
100 content = `${existingContent}
101${content}`;
102 }
103
104 await fsExtra.writeFile(gitAttributesPath, content);
105}
106
107function printSuggestedCommands() {
108 const npx =
109 getExecutionMode() === ExecutionMode.EXECUTION_MODE_GLOBAL_INSTALLATION
110 ? ""
111 : "npx ";
112
113 console.log(`Try running some of the following tasks:`);
114 console.log(` ${npx}buidler accounts`);
115 console.log(` ${npx}buidler compile`);
116 console.log(` ${npx}buidler test`);
117 console.log(` ${npx}buidler node`);
118 console.log(` node scripts/sample-script.js`);
119 console.log(` ${npx}buidler help`);
120}
121
122async function printRecommendedDepsInstallationInstructions() {
123 console.log(
124 `You need to install these dependencies to run the sample project:`
125 );
126
127 const cmd = await getRecommendedDependenciesInstallationCommand();
128
129 console.log(` ${cmd.join(" ")}`);
130}
131
132async function writeEmptyBuidlerConfig() {
133 return fsExtra.writeFile(
134 "buidler.config.js",
135 "module.exports = {};\n",
136 "utf-8"
137 );
138}
139
140async function getAction() {
141 const { default: enquirer } = await import("enquirer");
142 try {
143 const actionResponse = await enquirer.prompt<{ action: string }>([
144 {
145 name: "action",
146 type: "select",
147 message: "What do you want to do?",
148 initial: 0,
149 choices: [
150 {
151 name: CREATE_SAMPLE_PROJECT_ACTION,
152 message: CREATE_SAMPLE_PROJECT_ACTION,
153 value: CREATE_SAMPLE_PROJECT_ACTION,
154 },
155 {
156 name: CREATE_EMPTY_BUIDLER_CONFIG_ACTION,
157 message: CREATE_EMPTY_BUIDLER_CONFIG_ACTION,
158 value: CREATE_EMPTY_BUIDLER_CONFIG_ACTION,
159 },
160 { name: QUIT_ACTION, message: QUIT_ACTION, value: QUIT_ACTION },
161 ],
162 },
163 ]);
164
165 return actionResponse.action;
166 } catch (e) {
167 if (e === "") {
168 return QUIT_ACTION;
169 }
170
171 // tslint:disable-next-line only-buidler-error
172 throw e;
173 }
174}
175
176export async function createProject() {
177 const { default: enquirer } = await import("enquirer");
178 printAsciiLogo();
179
180 await printWelcomeMessage();
181
182 const action = await getAction();
183
184 if (action === QUIT_ACTION) {
185 return;
186 }
187
188 if (action === CREATE_EMPTY_BUIDLER_CONFIG_ACTION) {
189 await writeEmptyBuidlerConfig();
190 console.log(
191 `${emoji("✨ ")}${chalk.cyan(`Config file created`)}${emoji(" ✨")}`
192 );
193 return;
194 }
195
196 let responses: {
197 projectRoot: string;
198 shouldAddGitIgnore: boolean;
199 shouldAddGitAttributes: boolean;
200 };
201
202 try {
203 responses = await enquirer.prompt<typeof responses>([
204 {
205 name: "projectRoot",
206 type: "input",
207 initial: process.cwd(),
208 message: "Buidler project root:",
209 },
210 createConfirmationPrompt(
211 "shouldAddGitIgnore",
212 "Do you want to add a .gitignore?"
213 ),
214 createConfirmationPrompt(
215 "shouldAddGitAttributes",
216 "Do you want to add a .gitattributes to enable Soldity highlighting on GitHub?"
217 ),
218 ]);
219 } catch (e) {
220 if (e === "") {
221 return;
222 }
223
224 // tslint:disable-next-line only-buidler-error
225 throw e;
226 }
227
228 const { projectRoot, shouldAddGitIgnore, shouldAddGitAttributes } = responses;
229
230 await copySampleProject(projectRoot);
231
232 if (shouldAddGitIgnore) {
233 await addGitIgnore(projectRoot);
234 }
235
236 if (shouldAddGitAttributes) {
237 await addGitAttributes(projectRoot);
238 }
239
240 let shouldShowInstallationInstructions = true;
241
242 if (await canInstallRecommendedDeps()) {
243 const recommendedDeps = Object.keys(SAMPLE_PROJECT_DEPENDENCIES);
244 const installedRecommendedDeps = recommendedDeps.filter(isInstalled);
245
246 if (installedRecommendedDeps.length === recommendedDeps.length) {
247 shouldShowInstallationInstructions = false;
248 } else if (installedRecommendedDeps.length === 0) {
249 const shouldInstall = await confirmRecommendedDepsInstallation();
250 if (shouldInstall) {
251 const installed = await installRecommendedDependencies();
252
253 if (!installed) {
254 console.warn(
255 chalk.red("Failed to install the sample project's dependencies")
256 );
257 }
258
259 shouldShowInstallationInstructions = !installed;
260 }
261 }
262 }
263
264 if (shouldShowInstallationInstructions) {
265 console.log(``);
266 await printRecommendedDepsInstallationInstructions();
267 }
268
269 console.log(
270 `\n${emoji("✨ ")}${chalk.cyan("Project created")}${emoji(" ✨")}`
271 );
272
273 console.log(``);
274
275 printSuggestedCommands();
276}
277
278function createConfirmationPrompt(name: string, message: string) {
279 return {
280 type: "confirm",
281 name,
282 message,
283 initial: "y",
284 default: "(Y/n)",
285 isTrue(input: string | boolean) {
286 if (typeof input === "string") {
287 return input.toLowerCase() === "y";
288 }
289
290 return input;
291 },
292 isFalse(input: string | boolean) {
293 if (typeof input === "string") {
294 return input.toLowerCase() === "n";
295 }
296
297 return input;
298 },
299 format(): string {
300 const that = this as any;
301 const value = that.value === true ? "y" : "n";
302
303 if (that.state.submitted === true) {
304 return that.styles.submitted(value);
305 }
306
307 return value;
308 },
309 };
310}
311
312async function canInstallRecommendedDeps() {
313 return (
314 (await fsExtra.pathExists("package.json")) &&
315 (getExecutionMode() === ExecutionMode.EXECUTION_MODE_LOCAL_INSTALLATION ||
316 getExecutionMode() === ExecutionMode.EXECUTION_MODE_LINKED) &&
317 // TODO: Figure out why this doesn't work on Win
318 os.type() !== "Windows_NT"
319 );
320}
321
322function isInstalled(dep: string) {
323 const packageJson = fsExtra.readJSONSync("package.json");
324
325 const allDependencies = {
326 ...packageJson.dependencies,
327 ...packageJson.devDependencies,
328 ...packageJson.optionalDependencies,
329 };
330
331 return dep in allDependencies;
332}
333
334async function isYarnProject() {
335 return fsExtra.pathExists("yarn.lock");
336}
337
338async function installRecommendedDependencies() {
339 console.log("");
340 const installCmd = await getRecommendedDependenciesInstallationCommand();
341 return installDependencies(installCmd[0], installCmd.slice(1));
342}
343
344async function confirmRecommendedDepsInstallation(): Promise<boolean> {
345 const { default: enquirer } = await import("enquirer");
346
347 let responses: {
348 shouldInstallPlugin: boolean;
349 };
350
351 const packageManager = (await isYarnProject()) ? "yarn" : "npm";
352
353 try {
354 responses = await enquirer.prompt<typeof responses>([
355 createConfirmationPrompt(
356 "shouldInstallPlugin",
357 `Do you want to install the sample project's dependencies with ${packageManager} (${Object.keys(
358 SAMPLE_PROJECT_DEPENDENCIES
359 ).join(" ")})?`
360 ),
361 ]);
362 } catch (e) {
363 if (e === "") {
364 return false;
365 }
366
367 // tslint:disable-next-line only-buidler-error
368 throw e;
369 }
370
371 return responses.shouldInstallPlugin === true;
372}
373
374async function installDependencies(
375 packageManager: string,
376 args: string[]
377): Promise<boolean> {
378 const { spawn } = await import("child_process");
379
380 console.log(`${packageManager} ${args.join(" ")}`);
381
382 const childProcess = spawn(packageManager, args, {
383 stdio: "inherit" as any, // There's an error in the TS definition of ForkOptions
384 });
385
386 return new Promise((resolve, reject) => {
387 childProcess.once("close", (status) => {
388 childProcess.removeAllListeners("error");
389
390 if (status === 0) {
391 resolve(true);
392 return;
393 }
394
395 reject(false);
396 });
397
398 childProcess.once("error", (status) => {
399 childProcess.removeAllListeners("close");
400 reject(false);
401 });
402 });
403}
404
405async function getRecommendedDependenciesInstallationCommand(): Promise<
406 string[]
407> {
408 const isGlobal =
409 getExecutionMode() === ExecutionMode.EXECUTION_MODE_GLOBAL_INSTALLATION;
410
411 const deps = Object.entries(SAMPLE_PROJECT_DEPENDENCIES).map(
412 ([name, version]) => `${name}@${version}`
413 );
414
415 if (!isGlobal && (await isYarnProject())) {
416 return ["yarn", "add", "--dev", ...deps];
417 }
418
419 const npmInstall = ["npm", "install"];
420
421 if (isGlobal) {
422 npmInstall.push("--global");
423 }
424
425 return [...npmInstall, "--save-dev", ...deps];
426}