|
1 | 1 | #!/usr/bin/env node |
2 | 2 |
|
3 | | -import c from "ansi-colors"; |
4 | | -import enquirer from "enquirer"; |
| 3 | +import * as p from "@clack/prompts"; |
5 | 4 | import path from "path"; |
6 | 5 | import fs from "fs"; |
7 | 6 | import { fileURLToPath } from "url"; |
8 | | -import { execSync } from "child_process"; |
| 7 | +import { exec } from "child_process"; |
| 8 | +import { promisify } from "util"; |
| 9 | +import c from "picocolors"; |
9 | 10 |
|
10 | 11 | // Get __dirname in an ES6 module |
11 | 12 | const __filename = fileURLToPath(import.meta.url); |
12 | 13 | const __dirname = path.dirname(__filename); |
13 | 14 |
|
14 | 15 | const templates = [ |
15 | 16 | { |
16 | | - name: "rescript-template-vite", |
17 | | - message: "Vite", |
| 17 | + value: "rescript-template-vite", |
| 18 | + label: "Vite", |
18 | 19 | hint: "Opinionated boilerplate for Vite, Tailwind and ReScript", |
19 | 20 | }, |
20 | 21 | { |
21 | | - name: "rescript-template-nextjs", |
22 | | - message: "Next.js", |
| 22 | + value: "rescript-template-nextjs", |
| 23 | + label: "Next.js", |
23 | 24 | hint: "Opinionated boilerplate for Next.js, Tailwind and ReScript", |
24 | 25 | }, |
25 | 26 | { |
26 | | - name: "rescript-template-basic", |
27 | | - message: "Basic", |
| 27 | + value: "rescript-template-basic", |
| 28 | + label: "Basic", |
28 | 29 | hint: "Command line hello world app", |
29 | 30 | }, |
30 | 31 | ]; |
31 | 32 |
|
| 33 | +function checkCancel(value) { |
| 34 | + if (p.isCancel(value)) { |
| 35 | + p.cancel("Project creation cancelled."); |
| 36 | + process.exit(0); |
| 37 | + } |
| 38 | +} |
| 39 | + |
32 | 40 | function validateProjectName(projectName) { |
33 | 41 | const packageNameRegExp = /^[a-z0-9-]+$/; |
34 | 42 |
|
35 | | - if (packageNameRegExp.test(projectName)) { |
36 | | - return true; |
37 | | - } else { |
| 43 | + if (projectName.trim().length === 0) { |
| 44 | + return "Project name must not be empty."; |
| 45 | + } |
| 46 | + |
| 47 | + if (!packageNameRegExp.test(projectName)) { |
38 | 48 | return "Project name may only contain lower case letters, numbers and hyphens."; |
39 | 49 | } |
40 | | -} |
41 | 50 |
|
42 | | -async function getParams() { |
43 | | - return await enquirer.prompt([ |
44 | | - { |
45 | | - type: "input", |
46 | | - name: "projectName", |
47 | | - message: "What is the name of your new project?", |
48 | | - initial: process.argv[2] || "my-rescript-app", |
49 | | - validate: validateProjectName, |
50 | | - }, |
51 | | - { |
52 | | - type: "select", |
53 | | - name: "templateName", |
54 | | - message: "Select a template", |
55 | | - choices: templates, |
56 | | - }, |
57 | | - ]); |
| 51 | + const projectPath = path.join(process.cwd(), projectName); |
| 52 | + if (fs.existsSync(projectPath)) { |
| 53 | + return `The folder ${projectName} already exist in the current directory.`; |
| 54 | + } |
58 | 55 | } |
59 | 56 |
|
60 | | -function replaceLineInFile(filename, search, replace) { |
61 | | - const contents = fs.readFileSync(filename, "utf8"); |
| 57 | +async function replaceLineInFile(filename, search, replace) { |
| 58 | + const contents = await fs.promises.readFile(filename, "utf8"); |
62 | 59 | const replaced = contents.replace(search, replace); |
63 | | - fs.writeFileSync(filename, replaced, "utf8"); |
64 | | -} |
65 | | - |
66 | | -function setProjectName(templateName, projectName) { |
67 | | - replaceLineInFile("package.json", `"name": "${templateName}"`, `"name": "${projectName}"`); |
68 | | - replaceLineInFile("bsconfig.json", `"name": "${templateName}"`, `"name": "${projectName}"`); |
69 | | -} |
70 | | - |
71 | | -function installPackages() { |
72 | | - console.log("Installing packages. This might take a couple of seconds..."); |
73 | | - |
74 | | - execSync("npm install"); |
75 | | - console.log(`Packages installed.`); |
76 | | -} |
77 | | - |
78 | | -function initGitRepo() { |
79 | | - execSync("git init"); |
80 | | - console.log(`Initialized a git repository.`); |
| 60 | + await fs.promises.writeFile(filename, replaced, "utf8"); |
81 | 61 | } |
82 | 62 |
|
83 | | -function logSuccess(projectName, projectPath) { |
84 | | - console.log(`\n${c.green("✔ Success!")} Created ${projectName} at ${c.green(projectPath)}.`); |
85 | | - |
86 | | - console.log("\nNext steps:"); |
87 | | - console.log(`• ${c.bold("cd " + projectName)}`); |
88 | | - console.log(`• ${c.bold("npm run res:dev")} to start the ReScript compiler in watch mode.`); |
89 | | - console.log(`• See ${c.bold("README.md")} for more information.`); |
90 | | - console.log(`\n${c.bold("Happy hacking!")}`); |
| 63 | +async function setProjectName(templateName, projectName) { |
| 64 | + await replaceLineInFile("package.json", `"name": "${templateName}"`, `"name": "${projectName}"`); |
| 65 | + await replaceLineInFile("bsconfig.json", `"name": "${templateName}"`, `"name": "${projectName}"`); |
91 | 66 | } |
92 | 67 |
|
93 | 68 | async function main() { |
94 | | - console.log(c.cyan(`Welcome to ${c.red("create-rescript-app")}!`)); |
95 | | - console.log("This tool will help you set up your new ReScript project quickly.\n"); |
96 | | - |
97 | | - const { projectName, templateName } = await getParams(); |
98 | | - |
99 | | - console.log(); // newline |
| 69 | + console.clear(); |
| 70 | + |
| 71 | + p.intro(`${c.bgCyan(c.black(" create-rescript-app "))}`); |
| 72 | + |
| 73 | + const projectName = await p.text({ |
| 74 | + message: "What is the name of your new ReScript project?", |
| 75 | + placeholder: process.argv[2] || "my-rescript-app", |
| 76 | + validate: validateProjectName, |
| 77 | + }); |
| 78 | + checkCancel(projectName); |
| 79 | + |
| 80 | + const templateName = await p.select({ |
| 81 | + message: "Select a template", |
| 82 | + options: templates, |
| 83 | + }); |
| 84 | + checkCancel(templateName); |
| 85 | + |
| 86 | + const shouldContinue = await p.confirm({ |
| 87 | + message: `Your new ReScript project ${c.cyan(projectName)} will now be created. Continue?`, |
| 88 | + }); |
| 89 | + checkCancel(shouldContinue); |
| 90 | + |
| 91 | + if (!shouldContinue) { |
| 92 | + p.outro("No project created."); |
| 93 | + process.exit(0); |
| 94 | + } |
100 | 95 |
|
101 | 96 | const templatePath = path.join(__dirname, "templates", templateName); |
102 | 97 | const projectPath = path.join(process.cwd(), projectName); |
103 | 98 |
|
104 | | - if (fs.existsSync(projectPath)) { |
105 | | - console.log(`The folder ${c.red(projectName)} already exist in the current directory.`); |
106 | | - console.log("Please try again with another name."); |
107 | | - process.exit(1); |
108 | | - } |
109 | | - |
110 | | - console.log( |
111 | | - "Creating a new ReScript project", |
112 | | - `in ${c.green(projectPath)} with template ${c.cyan(templateName)}.` |
113 | | - ); |
| 99 | + const s = p.spinner(); |
| 100 | + s.start("Creating project..."); |
114 | 101 |
|
115 | 102 | try { |
116 | | - fs.cpSync(templatePath, projectPath, { recursive: true }); |
| 103 | + await fs.promises.cp(templatePath, projectPath, { recursive: true }); |
117 | 104 | process.chdir(projectPath); |
118 | 105 |
|
119 | | - setProjectName(templateName, projectName); |
120 | | - installPackages(); |
121 | | - initGitRepo(); |
122 | | - logSuccess(projectName, projectPath); |
| 106 | + await setProjectName(templateName, projectName); |
| 107 | + await promisify(exec)("npm install"); |
| 108 | + await promisify(exec)("git init"); |
| 109 | + s.stop("Project created."); |
| 110 | + |
| 111 | + p.note(`cd ${projectName}\nnpm run res:dev`, "Next steps"); |
| 112 | + p.outro(`Happy hacking! See ${c.cyan("README.md")} for more information.`); |
123 | 113 | } catch (error) { |
124 | | - console.log(error); |
| 114 | + s.stop("Installation error."); |
| 115 | + |
| 116 | + p.outro(`Project creation failed.`); |
| 117 | + |
| 118 | + p.log.error(error); |
125 | 119 | } |
126 | 120 | } |
127 | 121 |
|
|
0 commit comments