├── .markdownlintignore ├── .markdownlint.json ├── commitlint.config.js ├── docs ├── images │ └── cmd-hooks.png ├── index.md ├── local-config.md ├── config.md ├── leorc.md └── meta.md ├── packages ├── leo-generator │ ├── src │ │ ├── meta.ts │ │ ├── index.ts │ │ ├── interface.ts │ │ └── Generator.ts │ ├── README.md │ ├── tsconfig.json │ └── package.json ├── leo-cli │ ├── README.md │ ├── src │ │ ├── index.ts │ │ ├── utils │ │ │ ├── index.ts │ │ │ ├── rebuild-commander-args.ts │ │ │ └── get-unexpected-options.ts │ │ ├── interface.ts │ │ └── CLI.ts │ ├── tsconfig.json │ └── package.json ├── leo-core │ ├── README.md │ ├── src │ │ ├── index.ts │ │ ├── utils │ │ │ ├── getPackagejson.ts │ │ │ ├── runInstall.ts │ │ │ ├── getRemotePkgTagVersion.ts │ │ │ ├── getRC.ts │ │ │ ├── index.ts │ │ │ └── extendsProcessEnv.ts │ │ ├── interface.ts │ │ ├── config │ │ │ ├── index.ts │ │ │ ├── getDefaultConfig.ts │ │ │ ├── interface.ts │ │ │ ├── configStore.ts │ │ │ └── staticConfig.ts │ │ ├── defaultRC.ts │ │ └── Core.ts │ ├── tsconfig.json │ ├── bin │ │ └── leo.js │ └── package.json └── leo-utils │ ├── src │ ├── index.ts │ ├── log.ts │ └── loadPkg.ts │ ├── tsconfig.json │ ├── package.json │ └── README.md ├── .gitignore ├── .eslintignore ├── f2elint.config.js ├── .prettierrc.js ├── .editorconfig ├── .eslintrc.js ├── tsconfig.json ├── LICENSE ├── README.md ├── package.json ├── gulpfile.js └── NOTICE /.markdownlintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | dist/ 4 | 5 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "markdownlint-config-ali" 3 | } 4 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['ali'], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/images/cmd-hooks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jd-opensource/leo/HEAD/docs/images/cmd-hooks.png -------------------------------------------------------------------------------- /packages/leo-generator/src/meta.ts: -------------------------------------------------------------------------------- 1 | const defaultMeta = {}; 2 | 3 | export default defaultMeta; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/ 3 | .webpack_cache 4 | .idea/ 5 | .vscode 6 | lib/ 7 | lerna-debug.log 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | dist/ 4 | lib/ 5 | **/*.min.js 6 | **/*-min.js 7 | **/*.bundle.js 8 | -------------------------------------------------------------------------------- /packages/leo-generator/src/index.ts: -------------------------------------------------------------------------------- 1 | import Generator from './Generator'; 2 | 3 | export default Generator; 4 | -------------------------------------------------------------------------------- /packages/leo-cli/README.md: -------------------------------------------------------------------------------- 1 | # leo 2 | 3 | leo 是「京东-平台业务研发部-应用业务产品研发部」开发的一款覆盖前端开发全链路、可扩展、可定制的脚手架工具,并提供模板、构建器、扩展命令等丰富的周边生态 4 | -------------------------------------------------------------------------------- /packages/leo-core/README.md: -------------------------------------------------------------------------------- 1 | # leo 2 | 3 | leo 是「京东-平台业务研发部-应用业务产品研发部」开发的一款覆盖前端开发全链路、可扩展、可定制的脚手架工具,并提供模板、构建器、扩展命令等丰富的周边生态 4 | -------------------------------------------------------------------------------- /packages/leo-generator/README.md: -------------------------------------------------------------------------------- 1 | # leo 2 | 3 | leo 是「京东-平台业务研发部-应用业务产品研发部」开发的一款覆盖前端开发全链路、可扩展、可定制的脚手架工具,并提供模板、构建器、扩展命令等丰富的周边生态 4 | -------------------------------------------------------------------------------- /packages/leo-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | import loadPkg from './loadPkg'; 2 | import log from './log'; 3 | 4 | export { loadPkg, log }; 5 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 1. [config](./config.md) 2 | 2. [leorc](./leorc.md) 3 | 3. [meta](./meta.md) 4 | 4. [本地使用 config](./local-config.md) 5 | -------------------------------------------------------------------------------- /f2elint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | enableStylelint: false, 3 | enableMarkdownlint: false, 4 | enablePrettier: true, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/leo-cli/src/index.ts: -------------------------------------------------------------------------------- 1 | import CLI from './CLI'; 2 | 3 | export { ICommandSettings } from './interface'; 4 | 5 | export default CLI; 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: true, 6 | trailingComma: 'all', 7 | arrowParens: 'always', 8 | }; 9 | -------------------------------------------------------------------------------- /packages/leo-cli/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import getUnexpectedOptions from './get-unexpected-options'; 2 | import rebuildCommanderArgs from './rebuild-commander-args'; 3 | 4 | export { getUnexpectedOptions, rebuildCommanderArgs }; 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | quote_type = single 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /packages/leo-core/src/index.ts: -------------------------------------------------------------------------------- 1 | import Core from './Core'; 2 | 3 | export { IConfig as ICoreConfig, configStore } from './config'; 4 | 5 | export { IRC as ICoreRC } from './defaultRC'; 6 | 7 | export { IHooks as ICoreHooks } from './Core'; 8 | 9 | export default Core; 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['eslint-config-ali/typescript/node', 'prettier', 'prettier/@typescript-eslint'], 3 | rules: { 4 | 'no-empty': [2, { allowEmptyCatch: true }], 5 | 'no-console': [0], 6 | '@typescript-eslint/no-require-imports': [0], 7 | 'no-param-reassign': [0], 8 | 'no-useless-return': [0], 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/leo-core/src/utils/getPackagejson.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export const getCorePackageJSON = () => { 4 | return require(path.resolve(__dirname, '../../package.json')); 5 | }; 6 | 7 | export const getProjectPackageJSON = () => { 8 | // init config 等指令时,可能不存在 package.json 9 | try { 10 | return require(path.resolve(process.cwd(), './package.json')); 11 | } catch (e) { 12 | return undefined; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /packages/leo-core/src/utils/runInstall.ts: -------------------------------------------------------------------------------- 1 | import spawn from 'cross-spawn'; 2 | import path from 'path'; 3 | 4 | export default (isYarn: boolean, projectName: string) => { 5 | return new Promise((resolve) => { 6 | const executable = isYarn ? 'yarn' : 'npm'; 7 | 8 | spawn.sync(executable, ['install'], { 9 | stdio: 'inherit', 10 | cwd: `${path.resolve(process.cwd(), projectName)}`, 11 | }); 12 | resolve(''); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/leo-core/src/utils/getRemotePkgTagVersion.ts: -------------------------------------------------------------------------------- 1 | import spawn from 'cross-spawn'; 2 | 3 | // 通过npm info获取包的版本信息 4 | export default (projectName: string): Promise<{ latest: string; [tag: string]: string }> => { 5 | return new Promise((resolve) => { 6 | const pkgInfoStr = spawn.sync('npm', ['info', projectName, '--json']).stdout.toString(); 7 | 8 | const pkgInfo = JSON.parse(pkgInfoStr); 9 | 10 | resolve(pkgInfo['dist-tags']); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/leo-core/src/utils/getRC.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import merge from 'lodash.merge'; 4 | import defaultRC, { IRC } from '../defaultRC'; 5 | 6 | export default (rcFileName: string): IRC => { 7 | if (!fs.existsSync(path.resolve(process.cwd(), rcFileName))) { 8 | return defaultRC; 9 | } 10 | const localLeoRc = require(path.resolve(process.cwd(), rcFileName)); 11 | 12 | return merge({}, defaultRC, localLeoRc); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/leo-core/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { getCorePackageJSON, getProjectPackageJSON } from './getPackagejson'; 2 | import getLeoRC from './getRC'; 3 | import runInstall from './runInstall'; 4 | import extendsProcessEnv from './extendsProcessEnv'; 5 | import getRemotePkgTagVersion from './getRemotePkgTagVersion'; 6 | export { 7 | getCorePackageJSON, 8 | getProjectPackageJSON, 9 | getLeoRC, 10 | runInstall, 11 | extendsProcessEnv, 12 | getRemotePkgTagVersion, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/leo-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "outDir": "lib", 9 | "strict": true, 10 | "moduleResolution": "node", 11 | "paths": { 12 | "@": ["./"] 13 | }, 14 | "esModuleInterop": true, 15 | "skipLibCheck": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "resolveJsonModule": true, 18 | "strictNullChecks": false 19 | }, 20 | "include": ["**/src/**/*"], 21 | "exclude": ["**/node_modules/**"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/leo-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "outDir": "lib", 9 | "strict": true, 10 | "moduleResolution": "node", 11 | "paths": { 12 | "@": ["./"] 13 | }, 14 | "esModuleInterop": true, 15 | "skipLibCheck": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "resolveJsonModule": true, 18 | "strictNullChecks": false 19 | }, 20 | "include": ["**/src/**/*"], 21 | "exclude": ["**/node_modules/**"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/leo-core/src/interface.ts: -------------------------------------------------------------------------------- 1 | import { IConfig } from './config'; 2 | import { IRC } from './defaultRC'; 3 | 4 | export interface ILeoCaller { 5 | start: () => Promise; 6 | [key: string]: any; 7 | } 8 | 9 | export interface IBuilder extends ILeoCaller { 10 | build: () => Promise; 11 | } 12 | 13 | export interface IVirtualPath { 14 | entry: string; 15 | configPath: string; 16 | templatePath: string; 17 | nodeModulesPath: string; 18 | } 19 | 20 | // 需要为插件提供的公共参数 21 | export interface ICommonParams { 22 | leoConfig: IConfig; 23 | leoRC: IRC; 24 | leoUtils: {}; 25 | pkg?: { [key: string]: any }; 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "outDir": "lib", 9 | "strict": true, 10 | "moduleResolution": "node", 11 | "paths": { 12 | "@": [ 13 | "./" 14 | ] 15 | }, 16 | "esModuleInterop": true, 17 | "skipLibCheck": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "resolveJsonModule": true, 20 | "strictNullChecks": false 21 | }, 22 | "include": [ 23 | "**/src/**/*" 24 | ], 25 | "exclude": [ 26 | "**/node_modules/**" 27 | ] 28 | } -------------------------------------------------------------------------------- /packages/leo-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "outDir": "lib", 9 | "strict": true, 10 | "moduleResolution": "node", 11 | "paths": { 12 | "@": [ 13 | "./" 14 | ] 15 | }, 16 | "esModuleInterop": true, 17 | "skipLibCheck": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "resolveJsonModule": true, 20 | "strictNullChecks": false 21 | }, 22 | "include": [ 23 | "**/src/**/*" 24 | ], 25 | "exclude": [ 26 | "**/node_modules/**" 27 | ] 28 | } -------------------------------------------------------------------------------- /packages/leo-generator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "outDir": "lib", 9 | "strict": true, 10 | "moduleResolution": "node", 11 | "paths": { 12 | "@": [ 13 | "./" 14 | ] 15 | }, 16 | "esModuleInterop": true, 17 | "skipLibCheck": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "resolveJsonModule": true, 20 | "strictNullChecks": false 21 | }, 22 | "include": [ 23 | "**/src/**/*" 24 | ], 25 | "exclude": [ 26 | "**/node_modules/**" 27 | ] 28 | } -------------------------------------------------------------------------------- /packages/leo-core/src/utils/extendsProcessEnv.ts: -------------------------------------------------------------------------------- 1 | import { IConfig } from '../config'; 2 | import get from 'lodash.get'; 3 | 4 | // 设置环境变量,仅供 leo-utils 使用 5 | // 其他插件应当从实例化传参中获取相应数据 6 | export default (config: IConfig) => { 7 | const env = { 8 | // loadPkg 的安装路径 9 | __NODEMODULEPATH: 'virtualPath.nodeModulesPath', 10 | // 是否使用 yarn 11 | __USEYARN: 'useYarn', 12 | // 是否 dev 模式 13 | __ISDEV: 'isDev', 14 | // 是否 debug 模式 15 | __ISDEBUG: 'isDebug', 16 | // __GITQUERYTOKEN: ['gitQueryHeader', 'Private-Token'], 17 | }; 18 | 19 | Object.entries(env).forEach((item) => { 20 | const [key, path] = item; 21 | process.env[key] = get(config, path).toString(); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/leo-core/src/config/index.ts: -------------------------------------------------------------------------------- 1 | // import merge from 'lodash.merge'; 2 | // import getConfig from './get-config'; 3 | // import defaultConfig, { defaultLocalConfig } from './config'; 4 | // 5 | // import { IConfig } from './interface'; 6 | // 7 | // 8 | // // 深拷贝一份 9 | // export const getDeepCopyConfig = (): IConfig => (merge({}, defaultConfig) as unknown) as IConfig; 10 | // export { defaultLocalConfig }; 11 | // 12 | // export default getConfig; 13 | import configStore from './configStore'; 14 | import { localStaticConfig, staticConfig } from './staticConfig'; 15 | import getDefaultConfig from './getDefaultConfig'; 16 | 17 | export { localStaticConfig, staticConfig, getDefaultConfig, configStore }; 18 | 19 | export { IConfig, IActionArgs } from './interface'; 20 | -------------------------------------------------------------------------------- /packages/leo-utils/src/log.ts: -------------------------------------------------------------------------------- 1 | import { Signale } from 'signale'; 2 | import { Writable } from 'stream'; 3 | 4 | const rewriteDebugTypes = (defaultTypes: T): T => { 5 | const writable = new Writable({ 6 | write(chunk, encoding, callback): void { 7 | if (process.env.__ISDEBUG === 'true') { 8 | process.stdout.write(chunk); 9 | } 10 | process.nextTick(callback); 11 | }, 12 | }); 13 | 14 | return { 15 | ...defaultTypes, 16 | debug: Object.assign({}, defaultTypes.debug, { 17 | stream: writable, 18 | }), 19 | }; 20 | }; 21 | 22 | const defaultTypes = require('signale/types.js'); 23 | 24 | const costomTypes = rewriteDebugTypes(defaultTypes); 25 | 26 | const log = new Signale({ 27 | types: costomTypes, 28 | }); 29 | 30 | export default log; 31 | -------------------------------------------------------------------------------- /packages/leo-generator/src/interface.ts: -------------------------------------------------------------------------------- 1 | import { Question } from 'inquirer'; 2 | import { HelperDelegate } from 'handlebars'; 3 | 4 | export interface IPrompt extends Question { 5 | name: string; // 问题字段 6 | require?: boolean; // 是否为必填项 7 | choices?: []; // 对应的多选选项 8 | } 9 | 10 | export interface IMeta { 11 | prompts: IPrompt[]; 12 | filterFilesMap?: IFilterFilesMap; 13 | helpers?: IHelper; 14 | hooks?: { 15 | beforeGenerate?: Function; 16 | afterGenerate?: Function; 17 | }; 18 | } 19 | 20 | export interface IFilterFilesMap { 21 | [propName: string]: string; 22 | } 23 | 24 | export interface IMetaData { 25 | [propName: string]: any; 26 | } 27 | 28 | export interface IFiles { 29 | [propName: string]: any; 30 | } 31 | 32 | export interface IHelper { 33 | [propName: string]: HelperDelegate; 34 | } 35 | 36 | export default IMeta; 37 | -------------------------------------------------------------------------------- /packages/leo-cli/src/utils/rebuild-commander-args.ts: -------------------------------------------------------------------------------- 1 | // program 2 | // .arguments(' [password]') 3 | // .options: [ 4 | // ['-uc, --use-cache', 'use cache', false], 5 | // ], 6 | // .action((username, password, options, command) => { 7 | // console.log('username:', username); 8 | // console.log('environment:', password || 'no password given'); 9 | // }); 10 | 11 | 12 | /** 13 | * 由于 commander 的机制,在 arguments 声明的变量会依次传入 action 中,options 和 command 对象会在最后传入 14 | * 本函数用于处理参数,使其格式化 15 | * @returns {Promise} 16 | */ 17 | 18 | export default (args: any[]) => { 19 | // args 中第一个出现类型为 object 的即为 options 20 | const argumentsIndex = args.findIndex((v) => typeof v === 'object'); 21 | return { 22 | arguments: args.slice(0, argumentsIndex), 23 | options: args[argumentsIndex], 24 | command: args[argumentsIndex + 1], 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /packages/leo-core/src/config/getDefaultConfig.ts: -------------------------------------------------------------------------------- 1 | import home from 'user-home'; 2 | import merge from 'lodash.merge'; 3 | import { localStaticConfig, staticConfig } from './staticConfig'; 4 | 5 | import { IDynamicConfig, IConfig } from './interface'; 6 | 7 | export default (customConfig: { [key: string]: any }): IConfig => { 8 | const config = merge({}, localStaticConfig, staticConfig, customConfig); 9 | 10 | const rootPath = `${home}/.${config.rootName}`; // 通常为 /Users/username/.leo 11 | 12 | const dynamicConfig: IDynamicConfig = { 13 | virtualPath: { 14 | // 虚拟目录存储路径 15 | entry: rootPath, 16 | configPath: `${rootPath}/config.json`, 17 | templatePath: `${rootPath}/templates`, 18 | nodeModulesPath: `${rootPath}/${config.rootName}_modules`, 19 | }, 20 | }; 21 | 22 | return { 23 | ...config, 24 | ...dynamicConfig, 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /packages/leo-cli/src/utils/get-unexpected-options.ts: -------------------------------------------------------------------------------- 1 | import minimist from 'minimist'; 2 | import commander from 'commander'; 3 | 4 | // 获取未声明的命令行传递的参数 5 | export default (declaredOptions: commander.Option[]) => { 6 | const argv = minimist(process.argv.slice(2)); 7 | 8 | // 获取声明的 options 的参数 -a 和 --all 作为黑名单 9 | const declaredOptionsStrings = declaredOptions.reduce((list: string[], option) => { 10 | const { short = '', long = '' } = option; 11 | list.push(short.slice(1)); 12 | list.push(long.slice(2)); 13 | return list; 14 | }, []); 15 | 16 | return Object.keys(argv).reduce((unexpectedOptions: { [key: string]: any }, key) => { 17 | if (key !== '_' && !declaredOptionsStrings.includes(key)) { 18 | return { 19 | ...unexpectedOptions, 20 | [key]: argv[key], 21 | }; 22 | } 23 | 24 | return unexpectedOptions; 25 | }, {}); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/leo-cli/src/interface.ts: -------------------------------------------------------------------------------- 1 | export interface ICommandSettingAlone { 2 | cmdDesc: string; 3 | arguments: string | null; 4 | argumentsDesc: { [key: string]: string } | null; 5 | allowUnknownOption: boolean; 6 | options: Array<[string, string?, (string | boolean)?]> | null; 7 | subCommands: { [key: string]: ICommandSettingAlone }; 8 | helpTexts?: Array<{ position: 'before' | 'after'; text: string }>; 9 | 10 | // 扩展命令字段 11 | action?: (params: { args: IActionArgs; subCommandName: string }) => void; 12 | } 13 | 14 | export interface ICommandSettings { 15 | [key: string]: ICommandSettingAlone; 16 | } 17 | 18 | export interface IActionArgs { 19 | arguments: any[]; 20 | options: { [key: string]: any }; 21 | command: any; 22 | unexpectedOptions: { [key: string]: any }; 23 | } 24 | 25 | export interface IExtendsCommands extends ICommandSettingAlone { 26 | helpTexts?: Array<{ position: 'before' | 'after'; text: string }>; 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2021 JD.com, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /packages/leo-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jdfed/leo-utils", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "author": "JDFED", 7 | "contributors": [ 8 | { 9 | "name": "JDFE" 10 | } 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/JDFED/leo.git" 15 | }, 16 | "license": "MIT", 17 | "private": false, 18 | "devDependencies": { 19 | "@leo/lint": "^0.1.5", 20 | "@types/fs-extra": "^9.0.12", 21 | "@types/lodash.merge": "^4.6.6", 22 | "@types/node": "^16.4.4", 23 | "@types/signale": "^1.4.1", 24 | "del": "^6.0.0", 25 | "gulp": "^4.0.2", 26 | "gulp-cached": "^1.1.1", 27 | "gulp-eslint": "^6.0.0", 28 | "gulp-remember": "^1.0.1", 29 | "gulp-rename": "^2.0.0", 30 | "gulp-typescript": "^6.0.0-alpha.1", 31 | "typescript": "^4.1.5" 32 | }, 33 | "dependencies": { 34 | "axios": "^0.21.1", 35 | "compare-versions": "^3.6.0", 36 | "fs-extra": "^10.0.0", 37 | "global-dirs": "^3.0.0", 38 | "lodash.merge": "^4.6.2", 39 | "npminstall": "^4.11.0", 40 | "signale": "^1.4.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/leo-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jdfed/leo-cli", 3 | "version": "1.0.1", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib" 8 | ], 9 | "author": "JDFED", 10 | "contributors": [ 11 | { 12 | "name": "JDFE" 13 | } 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/JDFED/leo.git" 18 | }, 19 | "license": "MIT", 20 | "dependencies": { 21 | "@jdfed/leo-utils": "^1.0.0", 22 | "commander": "^7.1.0", 23 | "figlet": "^1.5.0", 24 | "gulp-uglify": "^3.0.2", 25 | "lodash.get": "^4.4.2", 26 | "lodash.merge": "^4.6.2", 27 | "minimist": "^1.2.5", 28 | "signale": "^1.4.0" 29 | }, 30 | "devDependencies": { 31 | "@types/lodash.get": "^4.4.6", 32 | "@types/lodash.merge": "^4.6.6", 33 | "@types/minimist": "^1.2.2", 34 | "@types/signale": "^1.4.1", 35 | "cz-conventional-changelog": "^3.3.0", 36 | "del": "^6.0.0", 37 | "gulp": "^4.0.2", 38 | "gulp-cached": "^1.1.1", 39 | "gulp-eslint": "^6.0.0", 40 | "gulp-remember": "^1.0.1", 41 | "gulp-rename": "^2.0.0", 42 | "gulp-typescript": "^6.0.0-alpha.1", 43 | "typescript": "^4.1.5" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/leo-core/src/defaultRC.ts: -------------------------------------------------------------------------------- 1 | export interface IRC { 2 | // templateOrigin?: string; 3 | cli?: { 4 | hooks?: { 5 | beforeEveryAction?: (cli: any, commandName: string, args: any[]) => void | Promise; 6 | /* 7 | * 如果返回 false 则会拦截后续行为,完全自己代理 8 | * 比如自己创建的命令,需要return true 9 | */ 10 | shouldActionContinue?: ( 11 | cli: any, 12 | commandName: string, 13 | args: any[], 14 | ) => boolean | Promise; 15 | afterEveryAction?: (commandName: string, args: any[]) => void | Promise; 16 | }; 17 | }; 18 | 19 | testAction?: (options: { [key: string]: any }) => Promise; 20 | lintAction?: (options: { [key: string]: any }) => Promise; 21 | // 发布之前test,lint,build是否要并行 22 | isPrePublishParallel?: boolean; 23 | 24 | // 对应事后,在rc中定义 25 | builder?: { 26 | name: string; 27 | version?: string; 28 | hooks?: { 29 | beforeDev?: (Builder: any) => Promise; 30 | afterDev?: (Builder: any) => Promise; 31 | 32 | beforeBuild?: (Builder: any) => Promise; 33 | afterBuild?: (Builder: any) => Promise; 34 | }; 35 | }; 36 | publisher?: { 37 | name: string; 38 | version?: string; 39 | hooks?: { 40 | beforePublish?: (Publisher: any) => Promise; 41 | afterPublish?: (Publisher: any) => Promise; 42 | }; 43 | }; 44 | } 45 | 46 | const defaultRC: IRC = {}; 47 | 48 | export default defaultRC; 49 | -------------------------------------------------------------------------------- /packages/leo-core/bin/leo.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const LeoCore = require('../lib/index.js').default; 4 | 5 | const core = new LeoCore(); 6 | 7 | core.start(); 8 | 9 | // ===================== 支持自定义 ==================== 10 | 11 | const config = {}; 12 | // // 模板仓库group 13 | config.gitTemplateGroupURL = ''; 14 | 15 | // 模板模糊查询API 16 | config.gitTemplateLikeNameURL = 17 | 'http://coding.jd.com/webapi/teams/drip-templates/projects?nameLike='; 18 | 19 | // 选择模板的命令行交互 20 | config.questions.chooseTemplate = [ 21 | { 22 | name: 'templateName', 23 | type: 'list', 24 | message: `选择模板,如需更多模板可查阅: http://coding.jd.com/teams/${config.gitTemplateGroupName}/`, 25 | choices: [ 26 | { 27 | name: 'drip', 28 | value: 'drip', 29 | }, 30 | ], 31 | }, 32 | ]; 33 | 34 | // 对应命令行根名字,如drip init 35 | config.rootName = 'drip'; 36 | 37 | // 项目中配置文件名,如.eslintrc.js 38 | config.rcFileName = 'drip-rc.js'; 39 | 40 | // 帮助命令行 41 | config.helpTexts = [ 42 | { position: 'afterAll', text: '如遇相关问题可加入咚咚群:1022042900 咨询' }, 43 | { 44 | position: 'afterAll', 45 | text: 'drip 使用文档:http://drip.jd.com/wiki/index.html#/', 46 | }, 47 | ]; 48 | 49 | // 水滴自己的扩展命令 50 | config.cmds = { 51 | add: { 52 | name: 'drip-add-cmd', 53 | version: '0.0.2', 54 | }, 55 | }; 56 | 57 | const customCore = new LeoCore({ 58 | config, 59 | hooks: { 60 | beforeStart() { 61 | console.log(this.leoRC); 62 | }, 63 | }, 64 | }); 65 | 66 | customCore.start(); 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # leo 2 | 3 | leo 脚手架是一款覆盖前端开发全链路、可扩展、可定制的终端运行的脚手架工具,并支持模板、构建器、扩展命令等丰富的周边生态扩展。 4 | 5 | ## 背景与介绍 6 | 7 | 在过去,脚手架虽然被前端广泛使用,但往往都局限于部门和团队内部,其中提供的模板、构建器等提效工具仅在内部使用,难以在团队、部门之间甚至公司层面做到复用和标准化,而新团队如需快速沉淀自己内部的规范,又需要额外开发脚手架工具,造成资源浪费。 8 | 9 | leo 通过提供模板,构建器统一扩展来打破部门、团队之间模板和构建复用的壁垒,提高了新团队快速沉淀规范的效率,并且通过生成器和构建器分离,解耦了代码与构建配置的关联,使得模板和构建配置可以一对多或者多对一,减少了 webpack 等构建工具配置的困扰。 10 | 11 | ## 如何使用 12 | 13 | leo 提供了丰富的配置项用于快速完成一套定制化的脚手架 14 | 15 | ```shell script 16 | npm i @jdfed/leo-core 17 | ``` 18 | 19 | 新建一个脚手架项目目录如下 20 | 21 | ``` 22 | yourProject 23 | |- bin 24 | | |- index.js 25 | |- package.json 26 | ``` 27 | 28 | `package.json`中声明指令入口 29 | 30 | ```json 31 | { 32 | "bin": { 33 | "yourCommand": "bin/index.js" 34 | } 35 | } 36 | ``` 37 | 38 | 在`bin/index.js`中进行配置`leo/core` 39 | 40 | ```js 41 | #!/usr/bin/env node 42 | 43 | const LeoCore = require('@jdfed/leo-core').default; 44 | 45 | const customConfig = { 46 | // 模板仓库group 47 | gitTemplateGroupURL: '', 48 | // 项目中配置文件名,默认为 leorc.js 49 | rcFileName: 'xxx-rc.js', 50 | }; 51 | 52 | const customCore = new LeoCore({ 53 | config: customConfig, 54 | hooks: { 55 | beforeStart() { 56 | console.log(this.leoRC); 57 | }, 58 | afterCommandExecute() { 59 | console.log(this); 60 | }, 61 | }, 62 | }); 63 | 64 | customCore.start(); 65 | ``` 66 | 67 | [更多配置项](./docs/config.md) 68 | 69 | 本地调试 70 | 71 | ```shell script 72 | npm link 73 | 74 | yourCommand -h 75 | ``` 76 | 77 | ## 更多文档 78 | 79 | 1. [config](./docs/config.md) 80 | 2. [leorc](./docs/leorc.md) 81 | 3. [meta](./docs/meta.md) 82 | -------------------------------------------------------------------------------- /docs/local-config.md: -------------------------------------------------------------------------------- 1 | # 全局配置 2 | 3 | **以下用 leo 作为你指定的命令名** 4 | 5 | 更改配置可有助于开发 template,builder 以及 publisher 6 | 7 | 你可以使用 `config` 指令来控制全局配置 8 | 9 | ```shell script 10 | leo config -h 11 | Usage: leo config [options] [command] 12 | 13 | Commands: 14 | set = [= 获取 leo config 参数 16 | delete 删除 leo config 参数 17 | list 查看 leo config 配置 18 | help [command] display help for command 19 | ``` 20 | 21 | ## 默认配置 22 | 23 | ```json 24 | { 25 | "isDebugger": false, 26 | "isDev": true, 27 | "isGrayUser": false, 28 | "forceUpdate": true, 29 | "useYarn": false, 30 | "cmds": {}, 31 | "isAlwaysCheck": true 32 | } 33 | ``` 34 | 35 | ## 字段解释 36 | 37 | ### isDebugger 38 | 39 | 当前是否为 debug 模式,开启后会输出 `@leo/core` 中的关键信息。 40 | 41 | **默认值**:`false` 42 | 43 | ### isDev 44 | 45 | 当前是否为开发模式 46 | 47 | 开启后加载资源的路径会由 `leo` 私有路径变为全局加载,可用于本地调试 builder 等。 48 | 49 | **默认值**:`false` 50 | 51 | ### isGrayUser 52 | 53 | 当前用户是否为灰度用户,设置为 true 后,当发布 leo 灰度版本也会提示更新使用。 54 | 55 | **默认值**:`false` 56 | 57 | ### focusUpdate 58 | 59 | 如有最新版本,是否强制升级 60 | 61 | **默认值**:`true` 62 | 63 | ### useYarn 64 | 65 | 是否使用 yarn,当本地开发 builder 等扩展时,如果使用 yarn link 连接至全局,则需开启次选项。 66 | 67 | ### cmds 68 | 69 | 全局指令扩展 70 | 71 | 以 `xxx-cmd` 扩展指令为例,你可以使用 `leo config set cmds.xxx-cmd=1.0.0` 来添加全局指令,之后你可以在任意项目中通过 `leo xxx` 来使用这个扩展指令。 72 | 73 | [如何编写扩展指令](http://doc.jd.com/feb-book/leo/advance/cmd.html) 74 | 75 | ### isAlwaysCheck 76 | 77 | 是否每次命令,都要进行检查。如果关闭,你也可以`leo check`单独使用 78 | 79 | **默认值**:`true` 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leo", 3 | "author": "JDFED", 4 | "contributors": [ 5 | { 6 | "name": "JDFE" 7 | } 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/JDFED/leo.git" 12 | }, 13 | "license": "MIT", 14 | "private": false, 15 | "workspaces": { 16 | "packages": [ 17 | "packages/*" 18 | ] 19 | }, 20 | "scripts": { 21 | "dev:core": "cross-env DIR=leo-core gulp", 22 | "dev:cli": "cross-env DIR=leo-cli gulp", 23 | "dev:generator": "cross-env DIR=leo-generator gulp", 24 | "dev:utils": "cross-env DIR=leo-utils gulp", 25 | "build:core": "cross-env DIR=leo-core gulp build", 26 | "build:cli": "cross-env DIR=leo-cli gulp build", 27 | "build:generator": "cross-env DIR=leo-generator gulp build", 28 | "build:utils": "cross-env DIR=leo-utils gulp build", 29 | "build:all": "npm run build:core && npm run build:cli && npm run build:generator && npm run build:utils", 30 | "f2elint-scan": "f2elint scan", 31 | "f2elint-fix": "f2elint fix" 32 | }, 33 | "devDependencies": { 34 | "cross-env": "^7.0.3", 35 | "del": "^6.0.0", 36 | "f2elint": "^1.2.1", 37 | "gulp": "^4.0.2", 38 | "gulp-cached": "^1.1.1", 39 | "gulp-eslint": "^6.0.0", 40 | "gulp-remember": "^1.0.1", 41 | "gulp-rename": "^2.0.0", 42 | "gulp-typescript": "^6.0.0-alpha.1", 43 | "gulp-uglify": "^3.0.2", 44 | "lerna": "^3.22.1", 45 | "typescript": "^4.4.3" 46 | }, 47 | "husky": { 48 | "hooks": { 49 | "pre-commit": "f2elint commit-file-scan", 50 | "commit-msg": "f2elint commit-msg-scan" 51 | } 52 | }, 53 | "engines": { 54 | "node": ">=10.0.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/leo-generator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jdfed/leo-generator", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib" 8 | ], 9 | "author": "JDFED", 10 | "contributors": [ 11 | { 12 | "name": "JDFE" 13 | } 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/JDFED/leo.git" 18 | }, 19 | "license": "MIT", 20 | "private": false, 21 | "dependencies": { 22 | "@jdfed/leo-utils": "^1.0.0", 23 | "command-exists": "^1.2.9", 24 | "consolidate": "^0.16.0", 25 | "cross-spawn": "^7.0.3", 26 | "download-git-repo": "^3.0.2", 27 | "fs-extra": "^9.1.0", 28 | "handlebars": "^4.7.7", 29 | "inquirer": "^8.0.0", 30 | "lodash.get": "^4.4.2", 31 | "lodash.merge": "^4.6.2", 32 | "metalsmith": "^2.3.0", 33 | "minimatch": "^3.0.4", 34 | "multimatch": "^5.0.0", 35 | "npminstall": "^4.11.0", 36 | "ora": "^5.3.0", 37 | "signale": "^1.4.0", 38 | "simple-git": "^2.40.0" 39 | }, 40 | "devDependencies": { 41 | "@types/command-exists": "^1.2.0", 42 | "@types/consolidate": "^0.14.0", 43 | "@types/cross-spawn": "^6.0.2", 44 | "@types/fs-extra": "^9.0.8", 45 | "@types/handlebars": "^4.1.0", 46 | "@types/inquirer": "^7.3.1", 47 | "@types/lodash.get": "^4.4.6", 48 | "@types/lodash.merge": "^4.6.6", 49 | "@types/metalsmith": "^2.3.0", 50 | "@types/minimatch": "^3.0.3", 51 | "@types/multimatch": "^4.0.0", 52 | "@types/signale": "^1.4.1", 53 | "cz-conventional-changelog": "^3.3.0", 54 | "del": "^6.0.0", 55 | "gulp": "^4.0.2", 56 | "gulp-cached": "^1.1.1", 57 | "gulp-eslint": "^6.0.0", 58 | "gulp-remember": "^1.0.1", 59 | "gulp-rename": "^2.0.0", 60 | "gulp-typescript": "^6.0.0-alpha.1", 61 | "gulp-uglify": "^3.0.2", 62 | "typescript": "^4.1.5" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/leo-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jdfed/leo-core", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "author": "JDFED", 8 | "contributors": [ 9 | { 10 | "name": "JDFE" 11 | } 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/JDFED/leo.git" 16 | }, 17 | "license": "MIT", 18 | "private": false, 19 | "dependencies": { 20 | "@acot/find-chrome": "0.0.12", 21 | "@jdfed/leo-utils": "^1.0.0", 22 | "@types/compare-versions": "^3.3.0", 23 | "@types/lodash.clonedeep": "^4.5.6", 24 | "@types/lodash.set": "^4.3.6", 25 | "@types/semver": "^7.3.5", 26 | "@types/update-notifier": "^5.0.0", 27 | "axios": "^0.19.0", 28 | "command-exists": "^1.2.9", 29 | "compare-versions": "^3.6.0", 30 | "cross-spawn": "^7.0.3", 31 | "fs-extra": "^9.1.0", 32 | "global-dirs": "^3.0.0", 33 | "inquirer": "8.0.0", 34 | "lodash.clonedeep": "^4.5.0", 35 | "lodash.get": "^4.4.2", 36 | "lodash.merge": "^4.6.2", 37 | "lodash.set": "^4.3.2", 38 | "lodash.unset": "^4.5.2", 39 | "module-exists": "^0.4.0", 40 | "npminstall": "^4.10.0", 41 | "ora": "^5.3.0", 42 | "semver": "^7.3.5", 43 | "signale": "^1.4.0", 44 | "simple-git": "^2.36.2", 45 | "update-notifier": "^5.1.0", 46 | "user-home": "^2.0.0" 47 | }, 48 | "devDependencies": { 49 | "@types/command-exists": "^1.2.0", 50 | "@types/cross-spawn": "^6.0.2", 51 | "@types/figlet": "^1.2.1", 52 | "@types/fs-extra": "^9.0.8", 53 | "@types/inquirer": "^7.3.1", 54 | "@types/lodash.get": "^4.4.6", 55 | "@types/lodash.merge": "^4.6.6", 56 | "@types/lodash.unset": "^4.5.6", 57 | "@types/node": "^14.14.31", 58 | "@types/puppeteer": "^5.4.3", 59 | "@types/qs": "^6.9.6", 60 | "@types/signale": "^1.4.1", 61 | "@types/user-home": "^2.0.0" 62 | }, 63 | "engines": { 64 | "node": ">=10.0.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const { src, dest, series, watch } = require('gulp'); 2 | const gulpTypescript = require('gulp-typescript'); 3 | const del = require('del'); 4 | const rename = require('gulp-rename'); 5 | const eslint = require('gulp-eslint'); 6 | const cache = require('gulp-cached'); 7 | const remember = require('gulp-remember'); 8 | const uglify = require('gulp-uglify'); 9 | 10 | const dir = require('path').resolve(process.cwd(), 'packages', process.env.DIR); 11 | 12 | function buildTs() { 13 | const tsProject = gulpTypescript.createProject(`${dir}/tsconfig.json`); 14 | 15 | return src(`${dir}/src/**/*.ts`) 16 | .pipe(cache('build-ts')) 17 | .pipe(tsProject()) 18 | .pipe( 19 | rename((path) => { 20 | path.dirname = path.dirname.replace(`${dir}/src`, `${dir}/lib`); 21 | }), 22 | ) 23 | .pipe(remember('build-ts')) 24 | .pipe(dest(`${dir}/lib`)); 25 | } 26 | function uglifyJS() { 27 | return src(`${dir}/lib/**/*.js`) 28 | .pipe( 29 | uglify({ 30 | output: { indent_level: 0 }, 31 | }), 32 | ) 33 | .pipe(dest(`${dir}/lib`)); 34 | } 35 | function moveJSON() { 36 | return src(`${dir}/src/**/*.json`).pipe(dest(`${dir}/lib`)); 37 | } 38 | 39 | function clean(cb) { 40 | del(`${dir}/lib`).then(() => cb()); 41 | } 42 | 43 | function buildPackages() { 44 | return series(buildTs, moveJSON); 45 | } 46 | 47 | function lint() { 48 | return src(`${dir}/src/**/*.ts`) 49 | .pipe(cache('linting')) 50 | .pipe( 51 | eslint({ 52 | quiet: false, 53 | fix: true, 54 | }), 55 | ) 56 | .pipe(eslint.format()) 57 | .pipe(remember('linting')) 58 | .pipe(eslint.failAfterError()); 59 | } 60 | 61 | function watchTask() { 62 | watch(`${dir}/src/**/*.ts`, series(lint)); 63 | watch(`${dir}/src/**/*.ts`, series(buildTs)); 64 | watch(`${dir}/src/**/*.json`, series(moveJSON)); 65 | } 66 | 67 | exports.default = series(clean, buildPackages(), lint, watchTask); 68 | exports.clean = clean; 69 | exports.build = series(clean, buildPackages(), lint, uglifyJS); 70 | exports.lint = lint; 71 | -------------------------------------------------------------------------------- /packages/leo-core/src/config/interface.ts: -------------------------------------------------------------------------------- 1 | export interface ICommandSetting { 2 | cmdDesc: string; 3 | arguments?: string | null; 4 | argumentsDesc?: { [key: string]: string } | null; 5 | allowUnknownOption?: boolean; 6 | options?: Array<[string, string?, (string | boolean)?]> | null; 7 | subCommands?: { [key: string]: ICommandSetting }; 8 | helpTexts?: Array<{ position: 'before' | 'after'; text: string }>; 9 | } 10 | 11 | export interface IActionArgs { 12 | arguments: any[]; 13 | options: { [key: string]: any }; 14 | command: any; 15 | unexpectedOptions: { [key: string]: any }; 16 | } 17 | 18 | export interface ICommandJSON { 19 | [key: string]: ICommandSetting; 20 | } 21 | 22 | // 暴露给用户配置的本地config 23 | export interface ILocalConfig { 24 | isDebug: boolean; 25 | // 本地开放模式 26 | isDev: boolean; 27 | // 用于设定当前用户是否问灰度用户 28 | isGrayUser?: false; 29 | // 是否强制升级 30 | forceUpdate: boolean; 31 | // 在init命令时,是否启用模糊查询模板名,开启后需要登录才能支持 32 | fuzzyTemp: boolean; 33 | useYarn: boolean; 34 | // 扩展命令 35 | cmds?: { [key: string]: string }; 36 | } 37 | 38 | export interface IStaticConfig { 39 | rootName: string; 40 | rcFileName: string; 41 | version: string; 42 | 43 | gitTemplateGroupURL: string; 44 | gitTemplateGroupQueryURL: string; 45 | gitQueryHeader: { [key: string]: any }; 46 | npmRegistry: string; 47 | commands: ICommandJSON; 48 | remoteConfigUrl: string; 49 | 50 | // 添加帮助信息 https://github.com/tj/commander.js#custom-help 51 | helpTexts?: Array<{ position: 'beforeAll' | 'afterAll'; text: string }>; 52 | questions: { [key: string]: Array<{ [key: string]: any }> }; 53 | // 对应事前,在config中定义 54 | cli: { 55 | name: string; 56 | version: string; 57 | plugins?: Array<{ name: string; version: string }>; 58 | }; 59 | generator: { 60 | name: string; 61 | version?: string; 62 | }; 63 | // 扩展命令 64 | cmds?: { [key: string]: string }; 65 | } 66 | 67 | // 依赖于静态config生成的config 68 | export interface IDynamicConfig { 69 | virtualPath: { 70 | // 虚拟目录存储路径 71 | entry: string; 72 | configPath: string; 73 | templatePath: string; 74 | nodeModulesPath: string; 75 | }; 76 | } 77 | 78 | export interface IConfig extends ILocalConfig, IStaticConfig, IDynamicConfig {} 79 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | This project uses the functions provided by the following packages. Please find the Copyright Notice and License Texts of the packages. 2 | 3 | cross-env https://github.com/kentcdodds/cross-env/blob/master/LICENSE 4 | del https://github.com/sindresorhus/del/blob/main/license 5 | f2elint https://github.com/alibaba/f2e-spec/blob/main/LICENSE 6 | gulp https://github.com/gulpjs/gulp/blob/master/LICENSE 7 | lerna https://github.com/lerna/lerna/blob/main/LICENSE 8 | typescript https://github.com/microsoft/TypeScript/blob/main/LICENSE.txt 9 | axios https://github.com/axios/axios/blob/master/LICENSE 10 | compare-versions https://github.com/omichelsen/compare-versions/blob/master/LICENSE 11 | fs-extra https://github.com/jprichardson/node-fs-extra/blob/master/LICENSE 12 | global-dirs https://github.com/sindresorhus/global-dirs/blob/main/license 13 | lodash.merge https://github.com/lodash/lodash/blob/master/LICENSE 14 | npminstall https://github.com/cnpm/npminstall/blob/master/LICENSE.txt 15 | signale https://github.com/klaussinani/signale/blob/master/license.md 16 | command-exists https://github.com/mathisonian/command-exists/blob/master/LICENSE 17 | consolidate https://github.com/tj/consolidate.js#license 18 | cross-spawn https://github.com/moxystudio/node-cross-spawn/blob/master/LICENSE 19 | download-git-repo https://gitlab.com/flippidippi/download-git-repo#license 20 | handlebars https://github.com/handlebars-lang/handlebars.js/blob/master/LICENSE 21 | inquirer https://github.com/SBoudrias/Inquirer.js/blob/master/LICENSE 22 | lodash.get https://github.com/lodash/lodash/blob/master/LICENSE 23 | minimatch https://github.com/isaacs/minimatch/blob/master/LICENSE 24 | multimatch https://github.com/sindresorhus/multimatch/blob/main/license 25 | ora https://github.com/sindresorhus/ora/blob/main/license 26 | simple-git https://github.com/steveukx/git-js/blob/main/LICENSE 27 | lodash.clonedeep https://github.com/lodash/lodash/blob/master/LICENSE 28 | lodash.set https://github.com/lodash/lodash/blob/master/LICENSE 29 | lodash.unset https://github.com/lodash/lodash/blob/master/LICENSE 30 | minimist https://github.com/substack/minimist/blob/master/LICENSE 31 | semver https://github.com/npm/node-semver/blob/main/LICENSE 32 | commander https://github.com/tj/commander.js/blob/master/LICENSE 33 | -------------------------------------------------------------------------------- /packages/leo-core/src/config/configStore.ts: -------------------------------------------------------------------------------- 1 | import merge from 'lodash.merge'; 2 | import clonedeep from 'lodash.clonedeep'; 3 | import set from 'lodash.set'; 4 | import compareVersions from 'compare-versions'; 5 | import axios from 'axios'; 6 | import fs from 'fs-extra'; 7 | import path from 'path'; 8 | import { IConfig } from './interface'; 9 | 10 | class ConfigStore { 11 | config: IConfig; 12 | remoteConfigPath: string = path.resolve(__dirname, './remote-config.json'); 13 | // 运行时的config的存储路径 14 | // 在 init 后,将获取的初始化的config写入该路径,并全部从此处取值 15 | // 16 | 17 | runTimeConfigPath :string = path.resolve() 18 | set(keyPath: Array, value: any) { 19 | set(this.config, keyPath, value); 20 | return this.config; 21 | } 22 | 23 | getConfig() { 24 | return clonedeep(this.config); 25 | } 26 | 27 | /* 28 | * config 获取流程: 29 | * 1. 获取项目中的 default-config(传入的config) 30 | * 2. 获取用户虚拟目录下的 config(如不存在,则默认为 {}) 31 | * 3. 与传入的默认 config 合并 32 | * 4. 异步执行:获取 cdn 存放的 config 文件,通过版本号检查与项目中的 config 是否匹配,不匹配则写入项目中作为 remote-config.json 33 | * 34 | * outerConfig指的是从new Core 传入的参数,进行进一步定制 35 | * 36 | * 注意: 37 | * 1. 异步获取的远程 config 仅在下次有效 38 | * 2. 优先级:虚拟目录 > 远程 > 初始化传入 39 | * 40 | */ 41 | 42 | init(defaultConfig: IConfig) { 43 | const remoteConfig = this.getSavedRemoteConfig() as IConfig; 44 | 45 | const localConfig = this.getLocalConfig(defaultConfig.virtualPath.configPath) as IConfig; 46 | 47 | this.config = merge({}, defaultConfig, remoteConfig, localConfig); 48 | 49 | this.remoteConfigOperation(); 50 | 51 | 52 | return this; 53 | } 54 | 55 | // 获取远程 config 并写入本地 56 | async remoteConfigOperation() { 57 | const { remoteConfigUrl, version } = this.config; 58 | 59 | if (!remoteConfigUrl) { 60 | return; 61 | } 62 | 63 | try { 64 | const res = await axios.get(remoteConfigUrl); 65 | 66 | const remoteConfigJSON: { [key: string]: any } = res.data; 67 | 68 | // 在项目中的 config 大于远程 config 的 version 时,不会再本地存储 69 | if (compareVersions.compare(version, remoteConfigJSON.version, '>')) { 70 | return; 71 | } 72 | 73 | await fs.writeJson(this.remoteConfigPath, remoteConfigJSON); 74 | } catch (e) {} 75 | } 76 | 77 | // 获取存在本地的远程 config 78 | getSavedRemoteConfig = () => { 79 | try { 80 | return require(this.remoteConfigPath); 81 | } catch (e) { 82 | return {}; 83 | } 84 | }; 85 | 86 | // 获取本地虚拟目录中的 config 87 | getLocalConfig = (localConfigPath: string) => { 88 | try { 89 | return require(localConfigPath); 90 | } catch (e) { 91 | return {}; 92 | } 93 | }; 94 | } 95 | 96 | export default new ConfigStore(); 97 | -------------------------------------------------------------------------------- /packages/leo-utils/README.md: -------------------------------------------------------------------------------- 1 | # leo-utils 2 | 3 | 为 leo 及其生态提供的工具库 4 | 5 | ## loadPkg 6 | 7 | 动态加载 8 | 9 | ### 基础使用 10 | 11 | ```js 12 | import { loadPkg } from '@leo/leo-utils'; 13 | 14 | (async () => { 15 | const puppeteer = await loadPkg('puppeteer', '10.0.0'); 16 | })(); 17 | ``` 18 | 19 | ### 高级使用 20 | 21 | ```js 22 | import { loadPkg, log } from '@leo/leo-utils'; 23 | 24 | (async () => { 25 | const puppeteer = await loadPkg('puppeteer', { 26 | version: '10.0.0', 27 | private: true, 28 | beforeInstall: () => { 29 | log.info('Log info when beforeInstall'); 30 | }, 31 | installSuccess: () => { 32 | log.success('Log info when installSuccess'); 33 | }, 34 | installFail: () => { 35 | log.error('Log info when installFail'); 36 | }, 37 | }); 38 | })(); 39 | ``` 40 | 41 | ### API 42 | 43 | #### loadPkg 44 | 45 | | 属性名 | 描述 | 类型 | 默认值 | 46 | | ------------------------------ | ------------------------ | ------------------------------------------ | ------- | 47 | | name | 安装路径 | `string` | `--` | 48 | | versionOrOptions | 版本号或相关配置 | `string` / `object` | `--` | 49 | | versionOrOptions.version | 版本号 | `string` | `false` | 50 | | versionOrOptions.dev | 本次安装是否为开发模式 | `boolean` | `false` | 51 | | versionOrOptions.private | 是否将安装的包单独隔离 | `boolean` | `false` | 52 | | versionOrOptions.beforeInstall | 本次安装后执行的声明周期 | `(name: string, version?: string) => void` | `--` | 53 | | versionOrOptions.afterInstall | 本次安装前执行的声明周期 | `(name: string, version?: string) => void` | `-- | 54 | 55 | ## log 56 | 57 | 基于 [signale](https://www.npmjs.com/package/signale) 封装的 log 工具 58 | 59 | ### 基础使用 60 | 61 | ```js 62 | import { log } from '@leo/leo-utils'; 63 | 64 | log.error(''); 65 | log.fatal(''); 66 | log.fav(''); 67 | log.info(''); 68 | log.star(''); 69 | log.success(''); 70 | log.wait(''); 71 | log.warn(''); 72 | log.complete(''); 73 | log.pending(''); 74 | log.note(''); 75 | log.start(''); 76 | log.pause(''); 77 | log.debug(''); 78 | log.await(''); 79 | log.watch(''); 80 | log.log(''); 81 | ``` 82 | 83 | ### 高级使用 84 | 85 | ```js 86 | import { log } from '@leo/leo-utils'; 87 | 88 | // 在输出时标识你的作用域,用于区分输出 89 | const myLog = log.scope('plugin scope'); 90 | 91 | myLog.info('info'); 92 | 93 | // [plugin scope] › ℹ info Info will has scope remark 94 | ``` 95 | 96 | ### 注意 97 | 98 | - `log.debug` 是否输出由 `leoConfig.isDebug` 来控制,你可以通过使用 `log.debug` 来定位线上问题 99 | 100 | 101 | -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | # config 2 | 3 | 以下配置都可以在实例化时传参进行覆盖 4 | 5 | ```js 6 | export const staticConfig = { 7 | // 脚手架名称 8 | rootName: 'leo', 9 | // rc文件名称 10 | rcFileName: 'leorc.js', 11 | // 当前配置的版本 12 | version: '1.0.0', 13 | // leo-generator 根据此地址 clone 模板 14 | gitTemplateGroupURL: '', 15 | // leo init 模糊查询接口 16 | gitTemplateGroupQueryURL: '', 17 | // git 查询接口需要的请求头 18 | gitQueryHeader: {}, 19 | // npm 源 20 | npmRegistry: 'https://registry.npmjs.org/', 21 | // 远程 config 地址 22 | remoteConfigUrl: ``, 23 | // cli 包名即版本 24 | cli: { 25 | name: '@jdfed/leo-cli', 26 | version: '1.0.1', 27 | }, 28 | // generator 包名即版本 29 | generator: { 30 | name: '@jdfed/leo-generator', 31 | version: '1.0.0', 32 | }, 33 | // 命令集合 34 | commands: { 35 | init: { 36 | cmdDesc: '创建一个项目', 37 | arguments: '[name]', 38 | argumentsDesc: { 39 | name: '模版名 不传则会推荐', 40 | }, 41 | options: [ 42 | ['--use-cache', '使用本地缓存的模板', false], 43 | ['-r, --repo ', '需要关联的 git 仓库地址'], 44 | ], 45 | }, 46 | dev: { 47 | cmdDesc: '本地开发', 48 | }, 49 | build: { 50 | cmdDesc: '构建项目', 51 | }, 52 | lint: { 53 | cmdDesc: '检查代码', 54 | }, 55 | test: { 56 | cmdDesc: '执行测试', 57 | }, 58 | config: { 59 | cmdDesc: '设置/获取 leo config', 60 | subCommands: { 61 | set: { 62 | cmdDesc: '设置 leo config 参数', 63 | arguments: '= [= ...]', 64 | }, 65 | get: { 66 | cmdDesc: '获取 leo config 参数', 67 | arguments: '', 68 | }, 69 | delete: { 70 | cmdDesc: '删除 leo config 参数', 71 | arguments: '', 72 | }, 73 | list: { 74 | cmdDesc: '查看 leo config 配置', 75 | }, 76 | }, 77 | helpTexts: [ 78 | { 79 | position: 'after', 80 | text: '欢迎使用 leo', 81 | }, 82 | ], 83 | }, 84 | }, 85 | // 终端询问集合 86 | questions: { 87 | chooseTemplate: [], 88 | init: [ 89 | { 90 | name: 'projectName', 91 | type: 'string', 92 | require: true, 93 | demand: true, 94 | message: '生成项目所在文件夹名称', 95 | validate: (val: string) => { 96 | return val ? true : '请输入生成项目所在文件夹名称'; 97 | }, 98 | }, 99 | { 100 | name: 'useInstall', 101 | type: 'list', 102 | message: '是否需要在初始化后安装依赖?', 103 | choices: [ 104 | { 105 | name: '使用 npm install', 106 | value: 'npm', 107 | }, 108 | { 109 | name: '使用 yarn install', 110 | value: 'yarn', 111 | }, 112 | { 113 | name: '不需要安装', 114 | value: 'custom', 115 | }, 116 | ], 117 | }, 118 | ], 119 | }, 120 | }; 121 | ``` 122 | -------------------------------------------------------------------------------- /packages/leo-core/src/config/staticConfig.ts: -------------------------------------------------------------------------------- 1 | import { IStaticConfig, ILocalConfig } from './interface'; 2 | 3 | export const localStaticConfig: ILocalConfig = { 4 | isDebug: false, 5 | isDev: false, 6 | isGrayUser: false, 7 | forceUpdate: true, 8 | useYarn: false, 9 | fuzzyTemp: true, 10 | cmds: {}, 11 | }; 12 | 13 | export const staticConfig: IStaticConfig = { 14 | rootName: 'leo', 15 | rcFileName: 'leorc.js', 16 | version: '1.0.0', 17 | // leo-generator 根据此地址 clone 模板 18 | gitTemplateGroupURL: '', 19 | // leo init 模糊查询接口 20 | gitTemplateGroupQueryURL: '', 21 | // git 查询接口需要的请求头 22 | gitQueryHeader: {}, 23 | npmRegistry: 'https://registry.npmjs.org/', 24 | remoteConfigUrl: ``, 25 | cli: { 26 | name: '@jdfed/leo-cli', 27 | version: '1.0.1', 28 | }, 29 | generator: { 30 | name: '@jdfed/leo-generator', 31 | version: '1.0.0', 32 | }, 33 | commands: { 34 | init: { 35 | cmdDesc: '创建一个项目', 36 | arguments: '[name]', 37 | argumentsDesc: { 38 | name: '模版名 不传则会推荐', 39 | }, 40 | options: [ 41 | ['--use-cache', '使用本地缓存的模板', false], 42 | ['-r, --repo ', '需要关联的 git 仓库地址'], 43 | ], 44 | }, 45 | dev: { 46 | cmdDesc: '本地开发', 47 | }, 48 | build: { 49 | cmdDesc: '构建项目', 50 | }, 51 | lint: { 52 | cmdDesc: '检查代码', 53 | }, 54 | test: { 55 | cmdDesc: '执行测试', 56 | }, 57 | config: { 58 | cmdDesc: '设置/获取 leo config', 59 | subCommands: { 60 | set: { 61 | cmdDesc: '设置 leo config 参数', 62 | arguments: '= [= ...]', 63 | }, 64 | get: { 65 | cmdDesc: '获取 leo config 参数', 66 | arguments: '', 67 | }, 68 | delete: { 69 | cmdDesc: '删除 leo config 参数', 70 | arguments: '', 71 | }, 72 | list: { 73 | cmdDesc: '查看 leo config 配置', 74 | }, 75 | }, 76 | helpTexts: [ 77 | { 78 | position: 'after', 79 | text: '欢迎使用 leo', 80 | }, 81 | ], 82 | }, 83 | }, 84 | questions: { 85 | chooseTemplate: [], 86 | init: [ 87 | { 88 | name: 'projectName', 89 | type: 'string', 90 | require: true, 91 | demand: true, 92 | message: '生成项目所在文件夹名称', 93 | validate: (val: string) => { 94 | return val ? true : '请输入生成项目所在文件夹名称'; 95 | }, 96 | }, 97 | { 98 | name: 'useInstall', 99 | type: 'list', 100 | message: '是否需要在初始化后安装依赖?', 101 | choices: [ 102 | { 103 | name: '使用 npm install', 104 | value: 'npm', 105 | }, 106 | { 107 | name: '使用 yarn install', 108 | value: 'yarn', 109 | }, 110 | { 111 | name: '不需要安装', 112 | value: 'custom', 113 | }, 114 | ], 115 | }, 116 | ], 117 | }, 118 | }; 119 | -------------------------------------------------------------------------------- /docs/leorc.md: -------------------------------------------------------------------------------- 1 | # leorc.js 2 | 3 | **以下用 leorc 作为你指定的 rc 文件名** 4 | 5 | `leorc.js` 位于每个项目的根目录下,用于单个项目与脚手架建立联系。 6 | 7 | ## 完整示例 8 | 9 | ```js 10 | module.exports = { 11 | cmd: { 12 | hooks: { 13 | beforeEveryAction: (cli, commandName, args) => { 14 | } 15 | /* 16 | * 如果返回 false 则会拦截后续行为,完全自己代理 17 | */ 18 | shouldActionContinue: (cli, commandName, args) => { 19 | } 20 | afterEveryAction: (commandName, args) => { 21 | } 22 | } 23 | }, 24 | 25 | testAction: (options) => {}, 26 | lintAction: (options) => {}, 27 | isPrePublishParallel: false, // 发布之前test,lint,build是否要并行 28 | builder: { 29 | name: 'xxx-builder', 30 | version: '0.0.1', 31 | hooks: { 32 | beforeDev: (Builder) => {}, 33 | afterDev: (Builder) => {}, 34 | 35 | beforeBuild: (Builder) => {}, 36 | afterBuild: (Builder) => {}, 37 | } 38 | }, 39 | publisher: { 40 | name: 'test-publisher', 41 | version: '0.0.1', 42 | hooks: { 43 | beforePublish: (Publisher) => {}, 44 | afterPublish: (Publisher) => {}, 45 | } 46 | } 47 | } 48 | 49 | ``` 50 | 51 | ## 字段解释 52 | 53 | ### cmd 54 | 55 | 扩展自定义命令并提供了钩子函数。运行时间如下: 56 | 57 | ![cmd-hooks运行时间](./images/cmd-hooks.png) 58 | 59 | #### cmd.beforeEveryAction 60 | 61 | 命令执行动作 action 之前。 62 | 63 | #### cmd.shouldActionContinue 64 | 65 | 命令执行`hooks.beforeEveryAction`之后,执行命令动作 action 之前。若`shouldActionContinue`返回`false`,则会拦截后续行为,完全自己代理。 66 | 67 | #### cmd.afterEveryAction 68 | 69 | 执行命令动作 action 之后。 70 | 71 | ### testAction 72 | 73 | 用于不同业务需求的测试点不同,leo 没有做统一的测试,而是通过项目模板针对业务需求自行提供测试内容 74 | 75 | ```typescript 76 | type testAction = () => Promise; 77 | ``` 78 | 79 | **必须返回 `boolean` 用来为 leo 判断是否通过测试** 80 | 81 | ### lintAction 82 | 83 | 用于不同团队的 lint 习惯不同,leo 没有做统一的 lint 校验,而是通过模板提供 lint 检测 84 | 85 | ```typescript 86 | type lintAction = () => Promise; 87 | ``` 88 | 89 | **必须返回 `boolean` 用来为 leo 判断是否通过 lint** 90 | 91 | ### isPrePublishParallel 92 | 93 | 在 `publish` 指令执行时,是否并行执行 `lint`,`test`,`build` 操作 94 | 95 | 默认:false 96 | 97 | ### builder 98 | 99 | builder 用来声明模板的构建工具 100 | 101 | builder 可以根据自身业务需求自行开发,[开发规范](http://doc.jd.com/feb-book/leo/advance/builder.html) 102 | 103 | #### builder.name 104 | 105 | 构建器名称 106 | 107 | #### builder.version 108 | 109 | 构建器版本 110 | 111 | #### builder.options 112 | 113 | 构建器可自定义更改的配置项,具体配置需查看相应构建器文档 114 | 115 | #### builder.hooks.beforeDev 116 | 117 | 生命周期:执行 dev 操作前 118 | 119 | #### builder.hooks.afterDev 120 | 121 | 生命周期:执行 dev 操作后 122 | 123 | #### builder.hooks.beforeBuild 124 | 125 | 生命周期:执行 build 操作前 126 | 127 | #### builder.hooks.afterBuild 128 | 129 | 生命周期:执行 build 操作后 130 | 131 | ### publisher 132 | 133 | #### publisher.options 134 | 135 | 发布器可自定义更改的配置项,具体配置需查看相应构建器文档 136 | 137 | - [npm](https://coding.jd.com/leo-publishers/npm-publisher/) 138 | - [ihub](https://coding.jd.com/leo-publishers/ihub-publisher/) 139 | - [pubfree](https://coding.jd.com/leo-publishers/pubfree-publisher/) 140 | 141 | #### publisher.hooks.beforePublish 142 | 143 | 生命周期:执行发布操作前 144 | 145 | #### publisher.hooks.afterPublish 146 | 147 | 生命周期:执行发布操作后 148 | -------------------------------------------------------------------------------- /packages/leo-utils/src/loadPkg.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import log from './log'; 3 | 4 | interface IHooks { 5 | beforeAllInstall?: (name: string, version?: string) => void; 6 | allInstallSuccess?: (name: string, version?: string) => void; 7 | allInstallFail?: (name: string, version?: string) => void; 8 | afterJudgePkgExists?: (Exists: boolean) => void; 9 | } 10 | 11 | interface ILoadProps extends IHooks { 12 | version?: string; 13 | dev?: boolean; 14 | private?: boolean; 15 | beforeInstall?: (name: string, version?: string) => void; 16 | installSuccess?: (name: string, version?: string) => void; 17 | installFail?: (name: string, version?: string) => void; 18 | } 19 | 20 | const getPkgName = (name: string, version?: string) => `${name}${version ? `@${version}` : ''}`; 21 | 22 | class LoadPkg { 23 | isDev: boolean; 24 | root: string; 25 | devRoot: string; 26 | hooks?: IHooks; 27 | 28 | constructor() { 29 | this.hooks = { 30 | beforeAllInstall: (name: string, version: string) => 31 | log.warn(`未安装 ${getPkgName(name, version)}`), 32 | allInstallSuccess: (name: string, version: string) => 33 | log.success(`${getPkgName(name, version)} 加载成功`), 34 | allInstallFail: (name: string, version: string) => 35 | log.error(`${getPkgName(name, version)} 加载失败`), 36 | }; 37 | this.load = this.load.bind(this); 38 | } 39 | 40 | async load(name: string, versionOrOptions?: ILoadProps | string) { 41 | this.init(); 42 | let version; 43 | let options; 44 | 45 | if (versionOrOptions && typeof versionOrOptions === 'string') { 46 | version = versionOrOptions; 47 | } 48 | 49 | if (versionOrOptions && typeof versionOrOptions === 'object') { 50 | options = versionOrOptions; 51 | version = versionOrOptions.version; 52 | } 53 | 54 | const isDev = options?.dev === undefined ? this.isDev : options?.dev; 55 | 56 | try { 57 | const isPrivate = !!options?.private; 58 | 59 | const { pkgPath, storeDir } = this.getPkgPath(name, { version, isDev, isPrivate }); 60 | 61 | const isPkgExists = this.isPkgExists(pkgPath); 62 | 63 | options?.afterJudgePkgExists?.(isPkgExists); 64 | 65 | if (!isPkgExists) { 66 | this.hooks?.beforeAllInstall?.(name, version); 67 | options?.beforeInstall?.(name, version); 68 | 69 | await this.installPkg(name, version || '', storeDir); 70 | 71 | options?.installSuccess?.(name, version); 72 | this.hooks?.allInstallSuccess?.(name, version); 73 | } 74 | 75 | const x = require(pkgPath); 76 | 77 | return x.default || x; 78 | } catch (e) { 79 | this.hooks?.allInstallFail?.(name, version); 80 | 81 | options?.installFail?.(name, version); 82 | 83 | // 抛出错误后由调用方捕获错误信息的调用栈会更改,所以在这里先log 84 | if (isDev) { 85 | log.error(e); 86 | } 87 | 88 | return Promise.reject(new Error(`${((e as unknown) as Error).message}`)); 89 | } 90 | } 91 | 92 | // 返回包绝对地址 93 | private getPkgPath = ( 94 | name: string, 95 | options: { 96 | version?: string; 97 | isDev?: boolean; 98 | isPrivate?: boolean; 99 | }, 100 | ): { 101 | pkgPath: string; 102 | storeDir: string; 103 | } => { 104 | const { version, isDev, isPrivate } = options; 105 | // dev模式下,包目录读取本机全局包,用于 npm link 后本地调试 106 | if (isDev) { 107 | return { 108 | pkgPath: path.resolve(this.devRoot, `./${name}`), 109 | storeDir: this.devRoot, 110 | }; 111 | } 112 | 113 | const storeDir = isPrivate ? `${this.root}/${name}/node_modules` : `${this.root}/node_modules`; 114 | const pkgName = version ? `_${name.replace('/', '_')}@${version}@${name}` : name; 115 | 116 | return { 117 | pkgPath: `${storeDir}/${pkgName}`, 118 | storeDir, 119 | }; 120 | }; 121 | 122 | // 安装 npm 依赖 123 | private async installPkg(name: string, version: string, storeDir?: string): Promise { 124 | const npmi = require('npminstall'); 125 | 126 | await npmi({ 127 | production: true, 128 | root: path.resolve(storeDir, '../'), 129 | pkgs: [{ name, version }], 130 | }); 131 | } 132 | 133 | private isPkgExists(pkgPath: string): boolean { 134 | try { 135 | require.resolve(pkgPath); 136 | return true; 137 | } catch (e) { 138 | return false; 139 | } 140 | } 141 | 142 | private init() { 143 | const globalDirectories = require('global-dirs'); 144 | 145 | // 环境变量由 leo core 控制 146 | this.root = process.env.__NODEMODULEPATH || process.cwd(); 147 | this.devRoot = 148 | process.env.__USEYARN === 'true' 149 | ? globalDirectories.yarn.packages 150 | : globalDirectories.npm.packages; 151 | this.isDev = process.env.__ISDEV === 'true'; 152 | this.load = this.load.bind(this); 153 | } 154 | } 155 | 156 | const loadPkg = new LoadPkg().load; 157 | 158 | export default loadPkg; 159 | -------------------------------------------------------------------------------- /docs/meta.md: -------------------------------------------------------------------------------- 1 | # meta.js 2 | 3 | 用于控制模版最终渲染和生成的项目文件。 4 | 5 | ## 完整示例 6 | 7 | ```js 8 | module.exports = { 9 | // generator钩子函数 10 | hooks: { 11 | beforeGenerate: (generator: Generator) => { 12 | // warn:不建议return true, 如果返回值为true将终止后续流程 13 | console.log('template beforeGenerate is run', generator); 14 | // return true 15 | }, 16 | afterGenerate: (generator: Generator) => { 17 | console.log('template afterGenerate is run', generator); 18 | }, 19 | /** 20 | * 参考项目https://github.com/segmentio/metalsmith 21 | * 以及@types/metalsmith 22 | * 渲染占位符时的hook,如果配置了该函数,将由你自己接管渲染占位符 23 | */ 24 | renderTemplatePlaceholder: (generator: Generator) => { 25 | return (files: IFiles, metalsmith: Metalsmith, done: Function) => {}; 26 | }, 27 | /** 28 | * 最终渲染文件的hook,理论上可以做任何事包括渲染占位符 29 | */ 30 | renderTemplateFile: (generator: Generator) => { 31 | return (files: IFiles, metalsmith: Metalsmith, done: Function) => {}; 32 | } 33 | }, 34 | // handlebars的helpers扩展函数 35 | // https://handlebarsjs.com/api-reference/runtime.html#handlebars-registerhelper-name-helper 36 | helpers: { 37 | if_equal(v1, v2, opts) { 38 | return v1 === v2 39 | ? opts.fn(this) 40 | : opts.inverse(this) 41 | }, 42 | } 43 | // 交互式问题 44 | // 参考inquirer https://github.com/SBoudrias/Inquirer.js 45 | prompts: [ 46 | { 47 | name: 'projectDesc', 48 | type: 'string', 49 | require: false, 50 | default: 'A test description', 51 | message: 'A project description' 52 | }, 53 | { 54 | name: 'build', 55 | type: 'list', 56 | message: '构建方式', 57 | choices: [ 58 | { 59 | name: '快速构建方案', 60 | value: 'platfomA', 61 | short: 'platfomA', 62 | }, 63 | { 64 | name:'稳定构建方案', 65 | value: 'platfomB', 66 | short: 'platfomB', 67 | }, 68 | ] 69 | }, 70 | { 71 | name: 'router', 72 | type: 'confirm', 73 | message: '是否安装vue-router?' 74 | }, 75 | { 76 | // 当用户的build的选项为platfomA时执行此问题 77 | when: (answer) => { return answer.build == 'platfomA' }, 78 | name: 'unit', 79 | type: 'confirm', 80 | message: '是否需要单元测试' 81 | } 82 | ], 83 | // 根据用户对相关name的回答,进行文件的过滤删除 84 | // 遵循glob的语法 https://rgb-24bit.github.io/blog/2018/glob.html 85 | filterFilesMap: { 86 | 'test/unit/**/*': 'unit', 87 | 'src/router/**/*': 'router' 88 | } 89 | // 编译白名单,考虑到handlebars编译{{ xxx }}的值时,可能会编译到一些不需要编译的文件导致报错 90 | // 将需要handlebars需要编译的文件写在compileWhiteList中(默认会编写leorc.js和package.json,无需额外添加leorc.js和package.json) 91 | // 若不传compileWhiteList或为空时,默认编译所有文件 92 | // 遵循glob的语法 https://rgb-24bit.github.io/blog/2018/glob.html 93 | compileWhiteList: [ 94 | 'src/router/**/*', 95 | 'test/unit/**/*', 96 | ] 97 | } 98 | ``` 99 | 100 | ## 字段解释 101 | 102 | ### prompts 103 | 104 | **类型:** Array 105 | 106 | **定义:** 模板配置询问阶段,模板开发者可设置一些选项供使用者选择,后期渲染阶段会根据用户的选择来控制模板的生成。 107 | 108 | **规范:** 由于模板配置询问功能基于[Inquirer.js](https://www.npmjs.com/package/inquirer)实现,所以 prompts 书写的规则需遵循[Inquirer.js](https://www.npmjs.com/package/inquirer)规范。 109 | 110 | **示例:** 111 | 112 | ```js 113 | prompts: [ 114 | { 115 | name: 'build', 116 | type: 'list', 117 | message: '构建方式', 118 | choices: [ 119 | { 120 | name: '快速构建方案', 121 | value: 'platfomA', 122 | short: 'platfomA', 123 | }, 124 | { 125 | name:'稳定构建方案', 126 | value: 'platfomB', 127 | short: 'platfomB', 128 | }, 129 | ] 130 | } 131 | ... 132 | ] 133 | ``` 134 | 135 | ### filterFilesMap 136 | 137 | **类型:** Object 138 | 139 | **定义:** 获取模板配置询问用户的配置选择,过滤掉不需要生成的文件。 140 | 141 | ::: tip 142 | `filterFilesMap`是由文件名作为`key`,`prompts`问题中的 name 字段作为`value`的一个对象,例如: 143 | 144 | ```js 145 | prompts: [ 146 | { 147 | name: 'router', 148 | type: 'confirm', 149 | message: '是否安装react-router?' 150 | } 151 | ] 152 | filterFilesMap: { 153 | 'src/router/**/*': 'router' 154 | } 155 | ``` 156 | 157 | 若用户 router 选项选择了 N,则最终不会生成 src 下 router 及 router 目录下所有文件。 158 | ::: 159 | 160 | **规范:** 文件名的书写规范遵循[glob 的语法](https://rgb-24bit.github.io/blog/2018/glob.html) 161 | 162 | ### helpers 163 | 164 | **类型:** Object 165 | 166 | **定义:** 支持`模版开发者`在开发中使用 Handlebars 的 helpers 控制文件内容的生成。 167 | 168 | **规范:** helpers 函数规范遵循[Handlebars 的 registerHelper](https://handlebarsjs.com/api-reference/runtime.html#handlebars-registerhelper-name-helper)规则。 169 | 170 | **示例:** 171 | 172 | ```js 173 | helpers: { 174 | if_equal(v1, v2, opts) { 175 | return v1 === v2 176 | ? opts.fn(this) 177 | : opts.inverse(this) 178 | }, 179 | } 180 | ``` 181 | 182 | ### compileWhiteList 183 | 184 | **类型:** Array 185 | 186 | **定义:** 模版编译白名单。 187 | 188 | ::: v-pre 189 | ::: tip 190 | 由于`handlebars`是根据双花括号`{{}}`去匹配并渲染变量。当在`react`或者`vue`的项目中,其内部语法 `{{variable}}`与`handlebars`语法`{{replaceName}}`冲突,若希望`{{variable}}`不被`handlebars`编译,可在变量前添加转义符`{{variable}}`写作`\{{variable}}`来防止`handlebars`编译变量。 191 | 192 | 除了上述的方法可以阻止 handlebars 编译,还可以将需要编译文件写入`compileWhiteList`中,这样只会编译`compileWhiteList`中文件,由于 generator 会默认编译 leorc.js 和 package.json 文件所以无需额外填写。若`compileWhiteList`为空默认编译项目所有文件。 193 | ::: 194 | 195 | **示例:** 196 | 197 | ```js 198 | compileWhiteList: ['src/router/**/*', 'test/unit/**/*']; 199 | // 实际编译文件有'src/router/**/*', 'test/unit/**/*', 'package.json', 'leorc.js' 200 | ``` 201 | 202 | **规范:** 文件名的书写规范遵循[glob 的语法](https://rgb-24bit.github.io/blog/2018/glob.html) 203 | 204 | ### hooks 205 | 206 | 快速理解不同 hooks 执行的时机可参考[此处](http://doc.jd.com/feb-book/leo/advance/generator.html)。 207 | 208 | #### hook.beforeGenerate 209 | 210 | **类型:** Function 211 | 212 | **定义:** 此函数为 Generator 的前置 hook 函数,此函数不建议 return true, 如果返回值为 true 将终止后续流程。 213 | 214 | #### hooks.afterGenerate 215 | 216 | **类型:** Function 217 | 218 | **定义:** 此函数为 Generator 的后置 hook 函数。 219 | 220 | #### hooks.renderTemplatePlaceholder 221 | 222 | **类型:** Function 223 | 224 | **定义:** 渲染占位符时的 hook 函数,如果配置了该函数,将由模版开发者模版控制占位符的渲染。 225 | 226 | #### hooks.renderTemplateFile 227 | 228 | **类型:** Function 229 | 230 | **定义:** 最终渲染文件的 hook,此函数为最终文件的生成函数。 231 | 232 | **示例:** 233 | 234 | ```js 235 | hooks: { 236 | beforeGenerate: (generator: Generator) => {}, 237 | afterGenerate: (generator: Generator) => {}, 238 | renderTemplatePlaceholder: (generator: Generator) => { 239 | return (files: IFiles, metalsmith: Metalsmith, done: Function) => {}; 240 | }, 241 | renderTemplateFile: (generator: Generator) => { 242 | return (files: IFiles, metalsmith: Metalsmith, done: Function) => {}; 243 | } 244 | }, 245 | ``` 246 | -------------------------------------------------------------------------------- /packages/leo-cli/src/CLI.ts: -------------------------------------------------------------------------------- 1 | import commander from 'commander'; 2 | import { ICommandSettingAlone, ICommandSettings, IActionArgs } from './interface'; 3 | import get from 'lodash.get'; 4 | import merge from 'lodash.merge'; 5 | import { log as leoLog, loadPkg } from '@jdfed/leo-utils'; 6 | import { getUnexpectedOptions, rebuildCommanderArgs } from './utils'; 7 | 8 | const log = leoLog.scope('cli'); 9 | 10 | type IActionHandler = ( 11 | commandName: string, 12 | args: IActionArgs, 13 | extendsCommandAction?: (params: { 14 | args: IActionArgs; 15 | subCommandName: string; 16 | [key: string]: any; 17 | }) => void, 18 | ) => void | Promise; 19 | 20 | const commandFactory = (params: { 21 | program: commander.Command; 22 | name: string; 23 | setting: ICommandSettingAlone; 24 | actionCB: (commandName: string) => IActionHandler; 25 | // name 用于定义命令的名字,actionName 用于定义 action 回调中命令名的传参,默认使用 name(为了配合 publish.name) 26 | actionName?: string; 27 | }) => { 28 | const { program, name, setting, actionCB, actionName } = params; 29 | const c = program.command(name); 30 | 31 | if (setting.arguments) { 32 | c.arguments(setting.arguments); 33 | } 34 | 35 | c.description(setting.cmdDesc, setting.argumentsDesc || {}); 36 | 37 | // 允许添加任意参数而不报错,默认为 true 38 | if (setting.allowUnknownOption !== false) { 39 | c.allowUnknownOption(); 40 | } 41 | 42 | setting.options && 43 | setting.options.forEach((option) => { 44 | c.option(...option); 45 | }); 46 | 47 | if (setting.helpTexts) { 48 | setting.helpTexts.forEach((helpInfo) => { 49 | c.addHelpText(helpInfo.position, helpInfo.text); 50 | }); 51 | } 52 | 53 | // 如果有子命令,则不在当前命令上添加 action 54 | if (setting.subCommands) { 55 | Object.keys(setting.subCommands).forEach((subCommandName) => { 56 | commandFactory({ 57 | program: c, 58 | name: subCommandName, 59 | setting: setting.subCommands[subCommandName], 60 | actionCB, 61 | actionName: `${name}.${subCommandName}`, 62 | }); 63 | }); 64 | 65 | return; 66 | } 67 | 68 | c.action(actionCB(actionName || name)); 69 | }; 70 | 71 | class CLI { 72 | commandSettings: ICommandSettings; 73 | program: commander.Command; 74 | leoConfig: { [key: string]: any }; 75 | leoRC: { [key: string]: any }; 76 | actionHandler: IActionHandler; 77 | // DOCUMENT 78 | hooks: { 79 | beforeEveryAction: (cli: CLI, commandName: string, args: any[]) => void | Promise; 80 | /* 81 | * 如果返回 false 则会拦截后续行为,完全自己代理 82 | * 比如自己创建的命令,需要return true 83 | */ 84 | shouldActionContinue: ( 85 | cli: CLI, 86 | commandName: string, 87 | args: any[], 88 | ) => boolean | Promise; 89 | afterEveryAction: (cli: CLI, commandName: string, args: any[]) => void; 90 | } = { 91 | beforeEveryAction: null, 92 | shouldActionContinue: null, 93 | afterEveryAction: null, 94 | }; 95 | version: string; 96 | // 添加帮助信息 https://github.com/tj/commander.js#custom-help 97 | helpTexts: Array<{ position: 'beforeAll' | 'afterAll'; text: string }>; 98 | 99 | constructor(params: { 100 | leoConfig: { [key: string]: any }; 101 | leoRC: { [key: string]: any }; 102 | commands: ICommandSettings; 103 | actionHandler: IActionHandler; 104 | version: string; 105 | helpTexts: Array<{ position: 'beforeAll' | 'afterAll'; text: string }>; 106 | virtualPath: { [key: string]: string }; 107 | }) { 108 | this.commandSettings = params.commands; 109 | this.program = new commander.Command(); 110 | this.actionHandler = params.actionHandler; 111 | this.version = params.version; 112 | this.helpTexts = params.helpTexts || []; 113 | this.leoConfig = params.leoConfig; 114 | this.leoRC = params.leoRC; 115 | 116 | // 从 leoRC 中获取配置的 hooks 117 | this.hooks = merge({}, this.hooks, get(this.leoRC, 'cli.hooks', {})); 118 | } 119 | 120 | async start() { 121 | this.program.version(this.version); 122 | await this.createCLI(); 123 | } 124 | 125 | async createCLI() { 126 | const extendsCommands = await this.getExtendsCommands(); 127 | 128 | this.commandSettings = merge({}, extendsCommands, this.commandSettings); 129 | 130 | // commandSettings 优先级高于 extendsCommands 131 | // 避免自定义指令覆盖leo原生指令 132 | Object.keys(this.commandSettings).forEach((name) => { 133 | commandFactory({ 134 | program: this.program, 135 | name, 136 | setting: this.commandSettings[name], 137 | actionCB: (commandName: string) => async (...args: any[]) => { 138 | this.hooks.beforeEveryAction && 139 | (await this.hooks.beforeEveryAction(this, commandName, args)); 140 | 141 | if ( 142 | this.hooks.shouldActionContinue && 143 | (await this.hooks.shouldActionContinue(this, commandName, args)) === false 144 | ) { 145 | return; 146 | } 147 | 148 | const rebuildedArgs = rebuildCommanderArgs(args); 149 | 150 | await this.actionHandler( 151 | commandName, 152 | { 153 | ...rebuildedArgs, 154 | unexpectedOptions: getUnexpectedOptions(rebuildedArgs.command.options), 155 | }, 156 | get(this.commandSettings[name], 'action'), 157 | ); 158 | 159 | this.hooks.afterEveryAction && this.hooks.afterEveryAction(this, commandName, args); 160 | }, 161 | }); 162 | }); 163 | 164 | this.helpTexts.forEach((helpInfo) => { 165 | this.program.addHelpText(helpInfo.position, helpInfo.text); 166 | }); 167 | 168 | this.program.parse(process.argv); 169 | } 170 | 171 | // 加载扩展指令,如果加载出错,仅展示错误信息,不中断主流程进行 172 | async loadExtendsCommands(pkgName: string, version: string) { 173 | return new Promise((resolve) => { 174 | loadPkg(pkgName, { 175 | version, 176 | private: true, 177 | }) 178 | .then((pkg: any) => { 179 | resolve(pkg); 180 | }) 181 | .catch((e: Error) => { 182 | log.error(e.message); 183 | log.error(`无法使用 ${pkgName}@${version} 提供的扩展命令,请联系开发者解决`); 184 | resolve(null); 185 | }); 186 | }); 187 | } 188 | async getExtendsCommands() { 189 | // 加载声明的扩展命令并处理为同 config.commands 中一样的格式 190 | // leorc 的命令声明优先级高于 leo config 191 | const extendsCommands = ( 192 | await Promise.all( 193 | Object.entries({ 194 | ...get(this.leoConfig, 'cmds', {}), 195 | ...get(this.leoRC, 'cmds', {}), 196 | }).map((commandInfo) => { 197 | const [pkgName, version] = commandInfo; 198 | return this.loadExtendsCommands(pkgName, version as string); 199 | }), 200 | ) 201 | ).filter((v) => v); 202 | 203 | return extendsCommands.reduce( 204 | (commandStore: { [key: string]: any }, extend: { [key: string]: any }) => { 205 | commandStore[extend.name] = extend; 206 | delete commandStore[extend.name].name; 207 | return commandStore; 208 | }, 209 | {}, 210 | ); 211 | } 212 | } 213 | 214 | export default CLI; 215 | -------------------------------------------------------------------------------- /packages/leo-generator/src/Generator.ts: -------------------------------------------------------------------------------- 1 | import merge from 'lodash.merge'; 2 | import get from 'lodash.get'; 3 | import fs from 'fs-extra'; 4 | import Metalsmith from 'metalsmith'; 5 | import inquirer from 'inquirer'; 6 | import match from 'minimatch'; 7 | import consolidate from 'consolidate'; 8 | import Handlebars from 'handlebars'; 9 | import multimatch from 'multimatch'; 10 | import simpleGit from 'simple-git'; 11 | import { log as leoLog } from '@jdfed/leo-utils'; 12 | import { IFiles, IMetaData, IFilterFilesMap, IHelper } from './interface'; 13 | 14 | const log = leoLog.scope('generator'); 15 | 16 | // 这个库没有@types发布接口 17 | const downloadGitRepo = require('download-git-repo'); 18 | 19 | type metalsmithCB = (files: IFiles, metalsmith: Metalsmith, done: Function) => void; 20 | class Generator { 21 | // 虚拟目录路径,在linux中一般都是 ~/.leo/templates 22 | cachePath: string; 23 | // ~/.leo/templates/${templateName} 24 | localTemplatePath: string; 25 | // 远程模板地址 26 | repoRemoteAddress: string; 27 | // 当前虚拟目录模板下的template文件夹 28 | currentTemplatePath: string; 29 | // 当前虚拟目录模板下的meta.js 30 | currentMetaPath: string; 31 | 32 | templateName: string; 33 | 34 | projectName: string; 35 | projectPath: string; 36 | 37 | answer: Record; 38 | officialEnv: Record; 39 | 40 | metalsmith: Metalsmith; 41 | 42 | // 默认编译文件 43 | defaultCompileFiles: string[]; 44 | 45 | leoRC: { [key: string]: any }; 46 | leoConfig: { [key: string]: any }; 47 | metaConfig: { [key: string]: any }; 48 | 49 | hooks: { 50 | // 返回true表示接管构建任务,不再执行原有逻辑。发生在下载模板完成后。 51 | beforeGenerate: (generator: Generator) => Promise; 52 | afterGenerate: (generator: Generator) => Promise; 53 | /** 54 | * 参考项目https://github.com/segmentio/metalsmith 55 | * 以及@types/metalsmith 56 | * 渲染占位符时的hook,如果实现讲自己接管渲染占位符 57 | */ 58 | renderTemplatePlaceholder: ( 59 | generator: Generator, 60 | ) => (files: IFiles, metalsmith: Metalsmith, done: Function) => void; 61 | /** 62 | * 最终渲染文件的hook,理论上可以做任何事包括渲染占位符 63 | */ 64 | renderTemplateFile: ( 65 | generator: Generator, 66 | ) => (files: IFiles, metalsmith: Metalsmith, done: Function) => void; 67 | } = { 68 | beforeGenerate: null, 69 | afterGenerate: null, 70 | renderTemplatePlaceholder: null, 71 | renderTemplateFile: null, 72 | }; 73 | 74 | options: { 75 | useCache: false; 76 | repo: ''; 77 | }; 78 | 79 | constructor(params: { 80 | templateName: string; 81 | projectName: string; 82 | leoConfig: { [key: string]: any }; 83 | options: { [key: string]: any }; 84 | cachePath: string; // 缓存文件存放的路径 85 | }) { 86 | this.templateName = params.templateName; 87 | this.projectName = params.projectName; 88 | this.cachePath = params.cachePath; 89 | this.leoConfig = params.leoConfig; 90 | 91 | // 模版的git地址 92 | this.repoRemoteAddress = `${this.leoConfig.gitTemplateGroupURL}/${this.templateName}-template.git`; 93 | // 本地模版的地址 94 | this.localTemplatePath = `${this.cachePath}/${this.templateName}-template`; 95 | 96 | this.currentTemplatePath = `${this.localTemplatePath}/template`; 97 | 98 | this.currentMetaPath = `${this.localTemplatePath}/meta.js`; 99 | 100 | this.defaultCompileFiles = ['package.json', this.leoConfig.rcFileName]; // 默认是 leorc.js 101 | 102 | this.answer = {}; 103 | 104 | this.officialEnv = {}; 105 | 106 | this.metalsmith = null; 107 | 108 | // 合并命令行设置的参数 109 | this.options = merge({}, this.options, params.options); 110 | } 111 | 112 | async start() { 113 | try { 114 | await this.prepare(); 115 | 116 | this.answer = await this.askQuestions(get(this.metaConfig, 'prompts', [])); 117 | this.officialEnv = await this.getGeneratorEnv(); 118 | 119 | // 若beforeGenerate为true直接return,相当于模版方自己接管 120 | if (this.hooks.beforeGenerate && (await this.hooks.beforeGenerate(this)) === true) { 121 | return; 122 | } 123 | 124 | await this.generate(); 125 | await this.initGitRepo(this.options.repo); 126 | 127 | // 构建结束之后执行afterGenerate 128 | this.hooks.afterGenerate && (await this.hooks.afterGenerate(this)); 129 | } catch (e) { 130 | log.error('generator', (e as Error).message); 131 | process.exit(1); 132 | } 133 | } 134 | 135 | async getGeneratorEnv(): Promise<{ [key: string]: any }> { 136 | // 官方系统环境变量 137 | return { 138 | $projectName: this.projectName, 139 | }; 140 | } 141 | 142 | async prepare() { 143 | const isUseCache = await this.useLocalCacheTemplate(); 144 | if (!isUseCache) { 145 | await this.downloadTemplate(); 146 | } 147 | 148 | // 判断是否有template的文件夹 149 | const hasCurTemplateFolder = await fs.pathExists(this.currentTemplatePath); 150 | if (!hasCurTemplateFolder) { 151 | throw new Error('模版下不存在template目录'); 152 | } 153 | 154 | const projectPath = `${process.cwd()}/${this.projectName}`; 155 | const currentHasProject = await fs.pathExists(projectPath); 156 | 157 | if (this.leoConfig.isDebugger) { 158 | log.debug('generator.start', 'projectPath:', projectPath); 159 | } 160 | 161 | if (currentHasProject) { 162 | throw new Error('当前目录下已存在相同目录名'); 163 | } 164 | this.projectPath = projectPath; 165 | 166 | // 不再获取leorc,改为获取模版的 meta.js 的配置 167 | this.metaConfig = await this.getTemplateMetaConfig(); 168 | // 从 meta.js 中获取配置的 hooks 169 | merge(this.hooks, get(this.metaConfig, 'hooks', {})); 170 | } 171 | 172 | async generate(): Promise { 173 | this.metalsmith = Metalsmith(this.currentTemplatePath); 174 | // 获取并注册用户的自定义handlebars的helper函数 175 | const customHelpers = get(this.metaConfig, 'helpers', {}); 176 | if (Object.keys(customHelpers).length > 0) { 177 | this.registerCustomHelper(customHelpers); 178 | } 179 | 180 | Object.assign(this.metalsmith.metadata(), this.officialEnv, this.answer, customHelpers); 181 | 182 | const filterFilesMap = get(this.metaConfig, 'filterFilesMap', null); 183 | 184 | if (filterFilesMap) { 185 | this.metalsmith = this.metalsmith.use(this.filterFiles(filterFilesMap)); // 删除文件 186 | } 187 | 188 | const compileWhiteList = get(this.metaConfig, 'compileWhiteList', null); 189 | 190 | let renderTemplatePlaceholder = this.renderTemplatePlaceholder(compileWhiteList); 191 | 192 | if (this.hooks.renderTemplatePlaceholder) { 193 | renderTemplatePlaceholder = this.hooks.renderTemplatePlaceholder(this); 194 | } 195 | this.metalsmith = this.metalsmith.use(renderTemplatePlaceholder); // 渲染占位符 196 | 197 | if (this.hooks.renderTemplateFile) { 198 | this.metalsmith = this.metalsmith.use(this.hooks.renderTemplateFile(this)); 199 | } 200 | 201 | return new Promise((resolve, reject) => { 202 | this.metalsmith 203 | .source('.') 204 | .destination(this.projectPath) 205 | .clean(false) 206 | .build(async (err: Error) => { 207 | if (err) { 208 | reject(err); 209 | return; 210 | } 211 | log.success('Generator构建完成'); 212 | resolve(0); 213 | }); 214 | }); 215 | } 216 | 217 | /** 218 | * 询问用户 219 | * @params prompts 220 | * @return {Promise} 保存用户回答的答案 221 | */ 222 | async askQuestions(prompts: any[]): Promise<{ [key: string]: any }> { 223 | return inquirer.prompt(prompts); 224 | } 225 | 226 | /** 227 | * 根据用户的回答的结果过滤相关文件 228 | * @params {IFilterFilesMap} filterFilesMap 229 | * @return {metalsmithCB} 执行done()回调表示执行完毕 230 | */ 231 | filterFiles(filterFilesMap: IFilterFilesMap): metalsmithCB { 232 | /* eslint no-param-reassign: "error" */ 233 | 234 | return (files: IFiles, metalsmith: Metalsmith, done: Function) => { 235 | // 如果不需要过滤文件直接终止 236 | if (!filterFilesMap) { 237 | return done(); 238 | } 239 | 240 | const metaData: IMetaData = metalsmith.metadata(); 241 | const fileNameList = Object.keys(files); 242 | const filtersList = Object.keys(filterFilesMap); 243 | if (this.leoConfig.isDebugger) { 244 | log.debug('generator.filterFiles.before', Object.keys(files)); 245 | } 246 | // 根据用户所选择的配置所对应的文件名或文件夹名进行匹配 247 | filtersList.forEach((filter) => { 248 | fileNameList.forEach((filename) => { 249 | // 匹配filtersList里面需要过滤的文件,(文件夹及文件的匹配需要通过minimatch库进行匹配,无法直接通过数组的方法直接匹配) 250 | // 例如src/router/**/*/代表匹配router下所有文件 251 | // dot为true表示可以匹配.开头的文件,例如.eslintrc.js等 252 | if (match(filename, filter, { dot: true })) { 253 | const conditionKey = filterFilesMap[filter]; 254 | // 对用户不需要的配置的文件进行删除处理 255 | if (!metaData[conditionKey]) { 256 | delete files[filename]; 257 | } 258 | } 259 | }); 260 | }); 261 | if (this.leoConfig.isDebugger) { 262 | log.debug('generator.filterFiles.after', Object.keys(files)); 263 | } 264 | // 终止回掉 265 | done(); 266 | }; 267 | } 268 | 269 | /** 270 | * 渲染template文件, 若有renderTemplatePlaceholder钩子函数则不执行官方的渲染 271 | * @return {metalsmithCB} 执行done()回调表示执行完毕 272 | */ 273 | renderTemplatePlaceholder(compileWhiteList: string[] | null): metalsmithCB { 274 | /* eslint no-param-reassign: "error" */ 275 | 276 | return (files: IFiles, metalsmith: Metalsmith, done: Function) => { 277 | const keys = Object.keys(files); 278 | const metalsmithMetadata = metalsmith.metadata(); 279 | // 判断模版是否有白名单 280 | const hasWhiteList = this.existCompileWhiteList(compileWhiteList); 281 | // 循环查询有模版语法的的文件,替换相关handlebars语法的地方,然后生成新的文件 282 | keys.forEach((fileName) => { 283 | const str = files[fileName].contents.toString(); 284 | const shouldCompileFile = 285 | this.isDefaultCompileFile(fileName) || 286 | this.matchCompileFile(hasWhiteList, fileName, compileWhiteList); 287 | // 匹配有handlebars语法的文件 288 | if (shouldCompileFile && /{{([^{}]+)}}/g.test(str)) { 289 | consolidate.handlebars.render(str, metalsmithMetadata, (err: Error, res: string) => { 290 | if (err) { 291 | throw new Error(`模版文件${fileName}渲染出现异常`); 292 | } 293 | files[fileName].contents = Buffer.from(res); 294 | }); 295 | } 296 | }); 297 | done(); 298 | }; 299 | } 300 | 301 | /** 302 | * 初始化 git 并关联远程仓库(如传入仓库地址) 303 | * @params 304 | * @return {Promise} 305 | */ 306 | async initGitRepo(repo: string): Promise { 307 | if (!repo) { 308 | return; 309 | } 310 | const git = simpleGit(this.projectPath); 311 | 312 | log.await(`正在关联远程仓库:${repo}`); 313 | await git.init(); 314 | await git.addRemote('origin', repo); 315 | await git.add(['.']); 316 | await git.commit(`leo init from ${this.templateName}-template`); 317 | await git.push('origin', 'master', ['--set-upstream']); 318 | log.success('关联成功'); 319 | } 320 | 321 | /** 322 | * 判断是否使用缓存模版,若使用需判断是否存在,不存在则抛出异常 323 | * @params 324 | * @return {Promise} 325 | */ 326 | private async useLocalCacheTemplate(): Promise { 327 | if (!this.options.useCache) { 328 | return false; 329 | } 330 | const templatePath = `${this.localTemplatePath}`; 331 | const hasTemplatePath = await fs.pathExists(templatePath); 332 | if (!hasTemplatePath) { 333 | throw new Error(`${templatePath} 不存在,无法使用缓存模版`); 334 | } else { 335 | return true; 336 | } 337 | } 338 | 339 | /** 340 | * 下载远程模板 341 | * @return {Promise} 342 | */ 343 | private async downloadTemplate(): Promise { 344 | log.await('generator', `下载远程模板: ${this.templateName}-template`); 345 | const gitPath = this.repoRemoteAddress; 346 | const target = this.localTemplatePath; 347 | 348 | if (this.leoConfig.isDebugger) { 349 | log.debug('generator.downloadTemplate', `gitPath:${gitPath} target:${target}`); 350 | } 351 | 352 | // 删除本地缓存文件后创建一个新的空文件 353 | await fs.remove(target); 354 | await fs.ensureDir(target); 355 | 356 | // 下载仓库中的模板至缓存文件夹 357 | return new Promise((resolve, reject) => { 358 | downloadGitRepo( 359 | `direct:${gitPath}`, 360 | target, 361 | { clone: true, headers: this.leoConfig.gitTemplateLikeQueryHeader || {} }, 362 | (err: Error) => { 363 | if (err) { 364 | return reject(new Error(`下载模板错误:${err}`)); 365 | } 366 | log.success('generator', '模板下载成功'); 367 | return resolve(); 368 | }, 369 | ); 370 | }); 371 | } 372 | 373 | /** 374 | * 获取模版的 meta.js 文件 375 | * @return Promise meta.js的值 376 | */ 377 | private async getTemplateMetaConfig(): Promise { 378 | const templateMetaPath = `${this.localTemplatePath}/meta.js`; 379 | const templatePackagePath = `${this.localTemplatePath}/package.json`; 380 | const hasMeta = await fs.pathExists(templateMetaPath); 381 | const hasPackage = await fs.pathExists(templatePackagePath); 382 | if (hasPackage) { 383 | log.await('====安装template的依赖===='); 384 | await this.installTemplatePkg(); 385 | } 386 | if (hasMeta) { 387 | const metaConfig = require(templateMetaPath); 388 | if (this.leoConfig.isDebugger) { 389 | log.debug('generator.getTemplateMetaConfig', JSON.stringify(metaConfig)); 390 | } 391 | return metaConfig; 392 | } 393 | 394 | return null; 395 | } 396 | 397 | /** 398 | * 判断metaConfig是否存在编译白名单 399 | * @return {boolean} 400 | */ 401 | private existCompileWhiteList(compileWhiteList: string[] | null): boolean { 402 | return !!compileWhiteList && Array.isArray(compileWhiteList) && compileWhiteList.length > 0; 403 | } 404 | 405 | /** 406 | * 判断当前文件是否需要编译 407 | * @return {boolean} 408 | */ 409 | private matchCompileFile( 410 | hasWhiteList: boolean, 411 | fileName: string, 412 | compileWhiteList: string[] | null, 413 | ): boolean { 414 | return ( 415 | !hasWhiteList || 416 | (hasWhiteList && multimatch([fileName], compileWhiteList, { dot: true }).length > 0) 417 | ); 418 | } 419 | 420 | /** 421 | * 默认自动编译的文件 422 | * @return {boolean} 423 | */ 424 | private isDefaultCompileFile(fileName: string): boolean { 425 | return this.defaultCompileFiles.indexOf(fileName) >= 0; 426 | } 427 | /** 428 | * 自动安装模版需要的package.json 429 | * @return {Promise} 430 | */ 431 | private async installTemplatePkg(): Promise { 432 | const npmi = require('npminstall'); 433 | try { 434 | await npmi({ 435 | production: true, 436 | root: this.localTemplatePath, 437 | registry: 'http://registry.m.jd.com/', 438 | }); 439 | } catch (e) { 440 | log.warn('template依赖包安装存在问题', e); 441 | } 442 | log.success('installTemplatePkg', '安装成功'); 443 | } 444 | 445 | /** 446 | * 注册用户自定义handlebars的helper函数 447 | * https://handlebarsjs.com/api-reference/runtime.html#handlebars-registerhelper-name-helper 448 | * @return {void} 449 | */ 450 | private registerCustomHelper(helpers: IHelper): void { 451 | Object.keys(helpers).forEach((key: string) => { 452 | Handlebars.registerHelper(key, helpers[key]); 453 | }); 454 | } 455 | } 456 | 457 | export default Generator; 458 | -------------------------------------------------------------------------------- /packages/leo-core/src/Core.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getLeoRC, 3 | getCorePackageJSON, 4 | getProjectPackageJSON, 5 | runInstall, 6 | extendsProcessEnv, 7 | getRemotePkgTagVersion, 8 | } from './utils'; 9 | import { IRC } from './defaultRC'; 10 | import fs from 'fs-extra'; 11 | import semver from 'semver'; 12 | import set from 'lodash.set'; 13 | import unset from 'lodash.unset'; 14 | import merge from 'lodash.merge'; 15 | import simpleGit from 'simple-git'; 16 | import inquirer from 'inquirer'; 17 | import axios from 'axios'; 18 | import { loadPkg, log as leoLog } from '@jdfed/leo-utils'; 19 | import { getDefaultConfig, IConfig, IActionArgs, localStaticConfig, configStore } from './config'; 20 | import { IBuilder, IVirtualPath, ICommonParams, ILeoCaller } from './interface'; 21 | 22 | export interface IHooks { 23 | beforeStart?: (this: Core) => any; 24 | afterCommandExecute?: (this: Core) => any; 25 | } 26 | 27 | const log = leoLog.scope('core'); 28 | 29 | class Core { 30 | leoRC: IRC; 31 | leoConfig: IConfig; 32 | cli: ILeoCaller = null; 33 | generator: ILeoCaller = null; 34 | builder: IBuilder = null; 35 | rootName: string; 36 | hooks: IHooks = {}; 37 | virtualPath: IVirtualPath; 38 | constructor({ config, hooks = {} }: { config?: IConfig; hooks?: IHooks } = {}) { 39 | this.hooks = hooks; 40 | 41 | this.leoConfig = configStore.init(getDefaultConfig(config)).getConfig() as IConfig; 42 | 43 | // 初始化一些环境变量供leo-utils使用 44 | extendsProcessEnv(this.leoConfig); 45 | // 初始化本地 config 46 | this.initLocalConfig(); 47 | } 48 | 49 | async start() { 50 | try { 51 | this?.hooks?.beforeStart?.call(this); 52 | this.leoRC = getLeoRC(this.leoConfig.rcFileName); 53 | 54 | log.debug(`${this.leoConfig.rcFileName}`, JSON.stringify(this.leoRC, null, 2)); 55 | log.debug('config', JSON.stringify(this.leoConfig, null, 2)); 56 | 57 | await this.initCLI(); 58 | 59 | await this.cli.start(); 60 | } catch (e) { 61 | log.error(this.leoConfig.isDev ? e : ((e as unknown) as Error)?.message); 62 | process.exit(-1); 63 | } 64 | } 65 | 66 | cliActionHandler = async ( 67 | command: string, 68 | args: IActionArgs, 69 | extendsCommandAction?: (params: { 70 | args: IActionArgs; 71 | subCommandName: string; 72 | [key: string]: any; 73 | }) => void, 74 | ) => { 75 | log.debug('cliActionHandler', 'commandName:', command, 'args:', args); 76 | 77 | try { 78 | const [commandName, subCommandName] = command.split('.'); 79 | 80 | await this.doStartCheck(); 81 | 82 | // 约定必须要实现的 83 | switch (commandName) { 84 | case 'init': 85 | await this.commandInitAction(args); 86 | break; 87 | case 'dev': 88 | await this.commandDevAction(args); 89 | break; 90 | case 'build': 91 | await this.commandBuildAction(args); 92 | break; 93 | case 'test': 94 | await this.commandTestAction(args); 95 | break; 96 | case 'lint': 97 | await this.commandLintAction(args); 98 | break; 99 | case 'clear': 100 | await this.commandClearAction(args); 101 | break; 102 | case 'config': 103 | await this.commandConfigAction(subCommandName, args); 104 | break; 105 | default: 106 | await this.extendsCommandAction(commandName, subCommandName, args, extendsCommandAction); 107 | break; 108 | } 109 | 110 | this?.hooks?.afterCommandExecute?.call(this); 111 | } catch (error) { 112 | log.error(this.leoConfig.isDev ? error : ((error as unknown) as Error)?.message); 113 | process.exit(-1); 114 | } 115 | }; 116 | 117 | // 以下命令回调 118 | // init 指令回调 119 | async commandInitAction(args: IActionArgs) { 120 | let { 121 | arguments: [templateName], 122 | } = args; 123 | const { options, unexpectedOptions } = args; 124 | 125 | // 模糊查询需要登录,如果直接使用如 leo init ifloor,也会提示,所以设置了配置项控制 126 | if (templateName) { 127 | const projectList = await this.getProjectsLikeName(templateName); 128 | 129 | if (projectList.length <= 0) { 130 | throw new Error(`未找到对应的模板,请校对模板名是否正确(${templateName})`); 131 | } 132 | 133 | // 当模板为 aa-bb 时,输出 leo init aa,也应当有提示 134 | if ( 135 | (projectList.length === 1 && projectList[0].value !== templateName) || 136 | projectList.length > 1 137 | ) { 138 | log.info('如需关闭模板模糊查询功能,可使用 leo config set fuzzyTemp=false'); 139 | const res = await this.askQuestions([ 140 | { 141 | name: 'templateName', 142 | type: 'list', 143 | message: `查询到与 ${templateName} 相关的模板,请选择:`, 144 | choices: projectList, 145 | }, 146 | ]); 147 | templateName = res.templateName; 148 | } 149 | } 150 | 151 | if (!templateName) { 152 | const res = await this.askQuestions(this.leoConfig.questions.chooseTemplate); 153 | templateName = res.templateName; 154 | } 155 | 156 | const { useInstall, projectName } = await this.askQuestions(this.leoConfig.questions.init); 157 | 158 | log.debug('commandInitAction', templateName, options); 159 | 160 | log.info('开始创建项目'); 161 | 162 | await this.initGenerator(templateName, { ...options, ...unexpectedOptions }, projectName); 163 | 164 | if (useInstall !== 'custom') { 165 | log.await('开始安装依赖'); 166 | await runInstall(useInstall === 'yarn', projectName); 167 | log.success('安装完成!'); 168 | } 169 | 170 | log.info( 171 | `初始化成功!\n 使用以下指令来开始项目:\n cd ${projectName}\n ${this.leoConfig.rootName} dev\n`, 172 | ); 173 | 174 | this.leoConfig.helpTexts.forEach((helpInfo) => { 175 | log.info(helpInfo.text); 176 | }); 177 | } 178 | 179 | async commandDevAction(args: IActionArgs) { 180 | log.debug('commandDevAction', args); 181 | 182 | const { options, unexpectedOptions } = args; 183 | 184 | log.info('启动调试'); 185 | 186 | await this.initBuilder({ ...options, ...unexpectedOptions }); 187 | 188 | // hook: beforeDev 189 | await this.leoRC?.builder?.hooks?.beforeDev?.(this.builder); 190 | 191 | await this.builder.start(); 192 | 193 | // hook: afterDev 194 | await this.leoRC?.builder?.hooks?.afterDev?.(this.builder); 195 | } 196 | 197 | async commandBuildAction(args: IActionArgs) { 198 | log.debug('commandBuildAction', args); 199 | 200 | const { options, unexpectedOptions } = args; 201 | log.info('开始构建'); 202 | 203 | await this.initBuilder({ ...options, ...unexpectedOptions }); 204 | 205 | // hook: beforeBuild 206 | await this.leoRC?.builder?.hooks?.beforeBuild?.(this.builder); 207 | 208 | await this.builder.build(); 209 | 210 | // hook: beforeBuild 211 | await this.leoRC?.builder?.hooks?.afterBuild?.(this.builder); 212 | } 213 | 214 | async commandTestAction(args: IActionArgs) { 215 | log.debug('commandTestAction', args); 216 | 217 | const { options, unexpectedOptions } = args; 218 | log.info('开始测试'); 219 | 220 | return await this.doTestAction({ ...options, ...unexpectedOptions }); 221 | } 222 | 223 | async doTestAction(options: { [key: string]: any } = {}) { 224 | if (this.leoRC.testAction) { 225 | // 外部传入优先级更高 226 | const testRes = await this.leoRC.testAction(options); 227 | if (testRes !== true) { 228 | throw new Error('test 失败'); 229 | } 230 | return testRes; 231 | } else { 232 | log.warn('未找到 leorc 中 testAction 配置'); 233 | } 234 | // true 为正常执行 235 | return true; 236 | } 237 | 238 | async commandLintAction(args: IActionArgs) { 239 | log.debug('commandLintAction', args); 240 | 241 | const { options, unexpectedOptions } = args; 242 | log.info('开始Lint'); 243 | 244 | return await this.doLintAction({ ...options, ...unexpectedOptions }); 245 | } 246 | 247 | async doLintAction(options: { [key: string]: any } = {}) { 248 | if (this.leoRC.lintAction) { 249 | // 外部传入优先级更高 250 | const lintRes = await this.leoRC.lintAction(options); 251 | // 由于 leorc 是 js 编写,必须强制校验 lintAction 的返回, 252 | // 同理 test action 也添加相同逻辑 253 | if (lintRes !== true) { 254 | throw new Error('lint 失败'); 255 | } 256 | return lintRes; 257 | } else { 258 | log.warn('未找到 leorc 中 lintAction 配置'); 259 | } 260 | // true 为正常执行 261 | return true; 262 | } 263 | 264 | async commandClearAction(args: IActionArgs) { 265 | const { options } = args; 266 | log.info('开始清理'); 267 | 268 | // 如果未填参数则显示帮助信息 269 | if (Object.values(options).every((v) => v === false)) { 270 | return this.doClearAction(); 271 | } 272 | 273 | await Promise.all( 274 | Object.entries(options) 275 | .filter((keyValue) => keyValue[1] === true) 276 | .map((keyValue) => this.doClearAction(keyValue[0])), 277 | ); 278 | } 279 | 280 | async prePublishAction(args: IActionArgs) { 281 | log.debug('prePublishAction', this.leoRC.isPrePublishParallel); 282 | 283 | if (this.leoRC.isPrePublishParallel) { 284 | return Promise.all([ 285 | this.commandLintAction(args), 286 | this.commandTestAction(args), 287 | this.commandBuildAction(args), 288 | ]); 289 | } 290 | const lintResult = await this.commandLintAction(args); 291 | if (lintResult !== true) return; 292 | const testResult = await this.commandTestAction(args); 293 | if (testResult !== true) return; 294 | await this.commandBuildAction(args); 295 | } 296 | 297 | async doClearAction(clearName?: string) { 298 | const { virtualPath } = this.leoConfig; 299 | 300 | switch (clearName) { 301 | case 'config': 302 | await fs.remove(virtualPath.configPath); 303 | log.success('清除 leoConfig 缓存成功'); 304 | break; 305 | case 'template': 306 | await fs.remove(virtualPath.templatePath); 307 | log.success('清除模板缓存成功'); 308 | break; 309 | case 'node_modules': 310 | await fs.remove(virtualPath.nodeModulesPath); 311 | log.success('清除 npm 包缓存成功'); 312 | break; 313 | case 'all': 314 | default: 315 | await fs.remove(virtualPath.entry); 316 | log.success('清除所有缓存成功'); 317 | } 318 | } 319 | 320 | async commandConfigAction(subCommandName: string, args: IActionArgs) { 321 | // 确保 config 文件存在 322 | const { 323 | virtualPath: { configPath }, 324 | } = this.leoConfig; 325 | await fs.ensureFile(configPath); 326 | const localConfig = (await fs.readJson(configPath)) || {}; 327 | const { 328 | arguments: [key], 329 | } = args; 330 | 331 | switch (subCommandName) { 332 | case 'set': 333 | await this.doSetConfigAction(args, localConfig); 334 | break; 335 | case 'get': 336 | log.info(localConfig[key]); 337 | break; 338 | case 'delete': 339 | unset(localConfig, key); 340 | await fs.writeJson(this.leoConfig.virtualPath.configPath, localConfig); 341 | log.success('删除成功'); 342 | break; 343 | case 'list': 344 | log.info(localConfig); 345 | break; 346 | default: 347 | // 不存在无值的情况,未传指令commander会拦截 348 | break; 349 | } 350 | } 351 | 352 | async doSetConfigAction(args: IActionArgs, localConfig: { [key: string]: any }) { 353 | // args.arguments 为 ['key=value','key=value'] 格式 354 | const keyValueMap = args.arguments.reduce((map, keyValue) => { 355 | // 命令行空格会出现 undefined 356 | if (keyValue) { 357 | const [key, value] = keyValue.split('='); 358 | const v = value === 'true' || value === 'false' ? value === 'true' : value; 359 | set(map, key, v); 360 | } 361 | return map; 362 | }, {}); 363 | 364 | await fs.writeJson(this.leoConfig.virtualPath.configPath, merge({}, localConfig, keyValueMap)); 365 | 366 | log.success('设置成功'); 367 | } 368 | 369 | async extendsCommandAction( 370 | commandName: string, 371 | subCommandName: string, 372 | args: IActionArgs, 373 | extendsCommandAction: (params: { 374 | args: IActionArgs; 375 | subCommandName: string; 376 | [key: string]: any; 377 | }) => void, 378 | ) { 379 | await extendsCommandAction?.({ 380 | args, 381 | subCommandName, 382 | ...this.getCommonParams(), 383 | }); 384 | } 385 | 386 | askQuestions(questions: any[]): Promise<{ [key: string]: any }> { 387 | return inquirer.prompt(questions); 388 | } 389 | 390 | /** 391 | * 执行检查 392 | * @returns {Promise} 393 | */ 394 | async doStartCheck(): Promise { 395 | // 云构建某些情况无法连接内网 396 | const checkList = [this.checkNodeVersion(), this.checkVersion()]; 397 | 398 | return Promise.all(checkList); 399 | } 400 | 401 | private async initBuilder(options: { [key: string]: any } = {}): Promise { 402 | log.debug('initBuilder', 'before builder create'); 403 | 404 | const { leoRC } = this; 405 | const Builder = (await loadPkg(leoRC.builder.name, { 406 | version: leoRC.builder.version, 407 | private: true, 408 | })) as any; 409 | 410 | this.builder = new Builder({ 411 | options, 412 | ...this.getCommonParams(), 413 | }); 414 | } 415 | 416 | private async initCLI(): Promise { 417 | log.debug('initCLI', 'before CLI create'); 418 | 419 | const { leoConfig } = this; 420 | const { version } = getCorePackageJSON(); 421 | 422 | const CLI = (await loadPkg(leoConfig.cli.name, leoConfig.cli.version)) as any; 423 | 424 | this.cli = new CLI({ 425 | commands: this.leoConfig.commands, 426 | actionHandler: this.cliActionHandler, 427 | version, 428 | helpTexts: this.leoConfig.helpTexts, 429 | ...this.getCommonParams(), 430 | }); 431 | } 432 | 433 | private async initGenerator( 434 | templateName: string, 435 | options: { [key: string]: any }, 436 | projectName: string, 437 | ): Promise { 438 | log.debug('initGenerator', 'start init generator'); 439 | 440 | const { leoConfig } = this; 441 | const Generator = (await loadPkg(leoConfig.generator.name, leoConfig.generator.version)) as any; 442 | 443 | this.generator = new Generator({ 444 | templateName, 445 | projectName, 446 | options, 447 | cachePath: this.leoConfig.virtualPath.templatePath, 448 | ...this.getCommonParams(), 449 | }); 450 | 451 | await this.generator.start(); 452 | } 453 | 454 | /** 455 | * 检查node版本 456 | * @private 457 | * @returns {Promise} 458 | */ 459 | private checkNodeVersion(): Promise { 460 | // 检查 node 版本 461 | const { engines } = getCorePackageJSON(); 462 | const nodeVersionSatisfy = semver.satisfies(process.version, engines.node); 463 | if (nodeVersionSatisfy) return Promise.resolve(0); 464 | return Promise.reject(new Error(`请使用${engines.node}版本的 node`)); 465 | } 466 | 467 | /** 468 | * 检查当前版本与最新版本是否一致并提示 469 | * @private 470 | * @returns {Promise} 471 | */ 472 | private async checkVersion(): Promise { 473 | // 默认提示升级,如开启强制升级,则中断后续操作 474 | const pkg = getCorePackageJSON(); 475 | const pkgTagVersion = await getRemotePkgTagVersion(pkg.name); 476 | 477 | const { latest } = pkgTagVersion; 478 | 479 | // 检查正式版本 480 | if (semver.gt(latest, pkg.version) && this.leoConfig.forceUpdate) { 481 | return Promise.reject(new Error('当前版本过旧,请升级后使用')); 482 | } 483 | } 484 | 485 | private async checkPublishStatus(): Promise { 486 | const git = simpleGit(); 487 | 488 | // 检查 git 是否全部提交 489 | // 检查当前登录状态(是否过期) 490 | const [{ files }] = await Promise.all([git.status()]); 491 | 492 | if (files.length) { 493 | return 'git 全部提交后才能进行发布'; 494 | } 495 | return ''; 496 | } 497 | 498 | /** 499 | * @description 模糊查询leo-templates下的模板 500 | * @private 501 | * @memberof Core 502 | */ 503 | private getProjectsLikeName = async (name: string): Promise<[{ [key: string]: any }]> => { 504 | const { data: templateList } = await axios 505 | .get(`${this.leoConfig.gitTemplateGroupQueryURL}`, { 506 | headers: this.leoConfig.gitQueryHeader, 507 | }) 508 | .catch(() => { 509 | return { data: [] }; 510 | }); 511 | 512 | return templateList 513 | .map((item: { [key: string]: any }) => { 514 | const templateName = item.path.replace(/-template$/, ''); 515 | return { 516 | name: `${templateName}(${item.name})`, 517 | value: templateName, 518 | }; 519 | }) 520 | .filter((item: { [key: string]: any }) => { 521 | return this.leoConfig.fuzzyTemp ? item.value.indexOf(name) !== -1 : item.value === name; 522 | }) 523 | .sort((item: { [key: string]: any }) => (item.value === name ? -1 : 0)); 524 | }; 525 | 526 | /** 527 | * @description 初始化本地 config,如果本地不存在,则创建 528 | * @private 529 | * @memberof Core 530 | */ 531 | private initLocalConfig = () => { 532 | const { 533 | virtualPath: { configPath }, 534 | } = this.leoConfig; 535 | 536 | if (fs.existsSync(configPath)) { 537 | return; 538 | } 539 | fs.outputJsonSync(configPath, localStaticConfig); 540 | }; 541 | 542 | /** 543 | * @description 提供需要为插件提供的公共参数 544 | * @private 545 | */ 546 | private getCommonParams(): ICommonParams { 547 | return { 548 | leoConfig: this.leoConfig, 549 | leoRC: this.leoRC, 550 | leoUtils: {}, 551 | pkg: getProjectPackageJSON(), 552 | }; 553 | } 554 | } 555 | 556 | export default Core; 557 | --------------------------------------------------------------------------------