UNPKG

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