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