UNPKG

11.5 kBPlain TextView Raw
1import {
2 chalk,
3 DEFAULT_TEMPLATE_SRC,
4 DEFAULT_TEMPLATE_SRC_GITEE,
5 getUserHomeDir,
6 SOURCE_DIR,
7 TARO_BASE_CONFIG,
8 TARO_CONFIG_FOLDER
9} from '@tarojs/helper'
10import { isArray } from '@tarojs/shared'
11import * as fs from 'fs-extra'
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_AVALIABLE_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 when (answers) {
320 return answers.templateSource === 'self-input'
321 }
322 }, {
323 type: 'list',
324 name: 'templateSource',
325 message: '请选择社区模板源',
326 async choices (answers) {
327 const choices = await getOpenSourceTemplates(answers.framework)
328 return choices
329 },
330 when (answers) {
331 return answers.templateSource === 'open-source'
332 }
333 })
334 }
335
336 askTemplate: AskMethods = function (conf, prompts, list = []) {
337 const choices = [
338 {
339 name: '默认模板',
340 value: 'default'
341 },
342 ...list.map(item => ({
343 name: item.desc ? `${item.name}${item.desc})` : item.name,
344 value: item.name
345 }))
346 ]
347
348 if ((typeof conf.template as 'string' | undefined) !== 'string') {
349 prompts.push({
350 type: 'list',
351 name: 'template',
352 message: '请选择模板',
353 choices
354 })
355 }
356 }
357
358 askNpm: AskMethods = function (conf, prompts) {
359 const packages = [
360 {
361 name: 'yarn',
362 value: 'yarn'
363 },
364 {
365 name: 'pnpm',
366 value: 'pnpm'
367 },
368 {
369 name: 'npm',
370 value: 'npm'
371 },
372 {
373 name: 'cnpm',
374 value: 'cnpm'
375 }
376 ]
377
378 if ((typeof conf.npm as string | undefined) !== 'string') {
379 prompts.push({
380 type: 'list',
381 name: 'npm',
382 message: '请选择包管理工具',
383 choices: packages
384 })
385 }
386 }
387
388 async fetchTemplates (answers): Promise<ITemplates[]> {
389 const { templateSource, framework } = answers
390 this.conf.templateSource = this.conf.templateSource || templateSource
391
392 // 使用默认模版
393 if (answers.templateSource === 'default-template') {
394 this.conf.template = 'default'
395 answers.templateSource = DEFAULT_TEMPLATE_SRC_GITEE
396 }
397 if (this.conf.template === 'default' || answers.templateSource === NONE_AVALIABLE_TEMPLATE) return Promise.resolve([])
398
399 // 从模板源下载模板
400 const isClone = /gitee/.test(this.conf.templateSource) || this.conf.clone
401 const templateChoices = await fetchTemplate(this.conf.templateSource, this.templatePath(''), isClone)
402
403 // 根据用户选择的框架筛选模板
404 const newTemplateChoices: ITemplates[] = templateChoices
405 .filter(templateChoice => {
406 const { platforms } = templateChoice
407 if (typeof platforms === 'string' && platforms) {
408 return framework === templateChoice.platforms
409 } else if (isArray(platforms)) {
410 return templateChoice.platforms?.includes(framework)
411 } else {
412 return true
413 }
414 })
415
416 return newTemplateChoices
417 }
418
419 write (cb?: () => void) {
420 this.conf.src = SOURCE_DIR
421 createApp(this, this.conf, cb).catch(err => console.log(err))
422 }
423}
424
425function getOpenSourceTemplates (platform) {
426 return new Promise((resolve, reject) => {
427 const spinner = ora('正在拉取开源模板列表...').start()
428 request.get('https://gitee.com/NervJS/awesome-taro/raw/next/index.json', (error, _response, body) => {
429 if (error) {
430 spinner.fail(chalk.red('拉取开源模板列表失败!'))
431 return reject(new Error())
432 }
433
434 spinner.succeed(`${chalk.grey('拉取开源模板列表成功!')}`)
435
436 const collection = JSON.parse(body)
437
438 switch (platform) {
439 case 'react':
440 return resolve(collection.react)
441 case 'vue':
442 return resolve(collection.vue)
443 default:
444 return resolve([NONE_AVALIABLE_TEMPLATE])
445 }
446 })
447 })
448}