1 | import chalk from "chalk";
|
2 | import fsExtra from "fs-extra";
|
3 | import os from "os";
|
4 | import path from "path";
|
5 |
|
6 | import { BUIDLER_NAME } from "../constants";
|
7 | import { ExecutionMode, getExecutionMode } from "../core/execution-mode";
|
8 | import { getRecommendedGitIgnore } from "../core/project-structure";
|
9 | import { getPackageJson, getPackageRoot } from "../util/packageInfo";
|
10 |
|
11 | import { emoji } from "./emoji";
|
12 |
|
13 | const CREATE_SAMPLE_PROJECT_ACTION = "Create a sample project";
|
14 | const CREATE_EMPTY_BUIDLER_CONFIG_ACTION = "Create an empty buidler.config.js";
|
15 | const QUIT_ACTION = "Quit";
|
16 |
|
17 | const 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 |
|
25 | async 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 |
|
32 | async function removeTempFilesIfPresent(projectRoot: string) {
|
33 | await removeProjectDirIfPresent(projectRoot, "cache");
|
34 | await removeProjectDirIfPresent(projectRoot, "artifacts");
|
35 | }
|
36 |
|
37 | function 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 |
|
51 | async 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 |
|
63 | async 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 |
|
70 | await removeTempFilesIfPresent(projectRoot);
|
71 |
|
72 | await fsExtra.remove(path.join(projectRoot, "LICENSE.md"));
|
73 | }
|
74 |
|
75 | async 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 |
|
89 | async 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 |
|
107 | function 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 |
|
122 | async 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 |
|
132 | async function writeEmptyBuidlerConfig() {
|
133 | return fsExtra.writeFile(
|
134 | "buidler.config.js",
|
135 | "module.exports = {};\n",
|
136 | "utf-8"
|
137 | );
|
138 | }
|
139 |
|
140 | async 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 |
|
172 | throw e;
|
173 | }
|
174 | }
|
175 |
|
176 | export 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 |
|
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 |
|
278 | function 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 |
|
312 | async function canInstallRecommendedDeps() {
|
313 | return (
|
314 | (await fsExtra.pathExists("package.json")) &&
|
315 | (getExecutionMode() === ExecutionMode.EXECUTION_MODE_LOCAL_INSTALLATION ||
|
316 | getExecutionMode() === ExecutionMode.EXECUTION_MODE_LINKED) &&
|
317 |
|
318 | os.type() !== "Windows_NT"
|
319 | );
|
320 | }
|
321 |
|
322 | function 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 |
|
334 | async function isYarnProject() {
|
335 | return fsExtra.pathExists("yarn.lock");
|
336 | }
|
337 |
|
338 | async function installRecommendedDependencies() {
|
339 | console.log("");
|
340 | const installCmd = await getRecommendedDependenciesInstallationCommand();
|
341 | return installDependencies(installCmd[0], installCmd.slice(1));
|
342 | }
|
343 |
|
344 | async 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 |
|
368 | throw e;
|
369 | }
|
370 |
|
371 | return responses.shouldInstallPlugin === true;
|
372 | }
|
373 |
|
374 | async 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,
|
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 |
|
405 | async 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 | }
|