返回 slidev
index.mjs
根目录 / packages / create-app / index.mjs
1 #!/usr/bin/env node
2 /* eslint-disable no-console */
3
4 import fs from 'node:fs'
5 import { createRequire } from 'node:module'
6 // @ts-check
7 import process from 'node:process'
8 import { fileURLToPath } from 'node:url'
9 import { blue, bold, cyan, dim, green, yellow } from 'ansis'
10 import minimist from 'minimist'
11 import { detect } from 'package-manager-detector/detect'
12 import path from 'pathe'
13 import prompts from 'prompts'
14 import { x } from 'tinyexec'
15
16 const argv = minimist(process.argv.slice(2))
17 const cwd = process.cwd()
18 const require = createRequire(import.meta.url)
19 const __dirname = fileURLToPath(new URL('.', import.meta.url))
20 const { version } = require('./package.json')
21
22 const RE_VALID_PACKAGE_NAME = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/
23 const RE_WHITESPACE = /\s+/g
24 const RE_LEADING_DOT_UNDERSCORE = /^[._]/
25 const RE_NON_ALPHANUMERIC = /[^a-z0-9-~]+/g
26
27 const renameFiles = {
28 _gitignore: '.gitignore',
29 }
30
31 async function init() {
32 console.log()
33 console.log(` ${cyan('●') + blue('■') + yellow('▲')}`)
34 console.log(`${bold(' Slidev') + dim(' Creator')} ${blue(`v${version}`)}`)
35 console.log()
36
37 let targetDir = argv._[0]
38 if (!targetDir) {
39 /**
40 * @type {{ projectName: string }}
41 */
42 const { projectName } = await prompts({
43 type: 'text',
44 name: 'projectName',
45 message: 'Project name:',
46 initial: 'slidev',
47 })
48 targetDir = projectName.trim()
49 }
50 const packageName = await getValidPackageName(targetDir)
51 const root = path.join(cwd, targetDir)
52
53 if (!fs.existsSync(root)) {
54 fs.mkdirSync(root, { recursive: true })
55 }
56 else {
57 const existing = fs.readdirSync(root)
58 if (existing.length) {
59 console.log(yellow(` Target directory "${targetDir}" is not empty.`))
60 /**
61 * @type {{ yes: boolean }}
62 */
63 const { yes } = await prompts({
64 type: 'confirm',
65 name: 'yes',
66 initial: 'Y',
67 message: 'Remove existing files and continue?',
68 })
69 if (yes)
70 emptyDir(root)
71
72 else
73 return
74 }
75 }
76
77 console.log(dim(' Scaffolding project in ') + targetDir + dim(' ...'))
78
79 const templateDir = path.join(__dirname, 'template')
80
81 const write = (file, content) => {
82 const targetPath = path.join(root, renameFiles[file] ?? file)
83 if (content)
84 fs.writeFileSync(targetPath, content)
85 else
86 copy(path.join(templateDir, file), targetPath)
87 }
88
89 const files = fs.readdirSync(templateDir)
90 for (const file of files.filter(f => f !== 'package.json'))
91 write(file)
92
93 const pkg = require(path.join(templateDir, 'package.json'))
94 pkg.name = packageName
95 write('package.json', JSON.stringify(pkg, null, 2))
96
97 console.log(green(' Done.\n'))
98
99 const pkgManager = await detect().then(r => r.name).catch(() => undefined) ?? 'npm'
100
101 /**
102 * @type {{ yes: boolean }}
103 */
104 const { yes } = await prompts({
105 type: 'confirm',
106 name: 'yes',
107 initial: 'Y',
108 message: `Install and start it now${pkgManager ? ` using ${pkgManager}` : ''}?`,
109 })
110
111 if (yes) {
112 if (!pkgManager)
113 return
114
115 writeReadme(pkgManager)
116 await x(pkgManager, ['install'], { nodeOptions: { stdio: 'inherit', cwd: root } })
117 await x(pkgManager, ['run', 'dev'], { nodeOptions: { stdio: 'inherit', cwd: root } })
118 }
119 else {
120 writeReadme(pkgManager)
121 console.log(dim('\n start it later by:\n'))
122 if (root !== cwd)
123 console.log(blue(` cd ${bold(path.relative(cwd, root))}`))
124
125 console.log(blue(` ${pkgManager} install`))
126 console.log(blue(` ${pkgManager} run dev`))
127 console.log()
128 console.log(` ${cyan('●')} ${blue('■')} ${yellow('▲')}`)
129 console.log()
130 }
131
132 function writeReadme(pm = 'npm') {
133 const readmeTemplate = fs.readFileSync(path.join(templateDir, 'README.md'), 'utf-8')
134 const readmeContent = readmeTemplate
135 .replace('npm install', `${pm} install`)
136 .replace('npm run dev', `${pm} run dev`)
137 write('README.md', readmeContent)
138 }
139 }
140
141 function copy(src, dest) {
142 const stat = fs.statSync(src)
143 if (stat.isDirectory())
144 copyDir(src, dest)
145 else
146 fs.copyFileSync(src, dest)
147 }
148
149 async function getValidPackageName(projectName) {
150 projectName = path.basename(projectName)
151 const packageNameRegExp = RE_VALID_PACKAGE_NAME
152 if (packageNameRegExp.test(projectName)) {
153 return projectName
154 }
155 else {
156 const suggestedPackageName = projectName
157 .trim()
158 .toLowerCase()
159 .replace(RE_WHITESPACE, '-')
160 .replace(RE_LEADING_DOT_UNDERSCORE, '')
161 .replace(RE_NON_ALPHANUMERIC, '-')
162
163 /**
164 * @type {{ inputPackageName: string }}
165 */
166 const { inputPackageName } = await prompts({
167 type: 'text',
168 name: 'inputPackageName',
169 message: 'Package name:',
170 initial: suggestedPackageName,
171 validate: input => packageNameRegExp.test(input) ? true : 'Invalid package.json name',
172 })
173 return inputPackageName
174 }
175 }
176
177 function copyDir(srcDir, destDir) {
178 fs.mkdirSync(destDir, { recursive: true })
179 for (const file of fs.readdirSync(srcDir)) {
180 const srcFile = path.resolve(srcDir, file)
181 const destFile = path.resolve(destDir, file)
182 copy(srcFile, destFile)
183 }
184 }
185
186 function emptyDir(dir) {
187 if (!fs.existsSync(dir))
188 return
189
190 for (const file of fs.readdirSync(dir)) {
191 const abs = path.resolve(dir, file)
192 // baseline is Node 12 so can't use rmSync :(
193 if (fs.lstatSync(abs).isDirectory()) {
194 emptyDir(abs)
195 fs.rmdirSync(abs)
196 }
197 else {
198 fs.unlinkSync(abs)
199 }
200 }
201 }
202
203 init().catch((e) => {
204 console.error(e)
205 })
206
206 lines Plain Text