UNPKG

10.1 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: 'react' | 'preact' | 'nerv' | 'vue' | 'vue3'
39}
40
41interface AskMethods {
42 (conf: IProjectConf, prompts: Record<string, unknown>[], 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: Record<string, unknown>[] = []
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: 'PReact',
204 value: 'preact'
205 },
206 {
207 name: 'Nerv',
208 value: 'nerv'
209 },
210 {
211 name: 'Vue',
212 value: 'vue'
213 },
214 {
215 name: 'Vue3',
216 value: 'vue3'
217 }
218 ]
219
220 if ((typeof conf.framework as string | undefined) !== 'string') {
221 prompts.push({
222 type: 'list',
223 name: 'framework',
224 message: '请选择框架',
225 choices: frameworks
226 })
227 }
228 }
229
230 askTemplateSource: AskMethods = async function (conf, prompts) {
231 if (conf.template === 'default' || conf.templateSource) return
232
233 const homedir = getUserHomeDir()
234 const taroConfigPath = path.join(homedir, TARO_CONFIG_FLODER)
235 const taroConfig = path.join(taroConfigPath, TARO_BASE_CONFIG)
236
237 let localTemplateSource: string
238
239 // 检查本地配置
240 if (fs.existsSync(taroConfig)) {
241 // 存在则把模板源读出来
242 const config = await fs.readJSON(taroConfig)
243 localTemplateSource = config?.templateSource
244 } else {
245 // 不存在则创建配置
246 await fs.createFile(taroConfig)
247 await fs.writeJSON(taroConfig, { templateSource: DEFAULT_TEMPLATE_SRC })
248 localTemplateSource = DEFAULT_TEMPLATE_SRC
249 }
250
251 const choices = [
252 {
253 name: 'Gitee(最快)',
254 value: DEFAULT_TEMPLATE_SRC_GITEE
255 },
256 {
257 name: 'Github(最新)',
258 value: DEFAULT_TEMPLATE_SRC
259 },
260 {
261 name: '输入',
262 value: 'self-input'
263 },
264 {
265 name: '社区优质模板源',
266 value: 'open-source'
267 }
268 ]
269
270 if (localTemplateSource && localTemplateSource !== DEFAULT_TEMPLATE_SRC && localTemplateSource !== DEFAULT_TEMPLATE_SRC_GITEE) {
271 choices.unshift({
272 name: `本地模板源:${localTemplateSource}`,
273 value: localTemplateSource
274 })
275 }
276
277 prompts.push({
278 type: 'list',
279 name: 'templateSource',
280 message: '请选择模板源',
281 choices
282 }, {
283 type: 'input',
284 name: 'templateSource',
285 message: '请输入模板源!',
286 when (answers) {
287 return answers.templateSource === 'self-input'
288 }
289 }, {
290 type: 'list',
291 name: 'templateSource',
292 message: '请选择社区模板源',
293 async choices (answers) {
294 const choices = await getOpenSourceTemplates(answers.framework)
295 return choices
296 },
297 when (answers) {
298 return answers.templateSource === 'open-source'
299 }
300 })
301 }
302
303 askTemplate: AskMethods = function (conf, prompts, list = []) {
304 const choices = [
305 {
306 name: '默认模板',
307 value: 'default'
308 },
309 ...list.map(item => ({
310 name: item.desc ? `${item.name}${item.desc})` : item.name,
311 value: item.name
312 }))
313 ]
314
315 if ((typeof conf.template as 'string' | undefined) !== 'string') {
316 prompts.push({
317 type: 'list',
318 name: 'template',
319 message: '请选择模板',
320 choices
321 })
322 }
323 }
324
325 async fetchTemplates (answers): Promise<ITemplates[]> {
326 const { templateSource, framework } = answers
327 this.conf.templateSource = this.conf.templateSource || templateSource
328
329 // 使用默认模版
330 if (this.conf?.template === 'default' || answers.templateSource === NONE_AVALIABLE_TEMPLATE) return Promise.resolve([])
331
332 // 从模板源下载模板
333 const isClone = /gitee/.test(this.conf.templateSource) || this.conf.clone
334 const templateChoices = await fetchTemplate(this.conf.templateSource, this.templatePath(''), isClone)
335
336 // 根据用户选择的框架筛选模板
337 const newTemplateChoices: ITemplates[] = templateChoices
338 .filter(templateChoice => {
339 const { platforms } = templateChoice
340 if (typeof platforms === 'string' && platforms) {
341 return framework === templateChoice.platforms
342 } else if (isArray(platforms)) {
343 return templateChoice.platforms?.includes(framework)
344 } else {
345 return true
346 }
347 })
348
349 return newTemplateChoices
350 }
351
352 write (cb?: () => void) {
353 this.conf.src = SOURCE_DIR
354 createApp(this, this.conf, cb).catch(err => console.log(err))
355 }
356}
357
358function getOpenSourceTemplates (platform) {
359 return new Promise((resolve, reject) => {
360 const spinner = ora('正在拉取开源模板列表...').start()
361 request.get('https://gitee.com/NervJS/awesome-taro/raw/next/index.json', (error, _response, body) => {
362 if (error) {
363 spinner.fail(chalk.red('拉取开源模板列表失败!'))
364 return reject(new Error())
365 }
366
367 spinner.succeed(`${chalk.grey('拉取开源模板列表成功!')}`)
368
369 const collection = JSON.parse(body)
370
371 switch (platform) {
372 case 'react':
373 return resolve(collection.react)
374 case 'vue':
375 return resolve(collection.vue)
376 default:
377 return resolve([NONE_AVALIABLE_TEMPLATE])
378 }
379 })
380 })
381}