@tarojs/cli
Version:
cli tool for taro
300 lines (265 loc) • 10 kB
text/typescript
import * as path from 'node:path'
import { CompilerType, createPage as createPageBinding, CSSType, FrameworkType, NpmType, PeriodType } from '@tarojs/binding'
import { babelKit, chalk, DEFAULT_TEMPLATE_SRC, fs, getUserHomeDir, resolveScriptPath, TARO_BASE_CONFIG, TARO_CONFIG_FOLDER } from '@tarojs/helper'
import { getPkgVersion, getRootPath, isNil } from '../util'
import { modifyPagesOrSubPackages } from '../util/createPage'
import { TEMPLATE_CREATOR } from './constants'
import Creator from './creator'
import fetchTemplate from './fetchTemplate'
export interface IPageConf {
projectDir: string
projectName: string
npm: NpmType
template: string
clone?: boolean
templateSource?: string
description?: string
pageName: string
date?: string
framework: FrameworkType
css: CSSType
typescript?: boolean
compiler?: CompilerType
isCustomTemplate?: boolean
customTemplatePath?: string
pageDir?: string
subPkg?: string
}
interface IPageArgs extends IPageConf {
modifyCustomTemplateConfig : TGetCustomTemplate
afterCreate?: TAfterCreate
}
interface ITemplateInfo {
css: CSSType
typescript?: boolean
compiler?: CompilerType
template?: string
templateSource?: string
clone?: boolean
}
type TCustomTemplateInfo = Omit<ITemplateInfo & {
isCustomTemplate?: boolean
customTemplatePath?: string
}, 'template'>
export type TSetCustomTemplateConfig = (customTemplateConfig: TCustomTemplateInfo) => void
type TGetCustomTemplate = (cb: TSetCustomTemplateConfig) => Promise<void>
type TAfterCreate = (state: boolean) => void
const DEFAULT_TEMPLATE_INFO = {
name: 'default',
css: CSSType.None,
typescript: false,
compiler: CompilerType.Webpack5,
framework: FrameworkType.React
}
export enum ConfigModificationState {
Success,
Fail,
NeedLess
}
export type ModifyCallback = (state: ConfigModificationState) => void
export default class Page extends Creator {
public rootPath: string
public conf: IPageConf
private modifyCustomTemplateConfig: TGetCustomTemplate
private afterCreate: TAfterCreate | undefined
private pageEntryPath: string
constructor (args: IPageArgs) {
super()
this.rootPath = this._rootPath
const { modifyCustomTemplateConfig, afterCreate, ...otherOptions } = args
this.conf = Object.assign(
{
projectDir: '',
projectName: '',
template: '',
description: '',
pageDir: ''
},
otherOptions
)
this.conf.projectName = path.basename(this.conf.projectDir)
this.modifyCustomTemplateConfig = modifyCustomTemplateConfig
this.afterCreate = afterCreate
this.processPageName()
}
processPageName () {
const { pageName } = this.conf
// todo 目前还没有对 subPkg 和 pageName 这两个字段做 格式验证或者处理
const lastDirSplitSymbolIndex = pageName.lastIndexOf('/')
if (lastDirSplitSymbolIndex !== -1) {
this.conf.pageDir = pageName.substring(0, lastDirSplitSymbolIndex)
this.conf.pageName = pageName.substring(lastDirSplitSymbolIndex + 1)
}
}
getPkgPath () {
const projectDir = this.conf.projectDir as string
let pkgPath = path.join(projectDir, 'package.json')
if (!fs.existsSync(pkgPath)) {
// 适配 云开发 项目
pkgPath = path.join(projectDir, 'client', 'package.json')
if (!fs.existsSync(pkgPath)) {
console.log(chalk.yellow('请在项目根目录下执行 taro create 命令!'))
process.exit(0)
}
}
return pkgPath
}
getPkgTemplateInfo () {
const pkg = fs.readJSONSync(this.getPkgPath())
const templateInfo = pkg.templateInfo || DEFAULT_TEMPLATE_INFO
// set template name
templateInfo.template = templateInfo.name
delete templateInfo.name
return templateInfo
}
setPageEntryPath (files: string[], handler) {
const configFileName = files.find((filename) => /\.config\.(js|ts)$/.test(filename))
if (!configFileName) return
const getPageFn = handler[configFileName]
const { setPageName = '', setSubPkgName = '' } = getPageFn?.(() => {}, this.conf) || {}
if (this.conf.subPkg) {
this.pageEntryPath = setSubPkgName.replace(/\.config\.(js|ts)$/, '')
} else {
this.pageEntryPath = setPageName.replace(/\.config\.(js|ts)$/, '')
}
}
setCustomTemplateConfig (customTemplateConfig: TCustomTemplateInfo) {
const pkgTemplateInfo = this.getPkgTemplateInfo()
const { compiler, css, customTemplatePath, typescript } = customTemplateConfig
const conf = {
compiler: compiler || pkgTemplateInfo.compiler,
css: css || pkgTemplateInfo.css,
typescript: !isNil(typescript) ? typescript : pkgTemplateInfo.typescript,
customTemplatePath,
isCustomTemplate: true,
}
this.setTemplateConfig(conf)
}
setTemplateConfig (templateInfo: ITemplateInfo) {
this.conf = Object.assign(this.conf, templateInfo)
}
async fetchTemplates () {
const homedir = getUserHomeDir()
let templateSource = DEFAULT_TEMPLATE_SRC
if (!homedir) chalk.yellow('找不到用户根目录,使用默认模版源!')
if (this.conf.templateSource) {
templateSource = this.conf.templateSource
} else {
const taroConfigPath = path.join(homedir, TARO_CONFIG_FOLDER)
const taroConfig = path.join(taroConfigPath, TARO_BASE_CONFIG)
if (fs.existsSync(taroConfig)) {
const config = await fs.readJSON(taroConfig)
templateSource = config && config.templateSource ? config.templateSource : DEFAULT_TEMPLATE_SRC
} else {
await fs.createFile(taroConfig)
await fs.writeJSON(taroConfig, { templateSource })
templateSource = DEFAULT_TEMPLATE_SRC
}
}
// 从模板源下载模板
await fetchTemplate(templateSource, this.templatePath(''), this.conf.clone)
}
async create () {
const date = new Date()
this.conf.date = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
// apply 插件,由插件设置自定义模版 config
await this.modifyCustomTemplateConfig(this.setCustomTemplateConfig.bind(this))
if (!this.conf.isCustomTemplate) {
const pkgTemplateInfo = this.getPkgTemplateInfo()
this.setTemplateConfig(pkgTemplateInfo)
if (!fs.existsSync(this.templatePath(this.conf.template))) {
await this.fetchTemplates()
}
}
this.write()
}
updateAppConfig () {
const { parse, generate, traverse } = babelKit
let modifyState: ConfigModificationState = ConfigModificationState.Fail
const { subPkg, projectDir, typescript } = this.conf
const [sourceString, pageString] = this.pageEntryPath.split('/src/')
const appConfigPath = resolveScriptPath(path.join(projectDir, sourceString, 'src', 'app.config'))
if (!fs.existsSync(appConfigPath)) {
return console.log(
`${chalk.red('x ')}${chalk.grey(`无法获取 ${appConfigPath} 配置文件,请手动到配置文件中补全新页面信息`)}`
)
}
const configFileContent = fs.readFileSync(appConfigPath, 'utf-8')
const ast = parse(configFileContent, {
sourceType: 'module',
plugins: typescript ? ['typescript'] : []
})
const callback = (state: ConfigModificationState) => {
modifyState = state
}
traverse(ast, {
ExportDefaultDeclaration (path) {
modifyPagesOrSubPackages({
path,
fullPagePath: pageString,
subPkgRootPath: subPkg,
callback
})
},
})
switch (modifyState as ConfigModificationState) {
case ConfigModificationState.Fail:
console.log(`${chalk.red('x ')}${chalk.grey(`自动补全新页面信息失败, 请手动到 ${appConfigPath} 文件中补全新页面信息`)}`)
break
case ConfigModificationState.Success:
{
const newCode = generate(ast, { retainLines: true })
fs.writeFileSync(appConfigPath, newCode.code)
console.log(`${chalk.green('✔ ')}${chalk.grey(`新页面信息已在 ${appConfigPath} 文件中自动补全`)}`)
break
}
case ConfigModificationState.NeedLess:
console.log(`${chalk.green('✔ ')}${chalk.grey(`新页面信息已存在在 ${appConfigPath} 文件中,不需要补全`)}`)
break
}
}
write () {
const { projectName, projectDir, template, pageName, isCustomTemplate, customTemplatePath, subPkg, pageDir } = this.conf as IPageConf
let templatePath
if (isCustomTemplate) {
templatePath = customTemplatePath
} else {
templatePath = this.templatePath(template)
}
if (!fs.existsSync(templatePath)) return console.log(chalk.red(`创建页面错误:找不到模板${templatePath}`))
// 引入模板编写者的自定义逻辑
const handlerPath = path.join(templatePath, TEMPLATE_CREATOR)
const basePageFiles = fs.existsSync(handlerPath) ? require(handlerPath).basePageFiles : []
const files = Array.isArray(basePageFiles) ? basePageFiles : []
const handler = fs.existsSync(handlerPath) ? require(handlerPath).handler : {}
this.setPageEntryPath(files, handler)
createPageBinding({
pageDir,
subPkg,
projectDir,
projectName,
template,
framework: this.conf.framework,
css: this.conf.css || CSSType.None,
typescript: this.conf.typescript,
compiler: this.conf.compiler,
templateRoot: getRootPath(),
version: getPkgVersion(),
date: this.conf.date,
description: this.conf.description,
pageName,
isCustomTemplate,
customTemplatePath,
basePageFiles: files,
period: PeriodType.CreatePage,
}, handler).then(() => {
console.log(`${chalk.green('✔ ')}${chalk.grey(`创建页面 ${this.conf.pageName} 成功!`)}`)
this.updateAppConfig()
this.afterCreate && this.afterCreate(true)
}).catch(err => {
console.log(err)
this.afterCreate && this.afterCreate(false)
})
}
}
export type { Page as PageCreator }