├── .husky └── pre-commit ├── src ├── type │ ├── config.type.ts │ ├── store.type.ts │ ├── manager.type.ts │ ├── plugin.type.ts │ └── package.type.ts ├── const │ ├── plugin.const.ts │ └── manager.const.ts ├── instance │ ├── config.instance.ts │ ├── tool.instance.ts │ ├── logger.instance.ts │ ├── command.instance.ts │ ├── node.instance.ts │ ├── plugin.instance.ts │ ├── store.instance.ts │ ├── installer.instance.ts │ └── package.instance.ts ├── controller │ ├── init.controller.ts │ ├── all.controller.ts │ ├── uninstall.controller.ts │ ├── install.controller.ts │ ├── config.controller.ts │ └── template.controller.ts ├── service │ ├── tool.service.ts │ ├── node.service.ts │ ├── logger.service.ts │ ├── plugin.service.ts │ ├── store.service.ts │ ├── command.service.ts │ ├── package.service.ts │ ├── config.service.ts │ └── installer.service.ts ├── main.module.ts └── main.ts ├── .prettierrc ├── .gitignore ├── copy.json.mjs ├── tsconfig.json ├── .eslintrc.js ├── .github └── workflows │ └── release.yml ├── rollup.config.js ├── README.md ├── store.default.json ├── store.user.json ├── package.json └── LICENSE /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/husky.sh" 3 | npm run lint-staged -------------------------------------------------------------------------------- /src/type/config.type.ts: -------------------------------------------------------------------------------- 1 | export type TYPE_CONFIG_ITEM = { 2 | [k: string]: { file: string; json: object }; 3 | }; 4 | -------------------------------------------------------------------------------- /src/type/store.type.ts: -------------------------------------------------------------------------------- 1 | import { TYPE_PLUGIN_ITEM } from "@/type/plugin.type"; 2 | 3 | export type TYPE_STORE_INFO = { 4 | installs: TYPE_PLUGIN_ITEM[]; 5 | }; 6 | -------------------------------------------------------------------------------- /src/const/plugin.const.ts: -------------------------------------------------------------------------------- 1 | export const HUSKY: "husky" = "husky"; 2 | export const PRETTIER: "prettier" = "prettier"; 3 | export const TS: "typescript" = "typescript"; 4 | -------------------------------------------------------------------------------- /src/instance/config.instance.ts: -------------------------------------------------------------------------------- 1 | export interface ConfigInstance { 2 | get(isDefault: boolean, matPlugins: any[]): any; 3 | set(isDefault: boolean, matPlugins: any[]): void; 4 | } 5 | -------------------------------------------------------------------------------- /src/type/manager.type.ts: -------------------------------------------------------------------------------- 1 | import { CNPM, NPM, PNPM, YARN } from "@/const/manager.const"; 2 | 3 | export type TYPE_MANAGER_NAME = 4 | | typeof NPM 5 | | typeof YARN 6 | | typeof PNPM 7 | | typeof CNPM; 8 | -------------------------------------------------------------------------------- /src/instance/tool.instance.ts: -------------------------------------------------------------------------------- 1 | export interface ToolInstance { 2 | isObject(obj: any): boolean; 3 | formatJSON(o: any): string; 4 | writeJSONFileSync(path: string, content: any): void; 5 | execSync(exec: string): void; 6 | } 7 | -------------------------------------------------------------------------------- /src/const/manager.const.ts: -------------------------------------------------------------------------------- 1 | export const NPM: "npm" = "npm"; 2 | export const YARN: "yarn" = "yarn"; 3 | export const PNPM: "pnpm" = "pnpm"; 4 | export const CNPM: "cnpm" = "cnpm"; 5 | 6 | export const MANAGER_LIST = [YARN, NPM, CNPM, PNPM]; 7 | -------------------------------------------------------------------------------- /src/instance/logger.instance.ts: -------------------------------------------------------------------------------- 1 | export interface LoggerInstance { 2 | success(s: string, bold?: boolean): void; 3 | warn(s: string, bold?: boolean): void; 4 | error(s: string, bold?: boolean): void; 5 | finish(s: string): void; 6 | } 7 | -------------------------------------------------------------------------------- /src/instance/command.instance.ts: -------------------------------------------------------------------------------- 1 | export interface CommandInstance { 2 | main: string; 3 | subs: { 4 | [key: string]: { 5 | alias: string; 6 | description: string; 7 | examples: string[]; 8 | }; 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": false, 6 | "semi": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "htmlWhitespaceSensitivity": "ignore" 10 | } 11 | -------------------------------------------------------------------------------- /src/instance/node.instance.ts: -------------------------------------------------------------------------------- 1 | export interface NodeInstance { 2 | readonly filename: string; 3 | readonly dirname: string; 4 | readonly root: string; 5 | readonly versions: { 6 | preVersion: number; 7 | fullVersion: string; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/instance/plugin.instance.ts: -------------------------------------------------------------------------------- 1 | import { TYPE_PLUGIN_ITEM, TYPE_PLUGIN_NAME } from "@/type/plugin.type"; 2 | 3 | export interface PluginInstance { 4 | getAll(): TYPE_PLUGIN_NAME[]; 5 | get(): TYPE_PLUGIN_ITEM[]; 6 | set(pluginName: string, file: object): void; 7 | } 8 | -------------------------------------------------------------------------------- /src/instance/store.instance.ts: -------------------------------------------------------------------------------- 1 | import { TYPE_STORE_INFO } from "@/type/store.type"; 2 | 3 | export interface StoreInstance { 4 | readonly curPath: string; 5 | get(): TYPE_STORE_INFO; 6 | getByPath(filename: string): TYPE_STORE_INFO; 7 | update(key: string, content: Object): void; 8 | } 9 | -------------------------------------------------------------------------------- /src/instance/installer.instance.ts: -------------------------------------------------------------------------------- 1 | import { TYPE_PLUGIN_ITEM } from "@/type/plugin.type"; 2 | 3 | export interface InstallerInstance { 4 | chooseManager(): Promise; 5 | install(plugins: TYPE_PLUGIN_ITEM[]): Promise; 6 | uninstall(plugins: TYPE_PLUGIN_ITEM[]): Promise; 7 | choose(): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /src/type/plugin.type.ts: -------------------------------------------------------------------------------- 1 | import { HUSKY, PRETTIER, TS } from "@/const/plugin.const"; 2 | 3 | export type TYPE_PLUGIN_NAME = typeof TS | typeof HUSKY | typeof PRETTIER; 4 | 5 | export type TYPE_PLUGIN_ITEM = { 6 | name: TYPE_PLUGIN_NAME; 7 | config: { file: string; json: object }; 8 | dev: boolean; 9 | pkgInject: object; 10 | }; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pnpm-debug.log* 7 | pnpm-lock.yaml 8 | lerna-debug.log* 9 | node_modules 10 | .DS_Store 11 | dist 12 | bin 13 | *.local 14 | 15 | # Editor directories and files 16 | .idea 17 | *.suo 18 | .vsocde 19 | .hbuilder 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? -------------------------------------------------------------------------------- /src/instance/package.instance.ts: -------------------------------------------------------------------------------- 1 | import { TYPE_PACKAGE_INFO } from "@/type/package.type"; 2 | 3 | export interface PackageInstance { 4 | script: string; 5 | curDir: string; 6 | curPath: string; 7 | get(): TYPE_PACKAGE_INFO; 8 | remove(key: string, isScript?: boolean): void; 9 | update(key: string, content: Object): void; 10 | } 11 | -------------------------------------------------------------------------------- /src/controller/init.controller.ts: -------------------------------------------------------------------------------- 1 | import { installerService } from "@/service/installer.service"; 2 | import { loggerService } from "@/service/logger.service"; 3 | 4 | export class InitController { 5 | static key = "init"; 6 | constructor() { 7 | installerService.choose().then(() => { 8 | loggerService.finish(InitController.key); 9 | }); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /copy.json.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import {fileURLToPath} from 'url' 3 | import {dirname, join} from 'path' 4 | const __dirname = dirname(fileURLToPath(import.meta.url)); 5 | 6 | // 定义源文件和目标文件的路径 7 | const sourcePath = join(__dirname, 'store.default.json'); 8 | const destinationPath = join(__dirname, 'store.user.json'); 9 | 10 | fs.copyFileSync(sourcePath, destinationPath); 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "outDir": "bin", 5 | "module": "esnext", 6 | "esModuleInterop": true, 7 | "moduleResolution": "node", 8 | "rootDir": "src", 9 | "sourceMap": true, 10 | "importHelpers": true, 11 | "paths": { 12 | "@/*": ["src/*"] 13 | }, 14 | "baseUrl": "./" 15 | }, 16 | "include": ["src"], 17 | "exclude": ["node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /src/type/package.type.ts: -------------------------------------------------------------------------------- 1 | // package.json接口类型 2 | export type TYPE_PACKAGE_INFO = { 3 | name?: string; 4 | version?: string; 5 | description?: string; 6 | main?: string; 7 | bin?: { 8 | [k: string]: string; 9 | }; 10 | scripts: { 11 | [scriptName: string]: string; 12 | }; 13 | dependencies: { 14 | [dependencyName: string]: string; 15 | }; 16 | devDependencies: { 17 | [devDependencyName: string]: string; 18 | }; 19 | // 这里可以继续添加其他你认为重要的属性 20 | }; 21 | -------------------------------------------------------------------------------- /src/service/tool.service.ts: -------------------------------------------------------------------------------- 1 | import fsExtra from "fs-extra"; 2 | import { execSync } from "child_process"; 3 | import { ToolInstance } from "@/instance/tool.instance"; 4 | 5 | class ToolService implements ToolInstance { 6 | isObject(obj: any): boolean { 7 | return Object.prototype.toString.call(obj) === "[object Object]"; 8 | } 9 | 10 | formatJSON(content: any) { 11 | return JSON.stringify(content, null, 2); 12 | } 13 | writeJSONFileSync(path: string, content: any): void { 14 | fsExtra.writeFileSync(path, this.formatJSON(content)); 15 | } 16 | 17 | execSync(exec: string): void { 18 | execSync(exec, { stdio: "inherit" }); 19 | } 20 | } 21 | 22 | export const toolService: ToolInstance = new ToolService(); 23 | -------------------------------------------------------------------------------- /src/controller/all.controller.ts: -------------------------------------------------------------------------------- 1 | import readlineSync from "readline-sync"; 2 | import { installerService } from "@/service/installer.service"; 3 | import { PluginService } from "@/service/plugin.service"; 4 | import { loggerService } from "@/service/logger.service"; 5 | 6 | export class AllController { 7 | static key = "all"; 8 | constructor() { 9 | const answer = readlineSync.question( 10 | "Would you like to install prettier, husky, and typescript that work with your Node.js version? (y/n) " 11 | ); 12 | if (answer.toLowerCase() !== "n") { 13 | installerService 14 | .install(new PluginService(false).get()) 15 | .then(() => loggerService.finish(AllController.key)); 16 | } else { 17 | loggerService.warn("Cancel the installation"); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/service/node.service.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "url"; 2 | import path from "path"; 3 | import process from "process"; 4 | import { NodeInstance } from "@/instance/node.instance"; 5 | 6 | class NodeService implements NodeInstance { 7 | public readonly filename: string; 8 | public readonly dirname: string; 9 | public readonly root: string; 10 | public readonly versions: { 11 | preVersion: number; 12 | fullVersion: string; 13 | }; 14 | 15 | constructor() { 16 | this.filename = fileURLToPath(import.meta.url); 17 | this.dirname = path.dirname(this.filename); 18 | this.root = path.join(this.dirname, ".."); 19 | this.versions = { 20 | preVersion: Number(process.versions.node.split(".")[0]), 21 | fullVersion: process.version 22 | }; 23 | } 24 | } 25 | 26 | export const nodeService = new NodeService(); 27 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | parser: "@typescript-eslint/parser", 3 | parserOptions: { 4 | project: "tsconfig.json", 5 | tsconfigRootDir: __dirname, 6 | sourceType: "module" 7 | }, 8 | plugins: ["@typescript-eslint/eslint-plugin"], 9 | extends: [ 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:prettier/recommended" 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true 17 | }, 18 | ignorePatterns: [".eslintrc.js"], 19 | rules: { 20 | // 以_开头不报eslint 21 | // "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 22 | "@typescript-eslint/interface-name-prefix": "off", 23 | "@typescript-eslint/explicit-function-return-type": "off", 24 | "@typescript-eslint/explicit-module-boundary-types": "off", 25 | "@typescript-eslint/no-explicit-any": "off" 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - release 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | 17 | - name: Set up Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: "20" 21 | 22 | - name: Install dependencies 23 | run: | 24 | npm install --force 25 | 26 | - name: Build 27 | run: npm run build 28 | 29 | - name: Authenticate with npm registry 30 | run: npm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }} 31 | 32 | - name: Publish 33 | env: 34 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | run: npm publish -r --access public --no-git-checks 36 | -------------------------------------------------------------------------------- /src/controller/uninstall.controller.ts: -------------------------------------------------------------------------------- 1 | import { PluginService } from "@/service/plugin.service"; 2 | import { loggerService } from "@/service/logger.service"; 3 | import { installerService } from "@/service/installer.service"; 4 | import { commandService } from "@/service/command.service"; 5 | 6 | export class UninstallController { 7 | static key = "uninstall"; 8 | constructor(pluginNames: string[] = []) { 9 | const pluginService = new PluginService(false); 10 | const matInstalls = pluginService 11 | .get() 12 | .filter((item) => pluginNames.includes(item.name)); 13 | if (!matInstalls.length) { 14 | const pluginStr = pluginService.getAll().join(" | ").trim(); 15 | loggerService.error( 16 | `Error: ${commandService.main} "is only allow to uninstall ${pluginStr}.` 17 | ); 18 | } else { 19 | installerService 20 | .uninstall(matInstalls) 21 | .then(() => loggerService.finish(UninstallController.key)); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/controller/install.controller.ts: -------------------------------------------------------------------------------- 1 | import { PluginService } from "@/service/plugin.service"; 2 | import { loggerService } from "@/service/logger.service"; 3 | import { installerService } from "@/service/installer.service"; 4 | import { commandService } from "@/service/command.service"; 5 | 6 | export class InstallController { 7 | static key = "install"; 8 | constructor(pluginNames: string[] = []) { 9 | // TODO 但是多个安装里,有一个错误,应该给到提示 10 | const pluginService = new PluginService(false); 11 | const matInstalls = pluginService 12 | .get() 13 | .filter((item) => pluginNames.includes(item.name)); 14 | if (!matInstalls.length) { 15 | console.log("插件名错误"); 16 | const pluginStr = pluginService.getAll().join(" | ").trim(); 17 | loggerService.error( 18 | `Error: ${commandService.main} is only allow to install "${pluginStr}.` 19 | ); 20 | } else { 21 | console.log("到这里"); 22 | installerService 23 | .install(matInstalls) 24 | .then(() => loggerService.finish(InstallController.key)); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main.module.ts: -------------------------------------------------------------------------------- 1 | import { NodeInstance } from "@/instance/node.instance"; 2 | import { nodeService } from "@/service/node.service"; 3 | import { CommandInstance } from "@/instance/command.instance"; 4 | import { commandService } from "@/service/command.service"; 5 | import { InitController } from "@/controller/init.controller"; 6 | import { InstallController } from "@/controller/install.controller"; 7 | import { UninstallController } from "@/controller/uninstall.controller"; 8 | import { AllController } from "@/controller/all.controller"; 9 | import { ConfigController } from "@/controller/config.controller"; 10 | import { TemplateController } from "@/controller/template.controller"; 11 | 12 | export class MainModule { 13 | public readonly nodeService: NodeInstance = nodeService; 14 | public readonly commandService: CommandInstance = commandService; 15 | getAll() { 16 | return [ 17 | InstallController, 18 | InitController, 19 | AllController, 20 | UninstallController, 21 | ConfigController, 22 | TemplateController 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/service/logger.service.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import { commandService } from "@/service/command.service"; 3 | import { LoggerInstance } from "@/instance/logger.instance"; 4 | import { CommandInstance } from "@/instance/command.instance"; 5 | 6 | class LoggerService implements LoggerInstance { 7 | private readonly commandService: CommandInstance = commandService; 8 | private colorize(type: string, s: string, bold: boolean): string { 9 | let color = "yellow"; 10 | if (type === "success") color = "green"; 11 | else if (type === "error") color = "red"; 12 | return bold ? chalk.bold[color](s) : chalk[color](s); 13 | } 14 | 15 | success(s: string, bold: boolean = false): void { 16 | console.log(this.colorize("success", s, bold)); 17 | } 18 | 19 | warn(s: string, bold: boolean = false): void { 20 | console.log(this.colorize("warn", s, bold)); 21 | } 22 | 23 | error(s: string, bold: boolean = false): void { 24 | console.log(this.colorize("error", s, bold)); 25 | } 26 | 27 | finish(s: string): void { 28 | this.success(`${this.commandService.main} ${s} done.`, true); 29 | } 30 | } 31 | 32 | export const loggerService = new LoggerService(); 33 | -------------------------------------------------------------------------------- /src/service/plugin.service.ts: -------------------------------------------------------------------------------- 1 | import { StoreService } from "@/service/store.service"; 2 | import { StoreInstance } from "@/instance/store.instance"; 3 | import { PluginInstance } from "@/instance/plugin.instance"; 4 | 5 | export class PluginService implements PluginInstance { 6 | public readonly normalKey = "installs"; 7 | private readonly storeService: StoreInstance; 8 | constructor(isUser: boolean) { 9 | this.storeService = new StoreService(isUser); 10 | } 11 | getAll() { 12 | const info = this.storeService.get(); 13 | // console.log(info, "info plugin.service"); 14 | return info[this.normalKey].map((item) => item.name); 15 | } 16 | get() { 17 | const info = this.storeService.get(); 18 | // console.log(info, "info"); 19 | return info[this.normalKey]; 20 | } 21 | // 这里都是调用store的crud 22 | reset() { 23 | const defaultInfo = this.storeService.getByPath(this.storeService.curPath); 24 | this.storeService.update(this.normalKey, defaultInfo[this.normalKey]); 25 | } 26 | set(pluginName: string, file: object) { 27 | const plugins = this.get(); 28 | plugins.forEach((item) => { 29 | if (item.name === pluginName) { 30 | item.config.json = file; 31 | } 32 | }); 33 | this.storeService.update(this.normalKey, plugins); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /** 借用打包文件 是为了实现 @符号对应路径,ts并不能很好的实现 */ 2 | 3 | // import rollupPluginJson from "@rollup/plugin-json"; 4 | import typescript from "rollup-plugin-typescript2"; 5 | import alias from "@rollup/plugin-alias"; 6 | import copy from "rollup-plugin-copy"; 7 | import { fileURLToPath } from "url"; 8 | import path, { dirname } from "path"; 9 | import { terser } from "rollup-plugin-terser"; // 引入压缩插件 10 | 11 | const __dirname = dirname(fileURLToPath(import.meta.url)); 12 | const isProductionEnv = process.env.ENV === "production"; 13 | 14 | export default { 15 | input: "src/main.ts", // 你的入口文件 16 | output: { 17 | sourcemap: !isProductionEnv, 18 | dir: "bin", // 输出目录,而不是具体的文件 19 | format: "es", // 输出格式 20 | preserveModules: true, // 保留模块结构 21 | preserveModulesRoot: "src" // 设置源模块的根目录 22 | }, 23 | plugins: [ 24 | alias({ 25 | entries: [ 26 | { find: "@", replacement: path.resolve(__dirname, "./src") } // 使用绝对路径 27 | ] 28 | }), 29 | typescript({ 30 | // typescript插件配置 31 | }), 32 | copy({ 33 | targets: [ 34 | // { src: "package.json", dest: "bin" }, // 复制 package.json 到输出目录 35 | { src: "*.json", dest: "bin" }, // 复制 所有 .json 文件到输出目录 36 | { src: "README.md", dest: "bin" } // 复制 md 37 | ] 38 | }), 39 | isProductionEnv && terser() // // 如果是生产环境,则添加压缩插件 40 | ].filter(Boolean), // 过滤掉所有假值(例如,非生产环境下的terser插件会是undefined) 41 | // 确保不删除未使用的导出 42 | treeshake: false 43 | }; 44 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { join } from "path"; 3 | import fsExtra from "fs-extra"; 4 | import { Command } from "commander"; 5 | import "source-map-support/register.js"; 6 | import { MainModule } from "@/main.module"; 7 | 8 | class Entry { 9 | constructor(private readonly mainModule: MainModule) { 10 | const localPkgPath = join(this.mainModule.nodeService.root, "package.json"); 11 | const localPkgInfo = JSON.parse( 12 | fsExtra.readFileSync(localPkgPath, "utf-8") 13 | ); 14 | const commandList = this.mainModule.commandService.subs; 15 | const program = new Command(); 16 | program 17 | .version(`${localPkgInfo.name}@${localPkgInfo.version}`) 18 | .usage(" [option]"); 19 | for (let key in commandList) { 20 | const { alias, description } = commandList[key]; 21 | program 22 | .command(key) // 注册命令 23 | .alias(alias) // 自定义命令缩写 24 | .description(description) // 命令描述 25 | .action((options, { parent }) => { 26 | const subExecWord: string = parent.args[0]; 27 | const modules = this.mainModule.getAll(); 28 | for (let ModuleClass of modules) { 29 | if ( 30 | subExecWord === ModuleClass.key || 31 | subExecWord === commandList[ModuleClass.key].alias 32 | ) { 33 | new ModuleClass(parent.args.slice(1)); 34 | } 35 | } 36 | }); 37 | } 38 | program.parse(process.argv); 39 | } 40 | } 41 | 42 | new Entry(new MainModule()); 43 | -------------------------------------------------------------------------------- /src/service/store.service.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import fsExtra from "fs-extra"; 3 | import { toolService } from "@/service/tool.service"; 4 | import { StoreInstance } from "@/instance/store.instance"; 5 | import { ToolInstance } from "@/instance/tool.instance"; 6 | import { NodeInstance } from "@/instance/node.instance"; 7 | import { nodeService } from "@/service/node.service"; 8 | import { TYPE_STORE_INFO } from "@/type/store.type"; 9 | 10 | export class StoreService implements StoreInstance { 11 | private readonly nodeService: NodeInstance = nodeService; 12 | private readonly toolService: ToolInstance = toolService; 13 | private readonly curDir: string = join(this.nodeService.root); 14 | public readonly curPath: string; 15 | 16 | constructor(isUser: boolean) { 17 | this.curPath = join( 18 | this.curDir, 19 | `store.${isUser ? "user" : "default"}.json` 20 | ); 21 | } 22 | 23 | getByPath(filepath: string): TYPE_STORE_INFO { 24 | try { 25 | if (!fsExtra.existsSync(filepath)) throw new Error("Error json path"); 26 | return JSON.parse(fsExtra.readFileSync(filepath, "utf-8")); 27 | } catch (e) { 28 | throw new Error("Error: " + e); 29 | } 30 | } 31 | 32 | get(): TYPE_STORE_INFO { 33 | return this.getByPath(this.curPath); 34 | } 35 | update(key: string, content: Object): void { 36 | const filepath = this.curPath; 37 | const info = this.get(); 38 | if (!info || !info[key]) throw new Error("Error at Storage.update"); 39 | info[key] = content; 40 | this.toolService.writeJSONFileSync(filepath, info); 41 | } 42 | // single set 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 介绍 2 | 3 | 功能是标准化前端工程项目-prettier, husky, typescript, 4 | 支持渐进式无侵害接入旧项目; 5 | 也可以一键生成新项目,包括安装插件, 并生成插件对应的配置文件, 6 | 如tsconfig.json ,.husky/pre-commit, .prettierrc等。 7 | 还会检测.git目录不存在,自动init master并创建.gitignore文件,配置git config core.ignorecase false 8 | 9 | ## 前言 10 | 11 | 每次公司或者自己创建新项目,都要手动安装 prettier husky typescript等一系列插件 还要配置文件, 12 | 基本记不住的都要去copy上一个项目,带来了心智负担, 13 | 尤其husky每次还要创建pre-commit文件,并且添加echo命令, 14 | 于是打算封装规范化的脚手架,供创建项目,或者旧项目要加上这些配置和依赖的时候使用。 15 | 16 | ## 支持 17 | 18 | 本插件只基于node,所以无论是node框架项目,还是vue/react/angular/webpack/vite/rollup都适用, 19 | 也无需芥蒂window or mac电脑,且提供默认的配置, 20 | 傻瓜式安装。(理论上所有项目规范一般都是统一的,也支持自定义配置插件的配置文件) 21 | 22 | ## 视频教程 23 | 24 | http://media.leaiv.cn/baize-pre/video.html 25 | 26 | ## 优势 27 | 28 | - 侵入成本小,新项目一键搞定,无心智负担 29 | - 渐进式命令init和install,无论新老项目皆可以 30 | - 自定义配置,多种场景多种命令匹配 31 | - 体积小,脚手架所有源码不到 100k 32 | - 不关心平台和前端框架,只要有node,几乎支持配置前端所有框架的项目规范 33 | 34 | ## 功能实现 35 | 36 | - init: Choose and install multiple plugins, and configure them according to your Node.js version. 37 | - install: Install and configure some plugins compatible with your Node.js version. 38 | - uninstall: Uninstall some plugins and remove their configuration settings that are related to your Node.js version. 39 | - all: Quickly install all plugins and configure them with your Node.js version. 40 | - config: Configure the CLI variable. Once configured, use it everywhere. 41 | - -h: View help. 42 | - -V: View current version. 43 | 44 | ## 依赖插件 45 | 46 | - commander: 参数解析 47 | - inquirer: 选项交互式工具,有它就可以实现命令行的选项 48 | - chalk: 粉笔帮我们在控制台画出各种各样的颜色 49 | - readline-sync:询问式交互工具 50 | 51 | ## 期望 52 | 53 | - 寻找更多有志之士,邮箱:1795691637@qq.com 54 | - 喜欢点个star吧~ 55 | 56 | ## TODO 57 | 58 | - 搜寻意见,是需要做“一个项目选择一次包管理工具”,还是“该项目每次安装都需要询问包管理工具”。 59 | - 使用TS重构(并配置alias路径) 60 | -------------------------------------------------------------------------------- /src/service/command.service.ts: -------------------------------------------------------------------------------- 1 | import { CommandInstance } from "@/instance/command.instance"; 2 | import { PackageInstance } from "@/instance/package.instance"; 3 | import { PackageService } from "@/service/package.service"; 4 | 5 | class CommandService implements CommandInstance { 6 | private readonly packageService: PackageInstance; 7 | public readonly main: string; 8 | public readonly subs; 9 | constructor() { 10 | this.packageService = new PackageService(false); 11 | this.main = Object.keys(this.packageService.get().bin)[0] + " "; 12 | this.subs = { 13 | init: { 14 | alias: "", 15 | description: 16 | "Choose and install multiple plugins, and configure them according to your Node.js version.", 17 | examples: [this.main + "init"] 18 | }, 19 | install: { 20 | alias: "i", 21 | description: 22 | "Install and configure some plugins compatible with your Node.js version.", 23 | examples: [this.main + "i "] 24 | }, 25 | uninstall: { 26 | alias: "", 27 | description: 28 | "Uninstall some plugins and remove their configuration settings that are related to your Node.js version.", 29 | examples: [] 30 | }, 31 | template: { 32 | alias: "t", 33 | description: "Create a new project with a template.", 34 | examples: [this.main + "template"] 35 | }, 36 | all: { 37 | alias: "a", 38 | description: 39 | "Quickly install all plugins and configure them with your Node.js version.", 40 | examples: [this.main + "all"] 41 | }, 42 | config: { 43 | alias: "conf", 44 | description: 45 | "Configure the CLI variable. Once configured, use it everywhere.", 46 | examples: [ 47 | this.main + "config set ", 48 | this.main + "config get " 49 | ] 50 | }, 51 | "*": { 52 | alias: "", 53 | description: "Command not found.", 54 | examples: [] 55 | } 56 | }; 57 | } 58 | } 59 | 60 | export const commandService = new CommandService(); 61 | -------------------------------------------------------------------------------- /store.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "installs": [ 3 | { 4 | "name": "prettier", 5 | "config": { 6 | "file": ".prettierrc", 7 | "json": { 8 | "printWidth": 80, 9 | "tabWidth": 2, 10 | "useTabs": false, 11 | "singleQuote": false, 12 | "semi": false, 13 | "trailingComma": "none", 14 | "bracketSpacing": true, 15 | "htmlWhitespaceSensitivity": "ignore" 16 | } 17 | }, 18 | "dev": true, 19 | "pkgInject": { 20 | "scripts": { 21 | "format": "prettier --write ." 22 | } 23 | } 24 | }, 25 | { 26 | "name": "husky", 27 | "config": { 28 | "file": ".husky/pre-commit", 29 | "json": " #!/usr/bin/env sh \n. \"$(dirname -- \"$0\")/_/husky.sh\" \nnpm run lint-staged " 30 | }, 31 | "dev": true, 32 | "pkgInject": { 33 | "lint-staged": { 34 | "**/*.{js,ts,json}": "prettier --write ." 35 | }, 36 | "scripts": { 37 | "lint-staged": "lint-staged" 38 | } 39 | } 40 | }, 41 | { 42 | "name": "typescript", 43 | "config": { 44 | "file": "tsconfig.json", 45 | "json": { 46 | 47 | "compilerOptions": { 48 | "target": "es6", 49 | "outDir": "bin", 50 | "module": "esnext", 51 | "esModuleInterop": true, 52 | "moduleResolution": "node", 53 | "rootDir": "src", 54 | "sourceMap": true, 55 | "importHelpers": true, 56 | "paths": { 57 | "@/*": ["src/*"] 58 | }, 59 | "baseUrl": "./" 60 | }, 61 | "include": ["src"], 62 | "exclude": ["node_modules"] 63 | 64 | } 65 | }, 66 | "dev": true, 67 | "pkgInject": {} 68 | } 69 | ], 70 | "gitignore": { 71 | "file": ".gitignore", 72 | "json": "logs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\nnode_modules\n.DS_Store\ndist\n*.local\n\n# Editor directories and files\n.idea\n*.suo\n.vsocde\n.hbuilder\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /store.user.json: -------------------------------------------------------------------------------- 1 | { 2 | "installs": [ 3 | { 4 | "name": "prettier", 5 | "config": { 6 | "file": ".prettierrc", 7 | "json": { 8 | "printWidth": 80, 9 | "tabWidth": 2, 10 | "useTabs": false, 11 | "singleQuote": false, 12 | "semi": false, 13 | "trailingComma": "none", 14 | "bracketSpacing": true, 15 | "htmlWhitespaceSensitivity": "ignore" 16 | } 17 | }, 18 | "dev": true, 19 | "pkgInject": { 20 | "scripts": { 21 | "format": "prettier --write ." 22 | } 23 | } 24 | }, 25 | { 26 | "name": "husky", 27 | "config": { 28 | "file": ".husky/pre-commit", 29 | "json": " #!/usr/bin/env sh \n. \"$(dirname -- \"$0\")/_/husky.sh\" \nnpm run lint-staged " 30 | }, 31 | "dev": true, 32 | "pkgInject": { 33 | "lint-staged": { 34 | "**/*.{js,ts,json}": "prettier --write ." 35 | }, 36 | "scripts": { 37 | "lint-staged": "lint-staged" 38 | } 39 | } 40 | }, 41 | { 42 | "name": "typescript", 43 | "config": { 44 | "file": "tsconfig.json", 45 | "json": { 46 | 47 | "compilerOptions": { 48 | "target": "es6", 49 | "outDir": "bin", 50 | "module": "esnext", 51 | "esModuleInterop": true, 52 | "moduleResolution": "node", 53 | "rootDir": "src", 54 | "sourceMap": true, 55 | "importHelpers": true, 56 | "paths": { 57 | "@/*": ["src/*"] 58 | }, 59 | "baseUrl": "./" 60 | }, 61 | "include": ["src"], 62 | "exclude": ["node_modules"] 63 | 64 | } 65 | }, 66 | "dev": true, 67 | "pkgInject": {} 68 | } 69 | ], 70 | "gitignore": { 71 | "file": ".gitignore", 72 | "json": "logs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\nnode_modules\n.DS_Store\ndist\n*.local\n\n# Editor directories and files\n.idea\n*.suo\n.vsocde\n.hbuilder\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/controller/config.controller.ts: -------------------------------------------------------------------------------- 1 | import { commandService } from "@/service/command.service"; 2 | import { loggerService } from "@/service/logger.service"; 3 | import { ConfigService, configService } from "@/service/config.service"; 4 | import { ConfigInstance } from "@/instance/config.instance"; 5 | import { PluginService } from "@/service/plugin.service"; 6 | import { PluginInstance } from "@/instance/plugin.instance"; 7 | 8 | export class ConfigController { 9 | static key = "config"; 10 | public args: any[] = []; 11 | public configService: ConfigInstance = configService; 12 | private pluginService: PluginInstance; 13 | private isDefault: boolean = false; 14 | constructor(args: string[] = []) { 15 | this.args = args; 16 | try { 17 | this.#check(); 18 | } catch (e) { 19 | loggerService.error(e); 20 | } 21 | } 22 | #check() { 23 | if (!this.args.length || this.args.length > 3) return this.#error(); 24 | const action: "get" | "set" = this.args[0].trim(); // 这时候必有action动作,判断是 get or set 25 | if (action !== "get" && action !== "set") return this.#error(); 26 | const expectPluginName = this.args[1]; 27 | if (!expectPluginName) return this.#error(); // 格式不对,get or set后要接 具体插件名 28 | if (expectPluginName === "default" && !this.args[2]) { 29 | // 如果 get or set 后只有 default,则检测的任务就完成了,直接get or set default给用户反馈 30 | return (this.isDefault = true); 31 | } 32 | this.pluginService = new PluginService(false); 33 | const pluginNames = this.pluginService.getAll(); // 34 | const matPlugins = pluginNames.filter((name) => 35 | this.args.slice(1).includes(name) 36 | ); 37 | if (!matPlugins.length) { 38 | throw new Error( 39 | `Key is not found.Try to get the key "${pluginNames.join(" | ").trim()}` 40 | ); 41 | } 42 | console.log(matPlugins, "matPlugins"); 43 | 44 | ConfigService[action](this.isDefault, matPlugins); // 尽力过check , action必为 get or set 45 | } 46 | #error() { 47 | const complete = `${commandService.main}${ConfigController.key} ${this.args.join(" ")}`; 48 | const example = commandService.subs[ConfigController.key].examples 49 | .join(" | ") 50 | .trim(); 51 | const msg = `Error command. Expected "${example}", got "${complete}".`; 52 | throw new Error(msg); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "baize-pre", 3 | "version": "0.2.9", 4 | "description": "A useful progressive front-end normalization tool.", 5 | "bin": { 6 | "baize": "/bin/main.js" 7 | }, 8 | "type": "module", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/baizeteam/baize-pre" 12 | }, 13 | "keywords": [ 14 | "front-end", 15 | "front-end format", 16 | "husky", 17 | "prettier", 18 | "husky", 19 | "basic", 20 | "vue", 21 | "react", 22 | "angular" 23 | ], 24 | "author": "dog", 25 | "license": "ISC", 26 | "scripts": { 27 | "build:w": "cross-env ENV=development rollup -c --watch & npm link", 28 | "build": "node copy.json.mjs & cross-env ENV=production rollup -c", 29 | "format": "prettier --write .", 30 | "lint-staged": "lint-staged" 31 | }, 32 | "dependencies": { 33 | "@octokit/rest": "^21.1.1", 34 | "chalk": "^4.1.2", 35 | "commander": "^11.1.0", 36 | "cross-env": "^7.0.3", 37 | "fs-extra": "^11.2.0", 38 | "inquirer": "^8.2.6", 39 | "readline-sync": "^1.4.10", 40 | "simple-git": "^3.27.0", 41 | "source-map-support": "^0.5.21" 42 | }, 43 | "devDependencies": { 44 | "@rollup/plugin-alias": "^5.1.0", 45 | "@rollup/plugin-json": "^6.1.0", 46 | "@types/node": "^20.3.1", 47 | "@typescript-eslint/eslint-plugin": "^6.0.0", 48 | "@typescript-eslint/parser": "^6.0.0", 49 | "eslint": "^8.42.0", 50 | "eslint-config-prettier": "^9.0.0", 51 | "eslint-plugin-prettier": "^5.0.0", 52 | "husky": "^9.1.1", 53 | "lint-staged": "^15.2.7", 54 | "prettier": "^3.3.3", 55 | "rollup": "^4.18.0", 56 | "rollup-plugin-copy": "^3.5.0", 57 | "rollup-plugin-terser": "^7.0.2", 58 | "rollup-plugin-typescript2": "^0.36.0", 59 | "ts-jest": "^29.1.0", 60 | "ts-loader": "^9.4.3", 61 | "ts-node": "^10.9.1", 62 | "tsconfig-paths": "^4.2.0", 63 | "tslib": "^2.6.3", 64 | "typescript": "^5.5.3" 65 | }, 66 | "jest": { 67 | "moduleFileExtensions": [ 68 | "js", 69 | "json", 70 | "ts" 71 | ], 72 | "rootDir": "src", 73 | "testRegex": ".*\\.spec\\.ts$", 74 | "transform": { 75 | "^.+\\.(t|j)s$": "ts-jest" 76 | }, 77 | "collectCoverageFrom": [ 78 | "**/*.(t|j)s" 79 | ], 80 | "coverageDirectory": "../coverage", 81 | "testEnvironment": "node" 82 | }, 83 | "lint-staged": { 84 | "**/*.{js,ts,json}": "prettier --write ." 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/service/package.service.ts: -------------------------------------------------------------------------------- 1 | import fsExtra from "fs-extra"; 2 | import { join } from "path"; 3 | import { toolService } from "@/service/tool.service"; 4 | import { PackageInstance } from "@/instance/package.instance"; 5 | import { ToolInstance } from "@/instance/tool.instance"; 6 | import { TYPE_PACKAGE_INFO } from "@/type/package.type"; 7 | import { nodeService } from "@/service/node.service"; 8 | import process from "process"; 9 | 10 | export class PackageService implements PackageInstance { 11 | public readonly script: string = "scripts"; 12 | public readonly curPath: string; 13 | public readonly curDir: string; 14 | private toolService: ToolInstance = toolService; 15 | 16 | constructor(isUser: boolean) { 17 | // 需要识别的是用户的根目录,还是自身的 18 | this.curDir = isUser ? process.cwd() : nodeService.root; 19 | this.curPath = join(this.curDir, "package.json"); 20 | } 21 | get(): TYPE_PACKAGE_INFO { 22 | const defaultInfo: TYPE_PACKAGE_INFO = { 23 | scripts: {}, 24 | devDependencies: {}, 25 | dependencies: {} 26 | }; 27 | let info; 28 | if (fsExtra.existsSync(this.curPath)) { 29 | try { 30 | const infoJSON = fsExtra.readFileSync(this.curPath, "utf-8"); 31 | info = JSON.parse(infoJSON); 32 | // console.log("识别成功") 33 | } catch (_) { 34 | // console.log("识别失败") 35 | info = defaultInfo; 36 | } 37 | if ( 38 | !this.toolService.isObject(info) || 39 | !this.toolService.isObject(info[this.script]) 40 | ) { 41 | // console.log("到这了") 42 | info = defaultInfo; 43 | } 44 | // console.log(info, "info return") 45 | return info; 46 | } 47 | 48 | this.toolService.writeJSONFileSync(this.curPath, defaultInfo); 49 | return defaultInfo; 50 | } 51 | 52 | remove(key: string, isScript: boolean = false): void { 53 | const info = this.get(); 54 | if (!isScript) { 55 | if (info[key] === undefined) { 56 | throw new Error(`Internal Error: the key of '${key}' does not exist.`); 57 | } 58 | delete info[key]; 59 | } else { 60 | if ( 61 | !this.toolService.isObject(info[this.script]) || 62 | info[this.script][key] === undefined 63 | ) { 64 | throw new Error( 65 | `Internal Error: the key of '${key}' does not exist in scripts.` 66 | ); 67 | } 68 | delete info[this.script][key]; 69 | } 70 | 71 | this.toolService.writeJSONFileSync(this.curPath, info); 72 | } 73 | 74 | update(key: string, content: Object): void { 75 | const info = this.get(); 76 | if (key === this.script) { 77 | info[this.script] = { ...info[this.script], ...content }; 78 | } else { 79 | info[key] = content; 80 | } 81 | this.toolService.writeJSONFileSync(this.curPath, info); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/service/config.service.ts: -------------------------------------------------------------------------------- 1 | import { loggerService } from "@/service/logger.service"; 2 | import { PackageService } from "@/service/package.service"; 3 | import * as readline from "readline-sync"; 4 | import fsExtra from "fs-extra"; 5 | import { join } from "path"; 6 | import { PluginService } from "@/service/plugin.service"; 7 | import { StoreService } from "@/service/store.service"; 8 | import { ConfigInstance } from "@/instance/config.instance"; 9 | import { TYPE_CONFIG_ITEM } from "@/type/config.type"; 10 | import { toolService } from "@/service/tool.service"; 11 | 12 | export class ConfigService implements ConfigInstance { 13 | public get: () => void; 14 | public set: () => void; 15 | static get(isDefault: boolean, matPlugins: any[]): TYPE_CONFIG_ITEM { 16 | const storeService = new StoreService(false); 17 | const plugins = storeService.get().installs; 18 | const matConfigs: TYPE_CONFIG_ITEM = {}; 19 | plugins.forEach((pluginItem) => { 20 | matPlugins.forEach((pluginName) => { 21 | if (pluginName === pluginItem.name) { 22 | matConfigs[pluginName] = pluginItem.config; 23 | } 24 | }); 25 | }); 26 | loggerService.success(toolService.formatJSON(matConfigs)); 27 | return matConfigs; 28 | } 29 | static set(isDefault: boolean, matPlugins: any[]) { 30 | // set default (恢复默认) 的判断 31 | if (isDefault) { 32 | const answer = readline.question( 33 | "Would you like to set the default config ? (y/n)" 34 | ); 35 | if (answer.toLowerCase() !== "n") { 36 | try { 37 | new PluginService(false).reset(); 38 | loggerService.success("Successfully."); 39 | } catch (e) { 40 | loggerService.error("Error: " + e); 41 | } 42 | } else { 43 | loggerService.warn("Cancel the setting process."); 44 | } 45 | return; 46 | } 47 | const keyStr = matPlugins.join(",").trim(); 48 | console.log(keyStr, "keyStr", matPlugins); 49 | const answer = readline.question( 50 | `Would you like to set "${keyStr}" for your local files? (y/n)` 51 | ); 52 | if (answer.toLowerCase() !== "n") { 53 | // 很明显是 读取 用户 的pkg 54 | const packageService = new PackageService(true); 55 | const pluginService = new PluginService(true); 56 | const configs = this.get(isDefault, matPlugins); 57 | console.log(configs, "configs"); 58 | for (let key in configs) { 59 | const item = configs[key]; 60 | const userConfigFilePath = join(packageService.curDir, item.file); 61 | console.log(userConfigFilePath, "filepath"); 62 | if (!fsExtra.existsSync(userConfigFilePath)) 63 | return loggerService.error("Error: read your local file failed."); 64 | const file = fsExtra.readFileSync(userConfigFilePath, "utf-8"); 65 | console.log(file, "file"); 66 | // 将用户的配置注入到我们的 store.user.json 中 67 | pluginService.set(key, file as any); 68 | loggerService.success("Successfully."); 69 | } 70 | } else { 71 | loggerService.warn("Cancel the setting process."); 72 | } 73 | } 74 | } 75 | 76 | export const configService = new ConfigService(); 77 | -------------------------------------------------------------------------------- /src/controller/template.controller.ts: -------------------------------------------------------------------------------- 1 | import inquirer from "inquirer"; 2 | import { Octokit } from "@octokit/rest"; 3 | import path from "path"; 4 | import fs from "fs"; 5 | import simpleGit, { SimpleGit } from "simple-git"; 6 | 7 | export class TemplateController { 8 | static key = "template"; 9 | private octokit: Octokit; 10 | private git: SimpleGit; 11 | private owner: string = "baizeteam"; 12 | private repo: string = "baize-template"; 13 | constructor() { 14 | this.octokit = new Octokit({}); 15 | this.git = simpleGit(); 16 | this.run(); 17 | } 18 | 19 | async run() { 20 | const branches = await this.getBranches(); 21 | if (branches) { 22 | const branch = await this.selectBranch(branches); 23 | const projectName = await this.projectName(); 24 | await this.cloneAndRename(branch, projectName); 25 | await this.deleteGit(projectName); 26 | console.log("项目创建成功!"); 27 | } 28 | } 29 | 30 | // 选择分支 31 | async selectBranch(branches) { 32 | const branchAnswer = await inquirer.prompt([ 33 | { 34 | type: "list", 35 | name: "branch", 36 | message: "请选择一个分支:", 37 | choices: branches 38 | } 39 | ]); 40 | return branchAnswer.branch; 41 | } 42 | 43 | // 项目命名 44 | async projectName() { 45 | const projectNameAnswer = await inquirer.prompt([ 46 | { 47 | type: "input", 48 | name: "projectName", 49 | message: "请输入项目名称:", 50 | validate: (input) => (input.trim() ? true : "项目名称不能为空!") 51 | } 52 | ]); 53 | return projectNameAnswer.projectName; 54 | } 55 | 56 | // 获取分支 57 | async getBranches() { 58 | try { 59 | const response = await this.octokit.repos.listBranches({ 60 | owner: this.owner, 61 | repo: this.repo 62 | }); 63 | return response.data.map((branch) => branch.name).filter(item=>item!=="master"); 64 | } catch (error) { 65 | console.error("Error fetching branches:", error); 66 | } 67 | } 68 | 69 | // 拉取代码并重命名项目 70 | async cloneAndRename(branch: string, projectName: string) { 71 | try { 72 | // 克隆代码库 73 | const cloneDir = path.join(process.cwd(), projectName); 74 | console.log(`正在克隆 ${branch} 分支到 ${cloneDir}...`); 75 | await this.git.clone( 76 | `https://github.com/${this.owner}/${this.repo}.git`, 77 | cloneDir, 78 | ["--branch", branch, "--single-branch"] 79 | ); 80 | console.log(`代码克隆成功!`); 81 | 82 | // 重命名项目 83 | const projectDir = path.join(cloneDir, "package.json"); 84 | if (fs.existsSync(projectDir)) { 85 | const packageJson = JSON.parse(fs.readFileSync(projectDir, "utf8")); 86 | packageJson.name = projectName; // 修改项目名称 87 | fs.writeFileSync(projectDir, JSON.stringify(packageJson, null, 2)); 88 | console.log(`项目名称已更新为 ${projectName}`); 89 | } 90 | 91 | // 可以在这里添加其他初始化步骤,比如安装依赖等 92 | // await this.git.cwd(cloneDir).raw(["npm", "install"]); 93 | } catch (error) { 94 | console.error("Error cloning or renaming project:", error); 95 | } 96 | } 97 | 98 | // 删除 git 99 | async deleteGit(projectName: string) { 100 | const projectDir = path.join(process.cwd(), projectName, ".git"); 101 | if (fs.existsSync(projectDir)) { 102 | await fs.rmSync(projectDir, { recursive: true }); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /src/service/installer.service.ts: -------------------------------------------------------------------------------- 1 | import fsExtra from "fs-extra"; 2 | import { join } from "path"; 3 | import inquirer from "inquirer"; 4 | import { PackageService } from "@/service/package.service"; 5 | import { TYPE_PLUGIN_ITEM } from "@/type/plugin.type"; 6 | import { HUSKY } from "@/const/plugin.const"; 7 | import { MANAGER_LIST, PNPM } from "@/const/manager.const"; 8 | import { TYPE_MANAGER_NAME } from "@/type/manager.type"; 9 | import { loggerService } from "@/service/logger.service"; 10 | import { nodeService } from "@/service/node.service"; 11 | import { toolService } from "@/service/tool.service"; 12 | import { PluginService } from "@/service/plugin.service"; 13 | import { PackageInstance } from "@/instance/package.instance"; 14 | import { LoggerInstance } from "@/instance/logger.instance"; 15 | import { ToolInstance } from "@/instance/tool.instance"; 16 | import { NodeInstance } from "@/instance/node.instance"; 17 | import { InstallerInstance } from "@/instance/installer.instance"; 18 | 19 | class InstallerService implements InstallerInstance { 20 | private userPkg: PackageInstance; 21 | private managerName: TYPE_MANAGER_NAME; 22 | private pluginService: PluginService; 23 | private readonly loggerService: LoggerInstance = loggerService; 24 | private readonly toolService: ToolInstance = toolService; 25 | private readonly nodeService: NodeInstance = nodeService; 26 | 27 | constructor() { 28 | this.userPkg = new PackageService(true); 29 | } 30 | 31 | async chooseManager(): Promise { 32 | const questionKey = "manager"; 33 | const question = [ 34 | { 35 | type: "list", 36 | name: questionKey, 37 | message: "Which package manager to use?", 38 | choices: MANAGER_LIST 39 | } 40 | ]; 41 | const answer = await inquirer.prompt(question); 42 | const result = answer[questionKey]; 43 | this.loggerService.success("You have chosen: " + result); 44 | const { preVersion, fullVersion } = this.nodeService.versions; 45 | if (preVersion < 16 && result === PNPM) { 46 | this.loggerService.error( 47 | `Sorry, your Node.js version is not supported by "${PNPM}".` 48 | ); 49 | this.loggerService.error(`Expected >= 16, but got "${fullVersion}".`); 50 | } else { 51 | this.managerName = result; 52 | } 53 | } 54 | async #handleInstall(pluginName: string, dev = false, version = null) { 55 | /** 必须在install前刷新一遍pkg的info,避免npm 安装时写入和我们的写入顺序冲掉了 */ 56 | this.userPkg.get(); 57 | const { managerName } = this; 58 | let exec = 59 | managerName === "yarn" 60 | ? managerName + " add " 61 | : managerName + " install "; 62 | // 如果是本地模块,则加上-D 63 | dev && (exec += " -D "); 64 | exec += pluginName; 65 | // 如果指定插件版本,则带上version 66 | version && (exec += "@" + version); 67 | 68 | try { 69 | // 捕获安装错误 70 | this.loggerService.warn("Installing " + pluginName + " ... "); 71 | this.toolService.execSync(exec); 72 | this.loggerService.success("Installed " + pluginName + " successfully. "); 73 | } catch (e) { 74 | this.loggerService.error("Error: install " + pluginName + " : "); 75 | console.log(e); // 承接上一行错误,但不要颜色打印 76 | } 77 | } 78 | async uninstall(plugins: TYPE_PLUGIN_ITEM[]) { 79 | // 卸载插件以及插件配置文件,由于包管理工具机制,比如你用npm安装,用yarn卸载某项,yarn执行完毕会去安装全部插件 80 | // 如果用户的包管理工具不一致,用户自己选择的,不能怪我们 81 | await this.chooseManager(); 82 | for (let item of plugins) { 83 | const { name, config, pkgInject } = item; 84 | const pluginName = name; 85 | this.#handleUninstall(pluginName) 86 | .then(async () => { 87 | // 移除配置项 88 | const { file } = config; 89 | const filepath = join(this.userPkg.curDir, file); 90 | if (!fsExtra.existsSync(filepath)) 91 | return this.loggerService.error('"Error config path: ' + filepath); 92 | // 删除配置文件 93 | if (pluginName === "husky") { 94 | const filepath = join(this.userPkg.curDir, ".husky"); 95 | fsExtra.removeSync(filepath); 96 | } else { 97 | fsExtra.removeSync(filepath); 98 | } 99 | 100 | // 删除包信息配置 101 | let info = this.userPkg.get(); 102 | for (let pkgKey in pkgInject) { 103 | if (info[pkgKey]) { 104 | const SCRIPTS = this.userPkg.script; 105 | // console.log(tool.isObject(pkgInject[pkgKey]), pkgKey, pkgInject, 'isObject') 106 | if ( 107 | pkgKey === SCRIPTS && 108 | this.toolService.isObject(pkgInject[SCRIPTS]) 109 | ) { 110 | // 此时pkg[pkgKey] 等同于 pkgInject[SCRIPTS] 但后者语义好 111 | for (let scriptKey in pkgInject[SCRIPTS]) { 112 | // SCRIPTS 里有这个键 113 | if (info[SCRIPTS].hasOwnProperty(scriptKey)) { 114 | // console.log('SCRIPTS 里有这个键', scriptKey, info[SCRIPTS]) 115 | // 多判断husky里携带的lint-staged 116 | if (pluginName === HUSKY) { 117 | const LINT = "lint-staged"; 118 | await this.#handleUninstall(LINT); 119 | this.userPkg.remove(LINT, true); 120 | } else { 121 | this.userPkg.remove(scriptKey, true); 122 | } 123 | } 124 | } 125 | } else { 126 | // console.log(info[pkgKey], 'other') 127 | this.userPkg.remove(pkgKey); 128 | } 129 | } 130 | } 131 | }) 132 | .catch((e) => { 133 | console.log(e); // 承接上一行错误,但不要颜色打印 134 | }); 135 | } 136 | } 137 | 138 | #handleUninstall(pkgName) { 139 | return new Promise((resolve, reject) => { 140 | this.userPkg.get(); 141 | const { managerName } = this; 142 | let exec = 143 | managerName === "yarn" 144 | ? managerName + " remove " 145 | : managerName + " uninstall "; 146 | exec += pkgName; 147 | try { 148 | // 捕获安装错误 149 | this.loggerService.warn("Uninstalling " + pkgName + " ... "); 150 | this.toolService.execSync(exec); 151 | this.loggerService.success( 152 | "Uninstalled " + pkgName + " successfully. " 153 | ); 154 | resolve(true); 155 | } catch (e) { 156 | this.loggerService.error("Error: uninstall " + pkgName + " : "); 157 | reject(e); 158 | } 159 | }); 160 | } 161 | #handleConfig(config) { 162 | this.userPkg.get(); 163 | const filepath = join(this.userPkg.curDir, config.file); 164 | // console.log(config, "有注入配置", filepath) 165 | try { 166 | const { json } = config; 167 | if (typeof json === "object") 168 | this.toolService.writeJSONFileSync(filepath, json); 169 | else fsExtra.writeFileSync(filepath, json); 170 | } catch (e) { 171 | // console.log(filepath,'失败 filepath') 172 | // 内部错误 173 | return this.loggerService.error( 174 | "Internal Error: Configuration injection failed in handleConfig." 175 | ); 176 | } 177 | } 178 | #updatePackage(pkgInject) { 179 | this.userPkg.get(); 180 | // console.log(pkgInject, "有注入命令") 181 | for (let key in pkgInject) { 182 | // 更新用户json 183 | this.userPkg.update(key, pkgInject[key]); 184 | } 185 | } 186 | 187 | #checkGit() { 188 | this.userPkg.get(); 189 | const gitPath = join(this.userPkg.curDir, ".git"); 190 | // console.log(gitPath,'gitPath') 191 | if (!fsExtra.existsSync(gitPath)) { 192 | // 最好用 'dev' 作为默认分支名 193 | // master就算不是默认分支时,都是不可删的 194 | this.toolService.execSync("git init -b dev"); 195 | // 更改git默认不区分大小写的配置 196 | // 如果A文件已提交远程,再改为小写的a文件,引用a文件会出现本地正确、远程错误,因为远程还是大A文件) 197 | this.toolService.execSync("git config core.ignorecase false"); 198 | } 199 | } 200 | async #checkHusky() { 201 | // 这一个依赖.git 202 | this.#checkGit(); 203 | // 如果node版本小于16,使用@8版本插件 204 | this.toolService.execSync("npx " + HUSKY + " install"); 205 | // console.log('checkhusky') 206 | await this.#handleInstall("lint-staged", true); 207 | } 208 | async install(plugins: TYPE_PLUGIN_ITEM[]) { 209 | await this.chooseManager(); 210 | for (let pluginItem of plugins) { 211 | const { name, config, dev, pkgInject } = pluginItem; 212 | const pluginName = name; 213 | await this.#handleInstall( 214 | pluginName, 215 | dev, 216 | pluginName === "husky" && this.nodeService.versions.preVersion < 16 217 | ? 8 218 | : null 219 | ); 220 | // 顺序很重要,放最前面 221 | pluginName === "husky" && (await this.#checkHusky()); 222 | // // 有需要合并的脚本 223 | pkgInject && (await this.#updatePackage(pkgInject)); 224 | // // 有需要write的config文件 225 | config && this.#handleConfig(config); 226 | } 227 | } 228 | async choose() { 229 | const questionKey = "plugins"; 230 | this.pluginService = new PluginService(false); 231 | const storagePlugins = this.pluginService.getAll(); 232 | const question = [ 233 | { 234 | type: "checkbox", 235 | name: questionKey, 236 | message: "Choose the plugins you want to install.", 237 | choices: storagePlugins, 238 | validate(answers) { 239 | if (!answers.length) return "You must choose at least one plugin."; 240 | return true; 241 | } 242 | } 243 | ]; 244 | const answers = await inquirer.prompt(question); 245 | this.pluginService = new PluginService(false); 246 | const matInstalls = this.pluginService 247 | .get() 248 | .filter((item) => answers[questionKey].includes(item.name)); 249 | await this.install(matInstalls); 250 | } 251 | } 252 | 253 | export const installerService = new InstallerService(); 254 | --------------------------------------------------------------------------------