import merge from 'lodash.merge' ; import get from 'lodash.get' ; import fs from 'fs-extra' ; import Metalsmith from 'metalsmith' ; import inquirer from 'inquirer' ; import match from 'minimatch' ; import consolidate from 'consolidate' ; import Handlebars from 'handlebars' ; import multimatch from 'multimatch' ; import simpleGit from 'simple-git' ; import { log as leoLog } from '@jdfed/leo-utils' ; import { IFiles , IMetaData , IFilterFilesMap , IHelper } from './interface' ; const log = leoLog . scope ( 'generator' ) ; const downloadGitRepo = require ( 'download-git-repo' ) ; type metalsmithCB = ( files : IFiles , metalsmith : Metalsmith , done : Function ) => void ; class Generator { cachePath : string ; localTemplatePath : string ; repoRemoteAddress : string ; currentTemplatePath : string ; currentMetaPath : string ; templateName : string ; projectName : string ; projectPath : string ; answer : Record < string , any > ; officialEnv : Record < string , any > ; metalsmith : Metalsmith ; defaultCompileFiles : string [ ] ; leoRC : { [ key ] : any ; } ; leoConfig : { [ key ] : any ; } ; metaConfig : { [ key ] : any ; } ; hooks : { beforeGenerate : ( generator : Generator ) => Promise < boolean > ; afterGenerate : ( generator : Generator ) => Promise < void > ; /**
         * 参考项目https://github.com/segmentio/metalsmith
         * 以及@types/metalsmith
         * 渲染占位符时的hook，如果实现讲自己接管渲染占位符
         */ renderTemplatePlaceholder : ( generator : Generator ) => ( files : IFiles , metalsmith : Metalsmith , done : Function ) => void ; /**
         * 最终渲染文件的hook，理论上可以做任何事包括渲染占位符
         */ renderTemplateFile : ( generator : Generator ) => ( files : IFiles , metalsmith : Metalsmith , done : Function ) => void ; } = { beforeGenerate : null , afterGenerate : null , renderTemplatePlaceholder : null , renderTemplateFile : null , } ; options : { useCache : false ; repo : '' ; } ; constructor ( params ) { this . templateName = params . templateName ; this . projectName = params . projectName ; this . cachePath = params . cachePath ; this . leoConfig = params . leoConfig ; this . repoRemoteAddress = `${ this . leoConfig . gitTemplateGroupURL }/${ this . templateName }-template.git` ; this . localTemplatePath = `${ this . cachePath }/${ this . templateName }-template` ; this . currentTemplatePath = `${ this . localTemplatePath }/template` ; this . currentMetaPath = `${ this . localTemplatePath }/meta.js` ; this . defaultCompileFiles = [ 'package.json' , this . leoConfig . rcFileName ] ; this . answer = { } ; this . officialEnv = { } ; this . metalsmith = null ; this . options = merge ( { } , this . options , params . options ) ; } async start ( ) { try { await this . prepare ( ) ; this . answer = await this . askQuestions ( get ( this . metaConfig , 'prompts' , [ ] ) ) ; this . officialEnv = await this . getGeneratorEnv ( ) ; if ( this . hooks . beforeGenerate && ( await this . hooks . beforeGenerate ( this ) ) === true ) { return ; } await this . generate ( ) ; await this . initGitRepo ( this . options . repo ) ; this . hooks . afterGenerate && ( await this . hooks . afterGenerate ( this ) ) ; } catch ( e ) { log . error ( 'generator' , ( e as Error ) . message ) ; process . exit ( 1 ) ; } } async getGeneratorEnv ( ) { return { $projectName : this . projectName , } ; } async prepare ( ) { const isUseCache = await this . useLocalCacheTemplate ( ) ; if ( ! isUseCache ) { await this . downloadTemplate ( ) ; } const hasCurTemplateFolder = await fs . pathExists ( this . currentTemplatePath ) ; if ( ! hasCurTemplateFolder ) { throw new Error ( '模版下不存在template目录' ) ; } const projectPath = `${ process . cwd ( ) }/${ this . projectName }` ; const currentHasProject = await fs . pathExists ( projectPath ) ; if ( this . leoConfig . isDebugger ) { log . debug ( 'generator.start' , 'projectPath:' , projectPath ) ; } if ( currentHasProject ) { throw new Error ( '当前目录下已存在相同目录名' ) ; } this . projectPath = projectPath ; this . metaConfig = await this . getTemplateMetaConfig ( ) ; merge ( this . hooks , get ( this . metaConfig , 'hooks' , { } ) ) ; } async generate ( ) { this . metalsmith = Metalsmith ( this . currentTemplatePath ) ; const customHelpers = get ( this . metaConfig , 'helpers' , { } ) ; if ( Object . keys ( customHelpers ) . length > 0 ) { this . registerCustomHelper ( customHelpers ) ; } Object . assign ( this . metalsmith . metadata ( ) , this . officialEnv , this . answer , customHelpers ) ; const filterFilesMap = get ( this . metaConfig , 'filterFilesMap' , null ) ; if ( filterFilesMap ) { this . metalsmith = this . metalsmith . use ( this . filterFiles ( filterFilesMap ) ) ; } const compileWhiteList = get ( this . metaConfig , 'compileWhiteList' , null ) ; let renderTemplatePlaceholder = this . renderTemplatePlaceholder ( compileWhiteList ) ; if ( this . hooks . renderTemplatePlaceholder ) { renderTemplatePlaceholder = this . hooks . renderTemplatePlaceholder ( this ) ; } this . metalsmith = this . metalsmith . use ( renderTemplatePlaceholder ) ; if ( this . hooks . renderTemplateFile ) { this . metalsmith = this . metalsmith . use ( this . hooks . renderTemplateFile ( this ) ) ; } return new Promise ( ( resolve , reject ) => { this . metalsmith . source ( '.' ) . destination ( this . projectPath ) . clean ( false ) . build ( async ( err ) => { if ( err ) { reject ( err ) ; return ; } log . success ( 'Generator构建完成' ) ; resolve ( 0 ) ; } ) ; } ) ; } params return { Promise } async askQuestions ( prompts ) { return inquirer . prompt ( prompts ) ; } params return { metalsmithCB } filterFiles ( filterFilesMap ) { return ( files , metalsmith , done ) => { if ( ! filterFilesMap ) { return done ( ) ; } const metaData = metalsmith . metadata ( ) ; const fileNameList = Object . keys ( files ) ; const filtersList = Object . keys ( filterFilesMap ) ; if ( this . leoConfig . isDebugger ) { log . debug ( 'generator.filterFiles.before' , Object . keys ( files ) ) ; } filtersList . forEach ( ( filter ) => { fileNameList . forEach ( ( filename ) => { if ( match ( filename , filter , { dot : true } ) ) { const conditionKey = filterFilesMap [ filter ] ; if ( ! metaData [ conditionKey ] ) { delete files [ filename ] ; } } } ) ; } ) ; if ( this . leoConfig . isDebugger ) { log . debug ( 'generator.filterFiles.after' , Object . keys ( files ) ) ; } done ( ) ; } ; } return { metalsmithCB } renderTemplatePlaceholder ( compileWhiteList ) { return ( files , metalsmith , done ) => { const keys = Object . keys ( files ) ; const metalsmithMetadata = metalsmith . metadata ( ) ; const hasWhiteList = this . existCompileWhiteList ( compileWhiteList ) ; keys . forEach ( ( fileName ) => { const str = files [ fileName ] . contents . toString ( ) ; const shouldCompileFile = this . isDefaultCompileFile ( fileName ) || this . matchCompileFile ( hasWhiteList , fileName , compileWhiteList ) ; if ( shouldCompileFile && /{{([^{}]+)}}/g . test ( str ) ) { consolidate . handlebars . render ( str , metalsmithMetadata , ( err , res ) => { if ( err ) { throw new Error ( `模版文件${ fileName }渲染出现异常` ) ; } files [ fileName ] . contents = Buffer . from ( res ) ; } ) ; } } ) ; done ( ) ; } ; } params return { Promise < any > } async initGitRepo ( repo ) { if ( ! repo ) { return ; } const git = simpleGit ( this . projectPath ) ; log . await ( `正在关联远程仓库：${ repo }` ) ; await git . init ( ) ; await git . addRemote ( 'origin' , repo ) ; await git . add ( [ '.' ] ) ; await git . commit ( `leo init from ${ this . templateName }-template` ) ; await git . push ( 'origin' , 'master' , [ '--set-upstream' ] ) ; log . success ( '关联成功' ) ; } params return { Promise < boolean > } private async useLocalCacheTemplate ( ) { if ( ! this . options . useCache ) { return false ; } const templatePath = `${ this . localTemplatePath }` ; const hasTemplatePath = await fs . pathExists ( templatePath ) ; if ( ! hasTemplatePath ) { throw new Error ( `${ templatePath } 不存在，无法使用缓存模版` ) ; } else { return true ; } } return { Promise } private async downloadTemplate ( ) { log . await ( 'generator' , `下载远程模板: ${ this . templateName }-template` ) ; const gitPath = this . repoRemoteAddress ; const target = this . localTemplatePath ; if ( this . leoConfig . isDebugger ) { log . debug ( 'generator.downloadTemplate' , `gitPath:${ gitPath } target:${ target }` ) ; } await fs . remove ( target ) ; await fs . ensureDir ( target ) ; return new Promise ( ( resolve , reject ) => { downloadGitRepo ( `direct:${ gitPath }` , target , { clone : true , headers : this . leoConfig . gitTemplateLikeQueryHeader || { } } , ( err ) => { if ( err ) { return reject ( new Error ( `下载模板错误：${ err }` ) ) ; } log . success ( 'generator' , '模板下载成功' ) ; return resolve ( ) ; } ) ; } ) ; } return private async getTemplateMetaConfig ( ) { const templateMetaPath = `${ this . localTemplatePath }/meta.js` ; const templatePackagePath = `${ this . localTemplatePath }/package.json` ; const hasMeta = await fs . pathExists ( templateMetaPath ) ; const hasPackage = await fs . pathExists ( templatePackagePath ) ; if ( hasPackage ) { log . await ( '====安装template的依赖====' ) ; await this . installTemplatePkg ( ) ; } if ( hasMeta ) { const metaConfig = require ( templateMetaPath ) ; if ( this . leoConfig . isDebugger ) { log . debug ( 'generator.getTemplateMetaConfig' , JSON . stringify ( metaConfig ) ) ; } return metaConfig ; } return null ; } return { boolean } private existCompileWhiteList ( compileWhiteList ) { return ! ! compileWhiteList && Array . isArray ( compileWhiteList ) && compileWhiteList . length > 0 ; } return { boolean } private matchCompileFile ( hasWhiteList , fileName , compileWhiteList ) { return ( ! hasWhiteList || ( hasWhiteList && multimatch ( [ fileName ] , compileWhiteList , { dot : true } ) . length > 0 ) ) ; } return { boolean } private isDefaultCompileFile ( fileName ) { return this . defaultCompileFiles . indexOf ( fileName ) >= 0 ; } return { Promise < void > } private async installTemplatePkg ( ) { const npmi = require ( 'npminstall' ) ; try { await npmi ( { production : true , root : this . localTemplatePath , registry : 'http://registry.m.jd.com/' , } ) ; } catch ( e ) { log . warn ( 'template依赖包安装存在问题' , e ) ; } log . success ( 'installTemplatePkg' , '安装成功' ) ; } return { void } private registerCustomHelper ( helpers ) { Object . keys ( helpers ) . forEach ( ( key ) => { Handlebars . registerHelper ( key , helpers [ key ] ) ; } ) ; } } export default Generator ;