@tarojs/cli
Version:
cli tool for taro
509 lines (458 loc) • 14 kB
text/typescript
import * as path from 'node:path'
import { CompilerType, createProject, CSSType, FrameworkType, NpmType, PeriodType } from '@tarojs/binding'
import {
chalk,
DEFAULT_TEMPLATE_SRC,
DEFAULT_TEMPLATE_SRC_GITEE,
fs,
getUserHomeDir,
SOURCE_DIR,
TARO_BASE_CONFIG,
TARO_CONFIG_FOLDER
} from '@tarojs/helper'
import { isArray } from '@tarojs/shared'
import axios from 'axios'
import * as inquirer from 'inquirer'
import * as ora from 'ora'
import * as semver from 'semver'
import { clearConsole, getPkgVersion, getRootPath } from '../util'
import { TEMPLATE_CREATOR } from './constants'
import Creator from './creator'
import fetchTemplate from './fetchTemplate'
import type { ITemplates } from './fetchTemplate'
export interface IProjectConf {
projectName: string
projectDir: string
npm: NpmType
templateSource: string
clone?: boolean
template: string
description?: string
typescript?: boolean
buildEs5?: boolean
css: CSSType
date?: string
src?: string
sourceRoot?: string
env?: string
autoInstall?: boolean
hideDefaultTemplate?: boolean
framework: FrameworkType
compiler?: CompilerType
}
type CustomPartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type IProjectConfOptions = CustomPartial<IProjectConf, 'projectName' | 'projectDir' | 'template' | 'css' | 'npm' | 'framework' | 'templateSource'>
interface AskMethods {
(conf: IProjectConfOptions, prompts: Record<string, unknown>[], choices?: ITemplates[]): void
}
const NONE_AVAILABLE_TEMPLATE = '无可用模板'
export default class Project extends Creator {
public rootPath: string
public conf: IProjectConfOptions
constructor (options: IProjectConfOptions) {
super(options.sourceRoot)
const unSupportedVer = semver.lt(process.version, 'v18.0.0')
if (unSupportedVer) {
throw new Error('Node.js 版本过低,推荐升级 Node.js 至 v18.0.0+')
}
this.rootPath = this._rootPath
this.conf = Object.assign(
{
projectName: '',
projectDir: '',
template: '',
description: '',
npm: ''
},
options
)
}
init () {
clearConsole()
console.log(chalk.green('Taro 即将创建一个新项目!'))
console.log(`Need help? Go and open issue: ${chalk.blueBright('https://tls.jd.com/taro-issue-helper')}`)
console.log()
}
async create () {
try {
const answers = await this.ask()
const date = new Date()
this.conf = Object.assign(this.conf, answers)
this.conf.date = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
this.write()
} catch (error) {
console.log(chalk.red('创建项目失败: ', error))
}
}
async ask () {
let prompts: Record<string, unknown>[] = []
const conf = this.conf
this.askProjectName(conf, prompts)
this.askDescription(conf, prompts)
this.askFramework(conf, prompts)
this.askTypescript(conf, prompts)
this.askBuildEs5(conf, prompts)
this.askCSS(conf, prompts)
this.askNpm(conf, prompts)
const answers = await inquirer.prompt<IProjectConf>(prompts)
// Note: 由于 Solid 框架适配 Vite 还存在某些问题,所以在选择 Solid 框架时,不再询问编译工具
prompts = []
if (answers.framework === FrameworkType.Solid || conf.framework === FrameworkType.Solid) {
answers.compiler = CompilerType.Webpack5
} else {
this.askCompiler(conf, prompts)
}
await this.askTemplateSource(conf, prompts)
const compilerAndTemplateSourceAnswer = await inquirer.prompt<IProjectConf>(prompts)
prompts = []
const templates = await this.fetchTemplates(Object.assign({}, answers, compilerAndTemplateSourceAnswer))
await this.askTemplate(conf, prompts, templates)
const templateChoiceAnswer = await inquirer.prompt<IProjectConf>(prompts)
return {
...answers,
...compilerAndTemplateSourceAnswer,
...templateChoiceAnswer
}
}
askProjectName: AskMethods = function (conf, prompts) {
if ((typeof conf.projectName) !== 'string') {
prompts.push({
type: 'input',
name: 'projectName',
message: '请输入项目名称!',
validate (input) {
if (!input) {
return '项目名不能为空!'
}
if (fs.existsSync(input)) {
return '当前目录已经存在同名项目,请换一个项目名!'
}
return true
}
})
} else if (fs.existsSync(conf.projectName!)) {
prompts.push({
type: 'input',
name: 'projectName',
message: '当前目录已经存在同名项目,请换一个项目名!',
validate (input) {
if (!input) {
return '项目名不能为空!'
}
if (fs.existsSync(input)) {
return '项目名依然重复!'
}
return true
}
})
}
}
askDescription: AskMethods = function (conf, prompts) {
if (typeof conf.description !== 'string') {
prompts.push({
type: 'input',
name: 'description',
message: '请输入项目介绍'
})
}
}
askTypescript: AskMethods = function (conf, prompts) {
if (typeof conf.typescript !== 'boolean') {
prompts.push({
type: 'confirm',
name: 'typescript',
message: '是否需要使用 TypeScript ?'
})
}
}
askBuildEs5: AskMethods = function (conf, prompts) {
if (typeof conf.buildEs5 !== 'boolean') {
prompts.push({
type: 'confirm',
name: 'buildEs5',
message: '是否需要编译为 ES5 ?',
default: false
})
}
}
askCSS: AskMethods = function (conf, prompts) {
const cssChoices = [
{
name: 'Sass',
value: CSSType.Sass
},
{
name: 'Less',
value: CSSType.Less
},
{
name: 'Stylus',
value: CSSType.Stylus
},
{
name: '无',
value: CSSType.None
}
]
if (typeof conf.css !== 'string') {
prompts.push({
type: 'list',
name: 'css',
message: '请选择 CSS 预处理器(Sass/Less/Stylus)',
choices: cssChoices
})
}
}
askCompiler: AskMethods = function (conf, prompts) {
const compilerChoices = [
{
name: 'Webpack5',
value: CompilerType.Webpack5
},
{
name: 'Vite',
value: CompilerType.Vite
}
]
if (typeof conf.compiler !== 'string') {
prompts.push({
type: 'list',
name: 'compiler',
message: '请选择编译工具',
choices: compilerChoices
})
}
}
askFramework: AskMethods = function (conf, prompts) {
const frameworks = [
{
name: 'React',
value: FrameworkType.React
},
{
name: 'PReact',
value: FrameworkType.Preact
},
{
name: 'Vue3',
value: FrameworkType.Vue3
},
{
name: 'Solid',
value: FrameworkType.Solid
}
]
if (typeof conf.framework !== 'string') {
prompts.push({
type: 'list',
name: 'framework',
message: '请选择框架',
choices: frameworks
})
}
}
askTemplateSource: AskMethods = async function (conf, prompts) {
if (conf.template === 'default' || conf.templateSource) return
const homedir = getUserHomeDir()
const taroConfigPath = path.join(homedir, TARO_CONFIG_FOLDER)
const taroConfig = path.join(taroConfigPath, TARO_BASE_CONFIG)
let localTemplateSource: string
// 检查本地配置
if (fs.existsSync(taroConfig)) {
// 存在则把模板源读出来
const config = await fs.readJSON(taroConfig)
localTemplateSource = config?.templateSource
} else {
// 不存在则创建配置
await fs.createFile(taroConfig)
await fs.writeJSON(taroConfig, { templateSource: DEFAULT_TEMPLATE_SRC })
localTemplateSource = DEFAULT_TEMPLATE_SRC
}
const choices = [
{
name: 'Gitee(最快)',
value: DEFAULT_TEMPLATE_SRC_GITEE
},
{
name: 'Github(最新)',
value: DEFAULT_TEMPLATE_SRC
},
{
name: 'CLI 内置默认模板',
value: 'default-template'
},
{
name: '自定义',
value: 'self-input'
},
{
name: '社区优质模板源',
value: 'open-source'
}
]
if (localTemplateSource && localTemplateSource !== DEFAULT_TEMPLATE_SRC && localTemplateSource !== DEFAULT_TEMPLATE_SRC_GITEE) {
choices.unshift({
name: `本地模板源:${localTemplateSource}`,
value: localTemplateSource
})
}
prompts.push({
type: 'list',
name: 'templateSource',
message: '请选择模板源',
choices
}, {
type: 'input',
name: 'templateSource',
message: '请输入模板源!',
askAnswered: true,
when (answers) {
return answers.templateSource === 'self-input'
}
}, {
type: 'list',
name: 'templateSource',
message: '请选择社区模板源',
async choices (answers) {
const choices = await getOpenSourceTemplates(answers.framework)
return choices
},
askAnswered: true,
when (answers) {
return answers.templateSource === 'open-source'
}
})
}
askTemplate: AskMethods = function (conf, prompts, list = []) {
const choices = list.map(item => ({
name: item.desc ? `${item.name}(${item.desc})` : item.name,
value: item.value || item.name
}))
if (!conf.hideDefaultTemplate) {
choices.unshift({
name: '默认模板',
value: 'default'
})
}
if ((typeof conf.template as 'string' | undefined) !== 'string') {
prompts.push({
type: 'list',
name: 'template',
message: '请选择模板',
choices
})
}
}
askNpm: AskMethods = function (conf, prompts) {
const packages = [
{
name: 'yarn',
value: NpmType.Yarn
},
{
name: 'pnpm',
value: NpmType.Pnpm
},
{
name: 'npm',
value: NpmType.Npm
},
{
name: 'cnpm',
value: NpmType.Cnpm
}
]
if ((typeof conf.npm as string | undefined) !== 'string') {
prompts.push({
type: 'list',
name: 'npm',
message: '请选择包管理工具',
choices: packages
})
}
}
async fetchTemplates (answers: IProjectConf): Promise<ITemplates[]> {
const { templateSource, framework, compiler } = answers
this.conf.framework = this.conf.framework || framework || ''
this.conf.templateSource = this.conf.templateSource || templateSource
// 使用默认模版
if (answers.templateSource === 'default-template') {
this.conf.template = 'default'
answers.templateSource = DEFAULT_TEMPLATE_SRC_GITEE
}
if (this.conf.template === 'default' || answers.templateSource === NONE_AVAILABLE_TEMPLATE) return Promise.resolve([])
// 从模板源下载模板
const isClone = /gitee/.test(this.conf.templateSource) || this.conf.clone
const templateChoices = await fetchTemplate(this.conf.templateSource, this.templatePath(''), isClone)
const filterFramework = (_framework) => {
const current = this.conf.framework?.toLowerCase()
if (typeof _framework === 'string' && _framework) {
return current === _framework.toLowerCase()
} else if (isArray(_framework)) {
return _framework?.map(name => name.toLowerCase()).includes(current)
} else {
return true
}
}
const filterCompiler = (_compiler) => {
if (_compiler && isArray(_compiler)) {
return _compiler?.includes(compiler)
}
return true
}
// 根据用户选择的框架筛选模板
const newTemplateChoices: ITemplates[] = templateChoices
.filter(templateChoice => {
const { platforms, compiler } = templateChoice
return filterFramework(platforms) && filterCompiler(compiler)
})
return newTemplateChoices
}
write (cb?: () => void) {
this.conf.src = SOURCE_DIR
const { projectName, projectDir, template, autoInstall = true, framework, npm } = this.conf as IProjectConf
// 引入模板编写者的自定义逻辑
const templatePath = this.templatePath(template)
const handlerPath = path.join(templatePath, TEMPLATE_CREATOR)
const handler = fs.existsSync(handlerPath) ? require(handlerPath).handler : {}
createProject({
projectRoot: projectDir,
projectName,
template,
npm,
framework,
css: this.conf.css || CSSType.None,
autoInstall: autoInstall,
templateRoot: getRootPath(),
version: getPkgVersion(),
typescript: this.conf.typescript,
buildEs5: this.conf.buildEs5,
date: this.conf.date,
description: this.conf.description,
compiler: this.conf.compiler,
period: PeriodType.CreateAPP,
}, handler).then(() => {
cb && cb()
})
}
}
function getOpenSourceTemplates (platform: string) {
return new Promise((resolve, reject) => {
const spinner = ora({ text: '正在拉取开源模板列表...', discardStdin: false }).start()
axios.get('https://gitee.com/NervJS/awesome-taro/raw/next/index.json')
.then(response => {
spinner.succeed(`${chalk.grey('拉取开源模板列表成功!')}`)
const collection = response.data
switch (platform.toLowerCase()) {
case 'react':
return resolve(collection.react)
default:
return resolve([NONE_AVAILABLE_TEMPLATE])
}
})
.catch(_error => {
spinner.fail(chalk.red('拉取开源模板列表失败!'))
return reject(new Error())
})
})
}