1 | import * as path from 'path'
|
2 | import * as fs from 'fs-extra'
|
3 | import * as inquirer from 'inquirer'
|
4 | import * as semver from 'semver'
|
5 | import {
|
6 | DEFAULT_TEMPLATE_SRC,
|
7 | TARO_CONFIG_FLODER,
|
8 | TARO_BASE_CONFIG,
|
9 | getUserHomeDir,
|
10 | chalk,
|
11 | SOURCE_DIR
|
12 | } from '@tarojs/helper'
|
13 | import { isArray } from '@tarojs/shared'
|
14 |
|
15 | import { createApp } from './init'
|
16 | import fetchTemplate from './fetchTemplate'
|
17 | import Creator from './creator'
|
18 |
|
19 | import type { ITemplates } from './fetchTemplate'
|
20 |
|
21 | export interface IProjectConf {
|
22 | projectName: string;
|
23 | projectDir: string;
|
24 | templateSource: string;
|
25 | clone?: boolean;
|
26 | template: string;
|
27 | description?: string;
|
28 | typescript?: boolean;
|
29 | css: 'none' | 'sass' | 'stylus' | 'less';
|
30 | date?: string;
|
31 | src?: string;
|
32 | sourceRoot?: string;
|
33 | env?: string;
|
34 | autoInstall?: boolean,
|
35 | framework: 'nerv' | 'react' | 'vue' | 'vue3'
|
36 | }
|
37 |
|
38 | interface AskMethods {
|
39 | (conf: IProjectConf, prompts: object[], choices?: ITemplates[]): void;
|
40 | }
|
41 |
|
42 | export default class Project extends Creator {
|
43 | public rootPath: string
|
44 | public conf: IProjectConf
|
45 |
|
46 | constructor (options: IProjectConf) {
|
47 | super(options.sourceRoot)
|
48 | const unSupportedVer = semver.lt(process.version, 'v7.6.0')
|
49 | if (unSupportedVer) {
|
50 | throw new Error('Node.js 版本过低,推荐升级 Node.js 至 v8.0.0+')
|
51 | }
|
52 | this.rootPath = this._rootPath
|
53 |
|
54 | this.conf = Object.assign(
|
55 | {
|
56 | projectName: '',
|
57 | projectDir: '',
|
58 | template: '',
|
59 | description: ''
|
60 | },
|
61 | options
|
62 | )
|
63 | }
|
64 |
|
65 | init () {
|
66 | console.log(chalk.green('Taro即将创建一个新项目!'))
|
67 | console.log('Need help? Go and open issue: https://github.com/NervJS/taro/issues/new')
|
68 | console.log()
|
69 | }
|
70 |
|
71 | create () {
|
72 | this.fetchTemplates()
|
73 | .then((templateChoices: ITemplates[]) => {
|
74 | return this.ask(templateChoices)
|
75 | })
|
76 | .then(answers => {
|
77 | const date = new Date()
|
78 | this.conf = Object.assign(this.conf, answers)
|
79 | this.conf.date = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
|
80 | this.write()
|
81 | })
|
82 | .catch(err => console.log(chalk.red('创建项目失败: ', err)))
|
83 | }
|
84 |
|
85 | async fetchTemplates (): Promise<ITemplates[]> {
|
86 | const conf = this.conf
|
87 |
|
88 | if (conf.template && conf.template === 'default') {
|
89 | return Promise.resolve([])
|
90 | }
|
91 |
|
92 |
|
93 | if (!conf.templateSource) {
|
94 | const homedir = getUserHomeDir()
|
95 | if (!homedir) {
|
96 | chalk.yellow('找不到用户根目录,使用默认模版源!')
|
97 | conf.templateSource = DEFAULT_TEMPLATE_SRC
|
98 | }
|
99 |
|
100 | const taroConfigPath = path.join(homedir, TARO_CONFIG_FLODER)
|
101 | const taroConfig = path.join(taroConfigPath, TARO_BASE_CONFIG)
|
102 |
|
103 | if (fs.existsSync(taroConfig)) {
|
104 | const config = await fs.readJSON(taroConfig)
|
105 | conf.templateSource = config && config.templateSource ? config.templateSource : DEFAULT_TEMPLATE_SRC
|
106 | } else {
|
107 | await fs.createFile(taroConfig)
|
108 | await fs.writeJSON(taroConfig, { templateSource: DEFAULT_TEMPLATE_SRC })
|
109 | conf.templateSource = DEFAULT_TEMPLATE_SRC
|
110 | }
|
111 | }
|
112 |
|
113 |
|
114 | return fetchTemplate(this.conf.templateSource, this.templatePath(''), this.conf.clone)
|
115 | }
|
116 |
|
117 | ask (templateChoices: ITemplates[]) {
|
118 | const prompts: object[] = []
|
119 | const templateChoicesPrompts: object[] = []
|
120 | const conf = this.conf
|
121 |
|
122 | this.askProjectName(conf, prompts)
|
123 | this.askDescription(conf, prompts)
|
124 | this.askFramework(conf, prompts)
|
125 | this.askTypescript(conf, prompts)
|
126 | this.askCSS(conf, prompts)
|
127 |
|
128 | return inquirer.prompt(prompts).then(answers => {
|
129 | const newTemplateChoices: ITemplates[] = templateChoices
|
130 | .filter(templateChoice => {
|
131 | const { platforms } = templateChoice
|
132 | if (typeof platforms === 'string') {
|
133 | return answers.framework === templateChoice.platforms
|
134 | } else if (isArray(platforms)) {
|
135 | return templateChoice.platforms?.includes(answers.framework)
|
136 | } else {
|
137 | return true
|
138 | }
|
139 | })
|
140 | this.askTemplate(conf, templateChoicesPrompts, newTemplateChoices)
|
141 | return inquirer.prompt(templateChoicesPrompts)
|
142 | .then(templateChoiceAnswer => {
|
143 | return {
|
144 | ...answers,
|
145 | ...templateChoiceAnswer
|
146 | }
|
147 | })
|
148 | })
|
149 | }
|
150 |
|
151 | askProjectName: AskMethods = function (conf, prompts) {
|
152 | if ((typeof conf.projectName as string | undefined) !== 'string') {
|
153 | prompts.push({
|
154 | type: 'input',
|
155 | name: 'projectName',
|
156 | message: '请输入项目名称!',
|
157 | validate (input) {
|
158 | if (!input) {
|
159 | return '项目名不能为空!'
|
160 | }
|
161 | if (fs.existsSync(input)) {
|
162 | return '当前目录已经存在同名项目,请换一个项目名!'
|
163 | }
|
164 | return true
|
165 | }
|
166 | })
|
167 | } else if (fs.existsSync(conf.projectName)) {
|
168 | prompts.push({
|
169 | type: 'input',
|
170 | name: 'projectName',
|
171 | message: '当前目录已经存在同名项目,请换一个项目名!',
|
172 | validate (input) {
|
173 | if (!input) {
|
174 | return '项目名不能为空!'
|
175 | }
|
176 | if (fs.existsSync(input)) {
|
177 | return '项目名依然重复!'
|
178 | }
|
179 | return true
|
180 | }
|
181 | })
|
182 | }
|
183 | }
|
184 |
|
185 | askDescription: AskMethods = function (conf, prompts) {
|
186 | if (typeof conf.description !== 'string') {
|
187 | prompts.push({
|
188 | type: 'input',
|
189 | name: 'description',
|
190 | message: '请输入项目介绍!'
|
191 | })
|
192 | }
|
193 | }
|
194 |
|
195 | askTypescript: AskMethods = function (conf, prompts) {
|
196 | if (typeof conf.typescript !== 'boolean') {
|
197 | prompts.push({
|
198 | type: 'confirm',
|
199 | name: 'typescript',
|
200 | message: '是否需要使用 TypeScript ?'
|
201 | })
|
202 | }
|
203 | }
|
204 |
|
205 | askCSS: AskMethods = function (conf, prompts) {
|
206 | const cssChoices = [
|
207 | {
|
208 | name: 'Sass',
|
209 | value: 'sass'
|
210 | },
|
211 | {
|
212 | name: 'Less',
|
213 | value: 'less'
|
214 | },
|
215 | {
|
216 | name: 'Stylus',
|
217 | value: 'stylus'
|
218 | },
|
219 | {
|
220 | name: '无',
|
221 | value: 'none'
|
222 | }
|
223 | ]
|
224 |
|
225 | if ((typeof conf.css as string | undefined) !== 'string') {
|
226 | prompts.push({
|
227 | type: 'list',
|
228 | name: 'css',
|
229 | message: '请选择 CSS 预处理器(Sass/Less/Stylus)',
|
230 | choices: cssChoices
|
231 | })
|
232 | }
|
233 | }
|
234 |
|
235 | askFramework: AskMethods = function (conf, prompts) {
|
236 | const frameworks = [
|
237 | {
|
238 | name: 'React',
|
239 | value: 'react'
|
240 | },
|
241 | {
|
242 | name: 'Nerv',
|
243 | value: 'nerv'
|
244 | },
|
245 | {
|
246 | name: 'Vue',
|
247 | value: 'vue'
|
248 | },
|
249 | {
|
250 | name: 'Vue3',
|
251 | value: 'vue3'
|
252 | }
|
253 | ]
|
254 |
|
255 | if ((typeof conf.framework as string | undefined) !== 'string') {
|
256 | prompts.push({
|
257 | type: 'list',
|
258 | name: 'framework',
|
259 | message: '请选择框架',
|
260 | choices: frameworks
|
261 | })
|
262 | }
|
263 | }
|
264 |
|
265 | askTemplate: AskMethods = function (conf, prompts, list = []) {
|
266 | const choices = [
|
267 | {
|
268 | name: '默认模板',
|
269 | value: 'default'
|
270 | },
|
271 | ...list.map(item => ({
|
272 | name: item.desc ? `${item.name}(${item.desc})` : item.name,
|
273 | value: item.name
|
274 | }))
|
275 | ]
|
276 |
|
277 | if ((typeof conf.template as 'string' | undefined) !== 'string') {
|
278 | prompts.push({
|
279 | type: 'list',
|
280 | name: 'template',
|
281 | message: '请选择模板',
|
282 | choices
|
283 | })
|
284 | }
|
285 | }
|
286 |
|
287 | write (cb?: () => void) {
|
288 | this.conf.src = SOURCE_DIR
|
289 | createApp(this, this.conf, cb).catch(err => console.log(err))
|
290 | }
|
291 | }
|