UNPKG

11.6 kBPlain TextView Raw
1import {
2 chalk,
3 DEFAULT_TEMPLATE_SRC,
4 DEFAULT_TEMPLATE_SRC_GITEE,
5 fs,
6 getUserHomeDir,
7 SOURCE_DIR,
8 TARO_BASE_CONFIG,
9 TARO_CONFIG_FOLDER
10} from '@tarojs/helper'
11import { isArray } from '@tarojs/shared'
12import * as inquirer from 'inquirer'
13import * as ora from 'ora'
14import * as path from 'path'
15import * as request from 'request'
16import * as semver from 'semver'
17
18import { clearConsole } from '../util'
19import Creator from './creator'
20import fetchTemplate from './fetchTemplate'
21import { createApp } from './init'
22
23import type { ITemplates } from './fetchTemplate'
24
25export interface IProjectConf {
26 projectName: string
27 projectDir: string
28 npm: string
29 templateSource: string
30 clone?: boolean
31 template: string
32 description?: string
33 typescript?: boolean
34 css: 'none' | 'sass' | 'stylus' | 'less'
35 date?: string
36 src?: string
37 sourceRoot?: string
38 env?: string
39 autoInstall?: boolean
40 framework: 'react' | 'preact' | 'nerv' | 'vue' | 'vue3'
41 compiler?: 'webpack4' | 'webpack5' | 'vite'
42}
43
44interface AskMethods {
45 (conf: IProjectConf, prompts: Record<string, unknown>[], choices?: ITemplates[]): void
46}
47
48const NONE_AVAILABLE_TEMPLATE = '无可用模板'
49
50export default class Project extends Creator {
51 public rootPath: string
52 public conf: IProjectConf
53
54 constructor (options: IProjectConf) {
55 super(options.sourceRoot)
56 const unSupportedVer = semver.lt(process.version, 'v7.6.0')
57 if (unSupportedVer) {
58 throw new Error('Node.js 版本过低,推荐升级 Node.js 至 v8.0.0+')
59 }
60 this.rootPath = this._rootPath
61
62 this.conf = Object.assign(
63 {
64 projectName: '',
65 projectDir: '',
66 template: '',
67 description: '',
68 npm: ''
69 },
70 options
71 )
72 }
73
74 init () {
75 clearConsole()
76 console.log(chalk.green('Taro 即将创建一个新项目!'))
77 console.log(`Need help? Go and open issue: ${chalk.blueBright('https://tls.jd.com/taro-issue-helper')}`)
78 console.log()
79 }
80
81 async create () {
82 try {
83 const answers = await this.ask()
84 const date = new Date()
85 this.conf = Object.assign(this.conf, answers)
86 this.conf.date = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
87 this.write()
88 } catch (error) {
89 console.log(chalk.red('创建项目失败: ', error))
90 }
91 }
92
93 async ask () {
94 let prompts: Record<string, unknown>[] = []
95 const conf = this.conf
96
97 this.askProjectName(conf, prompts)
98 this.askDescription(conf, prompts)
99 this.askFramework(conf, prompts)
100 this.askTypescript(conf, prompts)
101 this.askCSS(conf, prompts)
102 this.askCompiler(conf, prompts)
103 this.askNpm(conf, prompts)
104 await this.askTemplateSource(conf, prompts)
105
106 const answers = await inquirer.prompt(prompts)
107
108 prompts = []
109 const templates = await this.fetchTemplates(answers)
110 await this.askTemplate(conf, prompts, templates)
111 const templateChoiceAnswer = await inquirer.prompt(prompts)
112
113 return {
114 ...answers,
115 ...templateChoiceAnswer
116 }
117 }
118
119 askProjectName: AskMethods = function (conf, prompts) {
120 if ((typeof conf.projectName as string | undefined) !== 'string') {
121 prompts.push({
122 type: 'input',
123 name: 'projectName',
124 message: '请输入项目名称!',
125 validate (input) {
126 if (!input) {
127 return '项目名不能为空!'
128 }
129 if (fs.existsSync(input)) {
130 return '当前目录已经存在同名项目,请换一个项目名!'
131 }
132 return true
133 }
134 })
135 } else if (fs.existsSync(conf.projectName)) {
136 prompts.push({
137 type: 'input',
138 name: 'projectName',
139 message: '当前目录已经存在同名项目,请换一个项目名!',
140 validate (input) {
141 if (!input) {
142 return '项目名不能为空!'
143 }
144 if (fs.existsSync(input)) {
145 return '项目名依然重复!'
146 }
147 return true
148 }
149 })
150 }
151 }
152
153 askDescription: AskMethods = function (conf, prompts) {
154 if (typeof conf.description !== 'string') {
155 prompts.push({
156 type: 'input',
157 name: 'description',
158 message: '请输入项目介绍'
159 })
160 }
161 }
162
163 askTypescript: AskMethods = function (conf, prompts) {
164 if (typeof conf.typescript !== 'boolean') {
165 prompts.push({
166 type: 'confirm',
167 name: 'typescript',
168 message: '是否需要使用 TypeScript ?'
169 })
170 }
171 }
172
173 askCSS: AskMethods = function (conf, prompts) {
174 const cssChoices = [
175 {
176 name: 'Sass',
177 value: 'sass'
178 },
179 {
180 name: 'Less',
181 value: 'less'
182 },
183 {
184 name: 'Stylus',
185 value: 'stylus'
186 },
187 {
188 name: '无',
189 value: 'none'
190 }
191 ]
192
193 if ((typeof conf.css as string | undefined) !== 'string') {
194 prompts.push({
195 type: 'list',
196 name: 'css',
197 message: '请选择 CSS 预处理器(Sass/Less/Stylus)',
198 choices: cssChoices
199 })
200 }
201 }
202
203 askCompiler: AskMethods = function (conf, prompts) {
204 const compilerChoices = [
205 {
206 name: 'Webpack5',
207 value: 'webpack5'
208 },
209 {
210 name: 'Webpack4',
211 value: 'webpack4'
212 }
213 ]
214
215 if ((typeof conf.compiler as string | undefined) !== 'string') {
216 prompts.push({
217 type: 'list',
218 name: 'compiler',
219 message: '请选择编译工具',
220 choices: compilerChoices
221 })
222 }
223 }
224
225 askFramework: AskMethods = function (conf, prompts) {
226 const frameworks = [
227 {
228 name: 'React',
229 value: 'react'
230 },
231 {
232 name: 'PReact',
233 value: 'preact'
234 },
235 // {
236 // name: 'Nerv',
237 // value: 'nerv'
238 // },
239 {
240 name: 'Vue',
241 value: 'vue'
242 },
243 {
244 name: 'Vue3',
245 value: 'vue3'
246 }
247 ]
248
249 if ((typeof conf.framework as string | undefined) !== 'string') {
250 prompts.push({
251 type: 'list',
252 name: 'framework',
253 message: '请选择框架',
254 choices: frameworks
255 })
256 }
257 }
258
259 askTemplateSource: AskMethods = async function (conf, prompts) {
260 if (conf.template === 'default' || conf.templateSource) return
261
262 const homedir = getUserHomeDir()
263 const taroConfigPath = path.join(homedir, TARO_CONFIG_FOLDER)
264 const taroConfig = path.join(taroConfigPath, TARO_BASE_CONFIG)
265
266 let localTemplateSource: string
267
268 // 检查本地配置
269 if (fs.existsSync(taroConfig)) {
270 // 存在则把模板源读出来
271 const config = await fs.readJSON(taroConfig)
272 localTemplateSource = config?.templateSource
273 } else {
274 // 不存在则创建配置
275 await fs.createFile(taroConfig)
276 await fs.writeJSON(taroConfig, { templateSource: DEFAULT_TEMPLATE_SRC })
277 localTemplateSource = DEFAULT_TEMPLATE_SRC
278 }
279
280 const choices = [
281 {
282 name: 'Gitee(最快)',
283 value: DEFAULT_TEMPLATE_SRC_GITEE
284 },
285 {
286 name: 'Github(最新)',
287 value: DEFAULT_TEMPLATE_SRC
288 },
289 {
290 name: 'CLI 内置默认模板',
291 value: 'default-template'
292 },
293 {
294 name: '自定义',
295 value: 'self-input'
296 },
297 {
298 name: '社区优质模板源',
299 value: 'open-source'
300 }
301 ]
302
303 if (localTemplateSource && localTemplateSource !== DEFAULT_TEMPLATE_SRC && localTemplateSource !== DEFAULT_TEMPLATE_SRC_GITEE) {
304 choices.unshift({
305 name: `本地模板源:${localTemplateSource}`,
306 value: localTemplateSource
307 })
308 }
309
310 prompts.push({
311 type: 'list',
312 name: 'templateSource',
313 message: '请选择模板源',
314 choices
315 }, {
316 type: 'input',
317 name: 'templateSource',
318 message: '请输入模板源!',
319 askAnswered: true,
320 when (answers) {
321 return answers.templateSource === 'self-input'
322 }
323 }, {
324 type: 'list',
325 name: 'templateSource',
326 message: '请选择社区模板源',
327 async choices (answers) {
328 const choices = await getOpenSourceTemplates(answers.framework)
329 return choices
330 },
331 askAnswered: true,
332 when (answers) {
333 return answers.templateSource === 'open-source'
334 }
335 })
336 }
337
338 askTemplate: AskMethods = function (conf, prompts, list = []) {
339 const choices = [
340 {
341 name: '默认模板',
342 value: 'default'
343 },
344 ...list.map(item => ({
345 name: item.desc ? `${item.name}${item.desc})` : item.name,
346 value: item.name
347 }))
348 ]
349
350 if ((typeof conf.template as 'string' | undefined) !== 'string') {
351 prompts.push({
352 type: 'list',
353 name: 'template',
354 message: '请选择模板',
355 choices
356 })
357 }
358 }
359
360 askNpm: AskMethods = function (conf, prompts) {
361 const packages = [
362 {
363 name: 'yarn',
364 value: 'yarn'
365 },
366 {
367 name: 'pnpm',
368 value: 'pnpm'
369 },
370 {
371 name: 'npm',
372 value: 'npm'
373 },
374 {
375 name: 'cnpm',
376 value: 'cnpm'
377 }
378 ]
379
380 if ((typeof conf.npm as string | undefined) !== 'string') {
381 prompts.push({
382 type: 'list',
383 name: 'npm',
384 message: '请选择包管理工具',
385 choices: packages
386 })
387 }
388 }
389
390 async fetchTemplates (answers): Promise<ITemplates[]> {
391 const { templateSource, framework } = answers
392 this.conf.templateSource = this.conf.templateSource || templateSource
393
394 // 使用默认模版
395 if (answers.templateSource === 'default-template') {
396 this.conf.template = 'default'
397 answers.templateSource = DEFAULT_TEMPLATE_SRC_GITEE
398 }
399 if (this.conf.template === 'default' || answers.templateSource === NONE_AVAILABLE_TEMPLATE) return Promise.resolve([])
400
401 // 从模板源下载模板
402 const isClone = /gitee/.test(this.conf.templateSource) || this.conf.clone
403 const templateChoices = await fetchTemplate(this.conf.templateSource, this.templatePath(''), isClone)
404
405 // 根据用户选择的框架筛选模板
406 const newTemplateChoices: ITemplates[] = templateChoices
407 .filter(templateChoice => {
408 const { platforms } = templateChoice
409 if (typeof platforms === 'string' && platforms) {
410 return framework === templateChoice.platforms
411 } else if (isArray(platforms)) {
412 return templateChoice.platforms?.includes(framework)
413 } else {
414 return true
415 }
416 })
417
418 return newTemplateChoices
419 }
420
421 write (cb?: () => void) {
422 this.conf.src = SOURCE_DIR
423 createApp(this, this.conf, cb).catch(err => console.log(err))
424 }
425}
426
427function getOpenSourceTemplates (platform) {
428 return new Promise((resolve, reject) => {
429 const spinner = ora({ text: '正在拉取开源模板列表...', discardStdin: false }).start()
430 request.get('https://gitee.com/NervJS/awesome-taro/raw/next/index.json', (error, _response, body) => {
431 if (error) {
432 spinner.fail(chalk.red('拉取开源模板列表失败!'))
433 return reject(new Error())
434 }
435
436 spinner.succeed(`${chalk.grey('拉取开源模板列表成功!')}`)
437
438 const collection = JSON.parse(body)
439
440 switch (platform) {
441 case 'react':
442 return resolve(collection.react)
443 case 'vue':
444 return resolve(collection.vue)
445 default:
446 return resolve([NONE_AVAILABLE_TEMPLATE])
447 }
448 })
449 })
450}