UNPKG

@pgsz/pg-cli

Version:

pg-cli脚手架的自我学习

353 lines (323 loc) 12.4 kB
const fs = require('fs-extra') const path = require('path') const ejs = require('ejs') const sortObject = require('./utils/sortObject') const normalizeFilePaths = require('./utils/normalizeFilePaths') const { runTransformation } = require('vue-codemod') const writeFileTree = require('./utils/writeFileTree') const { isBinaryFileSync } = require('isbinaryfile') const isObject = (val) => val && typeof val === 'object' const ConfigTransform = require('./ConfigTransform') const defaultConfigTransforms = { babel: new ConfigTransform({ file: { js: ['babel.config.js'], }, }), postcss: new ConfigTransform({ file: { js: ['postcss.config.js'], json: ['.postcssrc.json', '.postcssrc'], yaml: ['.postcssrc.yaml', '.postcssrc.yml'], }, }), eslintConfig: new ConfigTransform({ file: { js: ['.eslintrc.js'], json: ['.eslintrc', '.eslintrc.json'], yaml: ['.eslintrc.yaml', '.eslintrc.yml'], }, }), jest: new ConfigTransform({ file: { js: ['jest.config.js'], }, }), browserslist: new ConfigTransform({ file: { lines: ['.browserslistrc'], }, }), } const reservedConfigTransforms = { vue: new ConfigTransform({ file: { js: ['vue.config.js'], }, }), } const ensureEOL = str => { if (str.charAt(str.length - 1) !== '\n') { return str + '\n' } return str } class Generator { constructor(pkg, context) { // 最终的 package.json 的模板 // 中间会注入很多内容,最后会根据 defaultConfigTransforms 对应的文件,并删除对应的字段。 this.pkg = pkg // main.js 中 new Vue 实例的选项 this.rootOptions = {} // main.js 中 导入语句 this.imports = {} // 所有需要生成文件的集合,最终根据此字段来生成文件及内容 this.files = {} // 入口文件 this.entryFile = `src/main.js` // 模板文件中间件: [ ()=> {}, ... ] this.fileMiddlewares = [] // 项目路径 this.context = context // 中间件文件的集合 { babel: ConfigTransform { fileDescriptor: { js: [Array] } }, ... } this.configTransforms = {} } // 合并对应的依赖项,向 pkg 中注入依赖 extendPackage(fields) { const pkg = this.pkg for (const key in fields) { const value = fields[key] const existing = pkg[key] if (isObject(value) && (key === 'dependencies' || key === 'devDependencies' || key === 'scripts')) { pkg[key] = Object.assign(existing || {}, value) } else { pkg[key] = value } } } async generate() { // 从 package.json 中提取文件 this.extractConfigFiles() // 解析文件内容 await this.resolveFiles() // 将 package.json 中的字段排序 this.sortPkg() this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n' // 将所有文件写入到用户要创建的目录 await writeFileTree(this.context, this.files) } // 按照下面的顺序对 package.json 中的 key 进行排序 sortPkg() { // ensure package.json keys has readable order this.pkg.dependencies = sortObject(this.pkg.dependencies) this.pkg.devDependencies = sortObject(this.pkg.devDependencies) this.pkg.scripts = sortObject(this.pkg.scripts, [ 'dev', 'build', 'test:unit', 'test:e2e', 'lint', 'deploy', ]) this.pkg = sortObject(this.pkg, [ 'name', 'version', 'private', 'description', 'author', 'scripts', 'husky', 'lint-staged', 'main', 'module', 'browser', 'jsDelivr', 'unpkg', 'files', 'dependencies', 'devDependencies', 'peerDependencies', 'vue', 'babel', 'eslintConfig', 'prettier', 'postcss', 'browserslist', 'jest', ]) } // 使用 ejs 解析 lib\generator\xx\template 中的文件 async resolveFiles() { const files = this.files for (const middleware of this.fileMiddlewares) { await middleware(files, ejs.render) } // 此时 files: { src/App.vue: '模板', xxx } // windows上规范文件路径:将反斜杠 \ 转换为正斜杠 / normalizeFilePaths(files) // 处理 import 语句的导入和 new Vue() 选项的注入 // vue-codemod 库,对代码进行解析得到 AST,再将 import 语句和根选项注入 // this.imports['src/main.js'].add('import xxx from \'./xxx\'') Object.keys(files).forEach(file => { // this.imports: // { // 'src/main.js': Set(2) { // "import router from './router'", // "import store from './store'" // } // } let imports = this.imports[file] imports = imports instanceof Set ? Array.from(imports) : imports if (imports && imports.length > 0) { files[file] = runTransformation( { path: file, source: files[file] }, require('./utils/codemods/injectImports'), { imports }, ) } // this.rootOptions: // { 'src/main.js': Set(2) { 'router', 'store' } } let injections = this.rootOptions[file] injections = injections instanceof Set ? Array.from(injections) : injections if (injections && injections.length > 0) { files[file] = runTransformation( { path: file, source: files[file] }, require('./utils/codemods/injectOptions'), { injections }, ) } }) } // 将 package.json 中的配置提取出来,生成单独的文件 // 例如将 package.json 中的 // babel: { // presets: ['@babel/preset-env'] // }, // 提取出来变成 babel.config.js 文件 extractConfigFiles() { // { // babel: ConfigTransform { fileDescriptor: { js: [Array] } }, // postcss: ConfigTransform { // fileDescriptor: { js: [Array], json: [Array], yaml: [Array] } // }, // eslintConfig: ConfigTransform { // fileDescriptor: { js: [Array], json: [Array], yaml: [Array] } // }, // jest: ConfigTransform { fileDescriptor: { js: [Array] } }, // browserslist: ConfigTransform { fileDescriptor: { lines: [Array] } }, // vue: ConfigTransform { fileDescriptor: { js: [Array] } } // } const configTransforms = { ...defaultConfigTransforms, ...this.configTransforms, ...reservedConfigTransforms, } const extract = (key) => { if (configTransforms[key] && this.pkg[key]) { const value = this.pkg[key] const configTransform = configTransforms[key] const res = configTransform.transform( value, this.files, this.context, ) const { content, filename } = res // 如果文件不是以 \n 结尾,则补上 \n this.files[filename] = ensureEOL(content) // 删除对应项 delete this.pkg[key] } } // note: 为什么有的通过模板的形式 有的通过extract生成? extract('vue') extract('babel') } // 生成文件所需模板 render(source, additionalData = {}, ejsOptions = {}) { // 获取调用 generator.render() 函数的文件的父目录路径 const baseDir = extractCallDir() // C:\Users\pengguang\Desktop\learning\cli-stu\cli-V3\packages\pg-cli-plugin-vue\generator\template source = path.resolve(baseDir, source) this._injectFileMiddleware(async (files) => { const data = this._resolveData(additionalData) // https://github.com/sindresorhus/globby const globby = require('globby') // 读取目录中所有的文件 // [ // 'src/App.vue', // 'src/views/About.vue', // 'src/views/Home.vue', // 'src/router/index.js' // ] const _files = await globby(['**/*'], { cwd: source, dot: true }) for (const rawPath of _files) { const sourcePath = path.resolve(source, rawPath) // 解析文件内容,生成模板 const content = this.renderFile(sourcePath, data, ejsOptions) // only set file if it's not all whitespace, or is a Buffer (binary files) // 同 Array.isArray() if (Buffer.isBuffer(content) || /[^\s]/.test(content)) { files[rawPath] = content } } }) } _injectFileMiddleware(middleware) { this.fileMiddlewares.push(middleware) } // 合并选项 _resolveData(additionalData) { return { // options: this.options, rootOptions: this.rootOptions, ...additionalData, } } renderFile(name, data, ejsOptions) { // 如果是二进制文件,直接将读取结果返回,如图片等 if (isBinaryFileSync(name)) { return fs.readFileSync(name) // return buffer } // 返回文件内容 const template = fs.readFileSync(name, 'utf-8') // 使用 ejs:可以结合变量来决定是否渲染某些代码 return ejs.render(template, data, ejsOptions) } /** * Add import statements to a file. */ injectImports(file, imports) { const _imports = ( this.imports[file] || (this.imports[file] = new Set()) ); (Array.isArray(imports) ? imports : [imports]).forEach(imp => { _imports.add(imp) }) } /** * Add options to the root Vue instance (detected by `new Vue`). */ injectRootOptions(file, options) { const _options = ( this.rootOptions[file] || (this.rootOptions[file] = new Set()) ); (Array.isArray(options) ? options : [options]).forEach(opt => { _options.add(opt) }) } } // http://blog.shaochuancs.com/about-error-capturestacktrace/ // 获取调用栈信息 function extractCallDir() { const obj = {} Error.captureStackTrace(obj) // 在 lib\generator\xx 等各个模块中 调用 generator.render() // 将会排在调用栈中的第四个,也就是 obj.stack.split('\n')[3] const callSite = obj.stack.split('\n')[3] // the regexp for the stack when called inside a named function const namedStackRegExp = /\s\((.*):\d+:\d+\)$/ // the regexp for the stack when called inside an anonymous const anonymousStackRegExp = /at (.*):\d+:\d+$/ let matchResult = callSite.match(namedStackRegExp) if (!matchResult) { matchResult = callSite.match(anonymousStackRegExp) } const fileName = matchResult[1] // 获取对应文件的目录 console.log(fileName) // 获取对应的文件目录 C:\Users\pengguang\Desktop\learning\cli-stu\cli-V3\packages\pg-cli-plugin-vue\generator return path.dirname(fileName) } module.exports = Generator