├── pathA └── to │ ├── ExampleFile.txt │ ├── example-file.txt │ └── fool-file.txt ├── unuse ├── mixins.js ├── test │ ├── index.js │ └── user-rulerts.vue ├── assets │ ├── dp.png │ ├── bg.jfif │ └── demo.png ├── main.js ├── components │ ├── test2 │ │ └── HelloWorld.vue │ ├── test │ │ └── deep │ │ │ └── user.vue │ └── user-rulerts.vue └── App.vue ├── api ├── aa.js └── user.js ├── md2.png ├── md3.png ├── .husky └── pre-commit ├── src ├── index.ts ├── bin.ts ├── commands │ ├── agmd.ts │ ├── mark-write-file.ts │ ├── get-router.ts │ ├── wirte-md.ts │ ├── command-handler.ts │ ├── command-actions.ts │ ├── mark-file.ts │ ├── change-path.ts │ ├── get-file.ts │ └── rename-path.ts ├── types.ts ├── shared │ └── constant.ts └── utils │ └── router-utils.ts ├── .eslintignore ├── .prettierignore ├── .prettierrc ├── classify.js ├── test ├── __mocks__ │ └── fs.ts ├── config │ ├── jest.setup.ts │ └── jest-global-setup.ts ├── utils │ ├── function-test.ts │ ├── utils.ts │ ├── deep-nodes-test.ts │ └── nodes-test.ts ├── rename-path.test.ts ├── mark-write-file.test.ts ├── wirte-md.test.ts ├── change-path-absolute.test.ts ├── get-router.test.ts ├── change-path.test.ts ├── get-file.test.ts ├── rename.test.ts ├── mark-file.test.ts └── renamecopy.ts ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── .github └── workflows │ └── test.yml ├── .gitignore ├── router ├── container │ └── index.js └── index.js ├── .eslintrc.js ├── jest.config.ts ├── script ├── cli │ ├── index.ts │ └── handle.ts └── help │ └── index.ts ├── tsconfig.json ├── tsconfig.es6.json ├── LICENSE ├── README.EN.md ├── package.json ├── README.md └── codeAndPrompt.md /pathA/to/ExampleFile.txt: -------------------------------------------------------------------------------- 1 | file contents -------------------------------------------------------------------------------- /pathA/to/example-file.txt: -------------------------------------------------------------------------------- 1 | file contents -------------------------------------------------------------------------------- /pathA/to/fool-file.txt: -------------------------------------------------------------------------------- 1 | fool contents -------------------------------------------------------------------------------- /unuse/mixins.js: -------------------------------------------------------------------------------- 1 | 2 | export function name(params) { 3 | 4 | } 5 | -------------------------------------------------------------------------------- /api/aa.js: -------------------------------------------------------------------------------- 1 | export default function name(params) { 2 | 3 | } 4 | //2工程 5 | -------------------------------------------------------------------------------- /api/user.js: -------------------------------------------------------------------------------- 1 | //2工程 2 | export default function name(params) {} 3 | //2工程 4 | -------------------------------------------------------------------------------- /md2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakajun/auto-generate-md/HEAD/md2.png -------------------------------------------------------------------------------- /md3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakajun/auto-generate-md/HEAD/md3.png -------------------------------------------------------------------------------- /unuse/test/index.js: -------------------------------------------------------------------------------- 1 | /* 我就是个测试 */ 2 | import app from '../app.vue' 3 | console.log('main') 4 | -------------------------------------------------------------------------------- /unuse/assets/dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakajun/auto-generate-md/HEAD/unuse/assets/dp.png -------------------------------------------------------------------------------- /unuse/assets/bg.jfif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakajun/auto-generate-md/HEAD/unuse/assets/bg.jfif -------------------------------------------------------------------------------- /unuse/assets/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kakajun/auto-generate-md/HEAD/unuse/assets/demo.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | # npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 这里抛出一些高级操作方法 */ 2 | import { getMd } from './commands/wirte-md' 3 | import { getFileNodes } from './commands/get-file' 4 | export { getMd, getFileNodes } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | 2 | *.sh 3 | node_modules 4 | lib 5 | *.md 6 | *.scss 7 | *.woff 8 | *.ttf 9 | .vscode 10 | .idea 11 | /build/ 12 | /dist/ 13 | /public/ 14 | index.d.ts 15 | .vscode 16 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | **/*es.js 3 | **/*.html 4 | **/*.txt 5 | **/*.md 6 | **/*.svg 7 | **/*.ttf 8 | **/*.woff 9 | **/*.eot 10 | readme-md.md 11 | package.json 12 | node_modules 13 | -------------------------------------------------------------------------------- /src/bin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { Command } from 'commander' 3 | import { handleCommand } from './commands/command-handler' 4 | const program = new Command() 5 | program.allowUnknownOption(true) 6 | program.action(handleCommand) 7 | program.parse(process.argv) 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "printWidth": 120, 7 | "bracketSpacing": true, 8 | "jsxBracketSameLine": false, 9 | "arrowParens": "always", 10 | "trailingComma": "none", 11 | "endOfLine": "auto" 12 | } 13 | -------------------------------------------------------------------------------- /classify.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | name: '2工程', 4 | router: [ 5 | { 6 | path: '/spc/list', 7 | component: '@/unuse/App.vue' 8 | }, 9 | { 10 | path: '/spc/list', 11 | component: '@/unuse/main.js' 12 | }, 13 | ] 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /test/__mocks__/fs.ts: -------------------------------------------------------------------------------- 1 | import { fs } from 'memfs' 2 | 3 | fs.mkdirSync('/tmp') 4 | if (process.env.TMPDIR) { 5 | fs.mkdirSync(process.env.TMPDIR, { recursive: true }) 6 | } 7 | 8 | const fsRealpath = fs.realpath 9 | ;(fsRealpath as any).native = fsRealpath 10 | 11 | module.exports = { ...fs, realpath: fsRealpath } 12 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "johnsoncodehk.volar", 4 | "johnsoncodehk.vscode-typescript-vue-plugin", 5 | "dbaeumer.vscode-eslint", 6 | "stylelint.vscode-stylelint", 7 | "esbenp.prettier-vscode", 8 | "christian-kohler.path-intellisense", 9 | "formulahendry.auto-close-tag", 10 | "formulahendry.auto-rename-tag" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/config/jest.setup.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import { createConsola } from 'consola' 3 | const rootPath = process.cwd().replace(/\\/g, '/') 4 | const foldPath = rootPath + '/temp' 5 | const logger = createConsola({ 6 | level: 4 7 | }) 8 | 9 | beforeAll(() => { 10 | logger.info('new unit test start') 11 | fs.ensureDirSync(foldPath) 12 | // 你可以在这里执行一些全局初始化代码 13 | }) 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branch: 'master' 6 | pull_request: 7 | branch: 'master' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 🛎️ 14 | uses: actions/checkout@v2 15 | 16 | - name: Install and test 🔧 17 | run: | 18 | yarn install 19 | npm run test 20 | -------------------------------------------------------------------------------- /test/utils/function-test.ts: -------------------------------------------------------------------------------- 1 | import { replaceName } from '../../src/commands/rename-path' 2 | import { createConsola } from 'consola' 3 | // const rootPath = process.cwd().replace(/\\/g, '/') 4 | const logger = createConsola({ 5 | level: 4 6 | }) 7 | 8 | async function get() { 9 | const p = await replaceName('/path/to/myFile.txt') 10 | logger.info('p: ', p) 11 | logger.info('我这里来了!!!') 12 | } 13 | get() 14 | -------------------------------------------------------------------------------- /unuse/main.js: -------------------------------------------------------------------------------- 1 | //2工程 2 | import { createApp } from 'vue' 3 | // import '../lib/style.css' 4 | import SketchRule from './components/test2/HelloWorld.vue' 5 | // import moduleName from '../api/aa.js'; 6 | const app = createApp(App) 7 | // app.use(SketchRule); 8 | import './mixins.js' 9 | // const MyComponent = app.component('SketchRule') 10 | // console.log(MyComponent, 'MyComponentMyComponent') 11 | app.mount('#app') 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /lib 4 | /es6 5 | codeAndPrompt 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | # unuse 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | base 16 | 2工程 17 | 1工程 18 | coverage 19 | temp 20 | data.json 21 | # Editor directories and files 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | readme-md.md 28 | readme-file.js 29 | /demo 30 | yarn.lock 31 | -------------------------------------------------------------------------------- /test/utils/utils.ts: -------------------------------------------------------------------------------- 1 | /* 测试公共方法 */ 2 | import { writeFile } from 'fs/promises' 3 | 4 | export async function creatFile(file: string) { 5 | const str = `// 我就是个注释 6 | ` 9 | await writeFile(file, str, { encoding: 'utf8' }) 10 | } 11 | 12 | export async function creatFileNoimport(file: string) { 13 | const str = `// 我就是个注释 14 | ` 16 | await writeFile(file, str, { encoding: 'utf8' }) 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/agmd.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 搞个文件做bug测试,命令行不好调试 */ 3 | import { generateAllAction } from './command-actions' 4 | import { getMd } from './wirte-md' 5 | import stringToArgs from '../../script/cli' 6 | import handle from '../../script/cli/handle' 7 | 8 | async function main() { 9 | const options = stringToArgs(process.argv) 10 | const { ignores: ignore, includes: include } = handle(options) 11 | const { md, nodes } =await getMd({ ignore, include }) 12 | await generateAllAction(nodes, md) 13 | } 14 | 15 | main() 16 | -------------------------------------------------------------------------------- /unuse/components/test2/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | //2工程 2 | 3 | 4 | 13 | 14 | 15 | 31 | -------------------------------------------------------------------------------- /router/container/index.js: -------------------------------------------------------------------------------- 1 | import layout from './components/layout' 2 | 3 | const abcd = [ 4 | { 5 | path: '/abcd', 6 | component: layout, 7 | redirect: '/abcd/index', 8 | children: [ 9 | { 10 | path: 'test/deep/user', 11 | name: 'abcd', 12 | component: () => import('@/unuse/components/test/deep/user.vue'), 13 | meta: { 14 | title: '主页', 15 | icon: 'container', 16 | affix: true, 17 | sideType: 1, 18 | hideBread: true 19 | } 20 | } 21 | ] 22 | } 23 | ] 24 | export default abcd 25 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // 定义 Router 接口 2 | export interface Router { 3 | path: string 4 | component: string 5 | } 6 | 7 | export interface RouterItem { 8 | name: string 9 | router: Router[] 10 | } 11 | 12 | export interface OptionType { 13 | ignore?: string[] 14 | include?: string[] 15 | } 16 | 17 | export type ItemType = { 18 | name: string 19 | copyed?: boolean 20 | isDir: boolean 21 | level: number 22 | note: string 23 | size?: number 24 | suffix?: string 25 | rowSize?: number 26 | fullPath: string 27 | belongTo: string[] // 标记归属设置 分类用 28 | imports: string[] // 依赖收集 29 | children?: ItemType[] 30 | } 31 | -------------------------------------------------------------------------------- /unuse/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 16 | 17 | 27 | //2工程 28 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | node: true, 5 | es6: true 6 | }, 7 | extends: ['prettier'], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { 10 | ecmaVersion: 12, 11 | sourceType: 'module' 12 | }, 13 | plugins: ['@typescript-eslint', 'import'], 14 | rules: { 15 | 'no-console': 0, 16 | 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], 17 | 'no-use-before-define': 'off', 18 | 'no-unused-vars': 'warn', 19 | 'import/prefer-default-export': 1, 20 | 'no-shadow': 1, 21 | 'prefer-const': 1, 22 | 'prefer-spread': 1, 23 | 'no-unused-expressions': ['error', { allowShortCircuit: true, allowTernary: true }] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/shared/constant.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | const baseDir = typeof __dirname !== 'undefined' ? __dirname : process.cwd() 5 | const candidates = [ 6 | path.resolve(baseDir, '../../../package.json'), 7 | path.resolve(baseDir, '../../package.json'), 8 | path.resolve(baseDir, '../package.json'), 9 | path.resolve(baseDir, 'package.json') 10 | ] 11 | const pkgPath = candidates.find((p) => fs.existsSync(p)) || candidates[0] 12 | const pkgRaw = fs.existsSync(pkgPath) ? fs.readFileSync(pkgPath, 'utf-8') : '{"name":"agmd","version":""}' 13 | const pkg = JSON.parse(pkgRaw) as { name: string; version: string } 14 | 15 | export const CWD = process.cwd() 16 | export const VERSION = pkg.version 17 | export const PKG_NAME = pkg.name 18 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | // 指定 Jest 环境 3 | testEnvironment: 'node', 4 | // 指定处理 TypeScript 的转换器 5 | transform: { 6 | '^.+\\.ts$': 'ts-jest' 7 | // 'ts-jest': { 8 | // useESM: true, 9 | // }, 10 | }, 11 | // 设置模块文件的扩展名 12 | moduleFileExtensions: ['ts', 'js', 'json', 'node'], 13 | 14 | // 设置需要忽略的文件或目录 15 | testPathIgnorePatterns: ['/node_modules/'], 16 | // 忽略编译产物,避免 Haste 命名冲突与不必要扫描 17 | modulePathIgnorePatterns: ['/es6', '/lib'], 18 | 19 | // 如果使用 ESM,则设置此选项 20 | extensionsToTreatAsEsm: ['.ts'], 21 | globalSetup: './test/config/jest-global-setup.ts', // 全局 22 | setupFilesAfterEnv: ['./test/config/jest.setup.ts'], 23 | clearMocks: true, 24 | // 配置 Jest 如何解析模块,特别是对于 ESM 25 | moduleNameMapper: { 26 | '^(\\.{1,2}/.*)\\.js$': '$1' 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /script/cli/index.ts: -------------------------------------------------------------------------------- 1 | import arg from 'arg' 2 | const stringToArgs = (rawArgs: string[]) => { 3 | const args = arg( 4 | { 5 | '--ignore': String, 6 | '--include': String, 7 | '--version': Boolean, 8 | '--help': Boolean, 9 | '--dry-run': Boolean, 10 | '--silent': Boolean, 11 | '-h': '--help', 12 | '-i': '--ignore', 13 | '-in': '--include', 14 | '-v': '--version', 15 | '-d': '--dry-run', 16 | '-s': '--silent' 17 | }, 18 | { 19 | argv: rawArgs.slice(2) 20 | } 21 | ) 22 | return { 23 | help: args['--help'], 24 | ignore: args['--ignore'], 25 | include: args['--include'], 26 | version: args['--version'], 27 | dryRun: args['--dry-run'], 28 | silent: args['--silent'] 29 | } 30 | } 31 | 32 | export default stringToArgs 33 | -------------------------------------------------------------------------------- /script/help/index.ts: -------------------------------------------------------------------------------- 1 | const st = `使用说明: 2 | 1. 在控制台按上下切换功能并回车进行确认, 执行相对应的操作! 3 | 2. 可以在 package.json 的 scripts 下配置如下, 然后运行命令: 4 | agmd --include --ignore [--dry-run] [--silent] 5 | 6 | 选项: 7 | --include string / -in string.......... 包括文件扩展名 (以空格分隔) 8 | --ignore string / -i string........... 忽略文件或文件夹 (以空格分隔) 9 | --dry-run / -d.................. 预演模式, 不对文件系统进行写入 10 | --silent / -s.................. 静默模式, 最小化日志输出 11 | 12 | 默认配置: 13 | --ignore img,styles,node_modules,LICENSE,.git,.github,dist,.husky,.vscode,readme-file.js,readme-md.js 14 | --include .js,.vue,.ts,.tsx 15 | 16 | 示例: 17 | $ agmd --ignore lib node_modules dist --include .js .ts .vue --dry-run --silent` 18 | 19 | function help() { 20 | console.log(st) 21 | process.exit(0) 22 | } 23 | export default help 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "skipLibCheck": true, 6 | "sourceMap": true, 7 | "outDir": "lib", 8 | "moduleResolution": "node", 9 | "removeComments": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "strictFunctionTypes": true, 13 | "noImplicitThis": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "allowSyntheticDefaultImports": true, 19 | "esModuleInterop": true, 20 | "emitDecoratorMetadata": true, 21 | "experimentalDecorators": true, 22 | "resolveJsonModule": true, 23 | "baseUrl": ".", 24 | "strict": true, 25 | "isolatedModules": true 26 | }, 27 | "exclude": ["node_modules"], 28 | "include": ["src/**/*.ts"] 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // { 2 | // "version": "0.2.0", 3 | // "configurations": [ 4 | // { 5 | // "type": "node", 6 | // "request": "launch", 7 | // "name": "funtion-test", 8 | // "runtimeExecutable": "nodemon", 9 | // "program": "${workspaceFolder}\\test\\utils\\function-test.ts", 10 | // "restart": true, 11 | // "console": "integratedTerminal", 12 | // "internalConsoleOptions": "neverOpen" 13 | // } 14 | // ] 15 | // } 16 | 17 | { 18 | "version": "0.2.0", 19 | "configurations": [ 20 | { 21 | "type": "node", 22 | "request": "launch", 23 | "name": "creat", 24 | "runtimeExecutable": "nodemon", 25 | "program": "${workspaceFolder}\\test\\utils\\creat.js", 26 | "restart": true, 27 | "console": "integratedTerminal", 28 | "internalConsoleOptions": "neverOpen" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /test/config/jest-global-setup.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import { createConsola } from 'consola' 3 | const rootPath = process.cwd().replace(/\\/g, '/') 4 | const logger = createConsola({ 5 | level: 4 6 | }) 7 | 8 | module.exports = async () => { 9 | logger.start('清空测试文件夹') 10 | // 你可以在这里执行一些全局初始化代码 11 | const foldPath = rootPath + '/temp' 12 | const foldPath2 = rootPath + '/test2' 13 | function deleteFolderRecursive(p: string) { 14 | if (fs.existsSync(p)) { 15 | fs.readdirSync(p).forEach((file) => { 16 | const curPath = `${p}/${file}` 17 | if (fs.lstatSync(curPath).isDirectory()) { 18 | deleteFolderRecursive(curPath) 19 | } else { 20 | fs.unlinkSync(curPath) 21 | } 22 | }) 23 | fs.rmdirSync(p) 24 | } 25 | } 26 | 27 | deleteFolderRecursive(foldPath) 28 | deleteFolderRecursive(foldPath2) 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.es6.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "es2020", 5 | "skipLibCheck": true, 6 | "sourceMap": true, 7 | "outDir": "es6", 8 | "moduleResolution": "node", 9 | "removeComments": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "strictFunctionTypes": true, 13 | "noImplicitThis": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "allowSyntheticDefaultImports": true, 19 | "esModuleInterop": true, 20 | "emitDecoratorMetadata": true, 21 | "experimentalDecorators": true, 22 | "resolveJsonModule": true, 23 | "baseUrl": ".", 24 | "strict": true, 25 | "isolatedModules": true 26 | }, 27 | "exclude": ["node_modules"], 28 | "include": ["src/**/*.ts"] 29 | } 30 | -------------------------------------------------------------------------------- /script/cli/handle.ts: -------------------------------------------------------------------------------- 1 | import help from '../help' 2 | import pkg from '../../package.json' 3 | interface parseType { 4 | version?: Boolean | undefined 5 | includes?: string[] 6 | ignores?: string[] 7 | help?: Boolean | undefined 8 | ignore?: string | undefined 9 | include?: string | undefined 10 | dryRun?: Boolean | undefined 11 | silent?: Boolean | undefined 12 | } 13 | function handle(settings: parseType) { 14 | if (settings.help) { 15 | help() 16 | } 17 | if (settings.version) { 18 | console.log(`agmd version is: ` + '\x1B[36m%s\x1B[0m', pkg.version) 19 | process.exit(0) 20 | } 21 | if (settings.ignore) { 22 | settings.ignores = settings.ignore.split(' ') 23 | } 24 | if (settings.include) { 25 | settings.includes = settings.include.split(' ') 26 | } 27 | if (settings.dryRun) { 28 | process.env.AGMD_DRY_RUN = '1' 29 | } 30 | if (settings.silent) { 31 | process.env.AGMD_SILENT = '1' 32 | } 33 | return settings 34 | } 35 | 36 | export default handle 37 | -------------------------------------------------------------------------------- /test/rename-path.test.ts: -------------------------------------------------------------------------------- 1 | import { replaceName } from '../src/commands/rename-path' 2 | import fs from 'fs-extra' 3 | import { vol } from 'memfs' 4 | 5 | jest.mock('fs') 6 | describe('replaceName function tests', () => { 7 | beforeEach(async () => { 8 | vol.reset() 9 | await fs.mkdirp('./pathA/to') 10 | fs.writeFileSync('./pathA/to/example-file.txt', 'file contents') 11 | fs.writeFileSync('./pathA/to/FoolFile.txt', 'fool contents') 12 | }) 13 | 14 | test('should rename a file to camelCase', async () => { 15 | const fullPath = './pathA/to/example-file.txt' 16 | const result = await replaceName(fullPath, true) 17 | expect(result.newName).toBe('ExampleFile.txt') 18 | expect(result.filename).toBe('example-file.txt') 19 | }) 20 | 21 | test('should rename a file to kebab-case', async () => { 22 | const fullPath = './pathA/to/FoolFile.txt' 23 | const result = await replaceName(fullPath) 24 | expect(result.newName).toBe('fool-file.txt') 25 | expect(result.filename).toBe('FoolFile.txt') 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /test/mark-write-file.test.ts: -------------------------------------------------------------------------------- 1 | // import { createConsola } from 'consola' 2 | import fs from 'fs-extra' 3 | const rootPath = process.cwd().replace(/\\/g, '/') 4 | 5 | import { setDispFileNew, markWriteFile } from '../src/commands/mark-write-file' 6 | import { nodeOne } from './utils/nodes-test' 7 | // const logger = createConsola({ 8 | // level: 4 9 | // }) 10 | describe('mark-write-file.test的测试', () => { 11 | test('setDispFileNew--找到文件然后copy文件', (done) => { 12 | const file = rootPath + '/temp/app-file-test.vue' 13 | try { 14 | fs.ensureFileSync(file) 15 | async function get() { 16 | await setDispFileNew(file, 'base') 17 | done() 18 | } 19 | get() 20 | } catch (error) { 21 | done(error) 22 | } 23 | }) 24 | 25 | test('mark-write-file--递归打标记', (done) => { 26 | const file = rootPath + '/temp/app-file-test.vue' 27 | try { 28 | async function get() { 29 | await markWriteFile(nodeOne, 'base', file) 30 | done() 31 | } 32 | get() 33 | } catch (error) { 34 | done(error) 35 | } 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /test/wirte-md.test.ts: -------------------------------------------------------------------------------- 1 | import type { ItemType } from '../src/types' 2 | 3 | import { getCountMd, setCountMd } from '../src/commands/wirte-md' 4 | import { nodeOne } from './utils/nodes-test' 5 | 6 | describe('getCountMd', () => { 7 | it('should correctly count the total number of rows and size', () => { 8 | const datas: ItemType[] = [...nodeOne] 9 | const result = getCountMd(datas) 10 | expect(result).toEqual({ 11 | rowTotleNumber: 4 + 4 + 4, 12 | sizeTotleNumber: 96 * 3, 13 | coutObj: { 14 | '.vue': 3 15 | } 16 | }) 17 | }) 18 | }) 19 | 20 | describe('setCountMd', () => { 21 | it('should correctly format the count string', () => { 22 | const obj = { 23 | rowTotleNumber: 4 + 4 + 4, 24 | sizeTotleNumber: 96 * 3, 25 | coutObj: { 26 | '.vue': 3 27 | } 28 | } 29 | 30 | const result = setCountMd(obj) 31 | const expected = 32 | '😍 代码总数统计:\n' + 33 | '后缀是 .vue 的文件有 3 个\n' + 34 | '总共有 3 个文件\n' + 35 | '总代码行数有: 12行,\n' + 36 | '总代码字数有: 288个\n' 37 | 38 | expect(result).toEqual(expected) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils/router-utils.ts: -------------------------------------------------------------------------------- 1 | 2 | import { access, readFile } from 'fs/promises'; 3 | /** 4 | * 解析路由文件中的路由路径。 5 | * @param {string} line - 路由文件中的一行。 6 | * @return {string} - 解析出的路由路径。 7 | */ 8 | export function parseRouterPath(line: string): string { 9 | const pathRegex = /path:\s*['"]([^'"]+)['"]/ 10 | const match = line.match(pathRegex) 11 | return match ? match[1] : '' 12 | } 13 | 14 | /** 15 | * 解析路由文件中的组件路径。 16 | * @param {string} line - 路由文件中的一行。 17 | * @return {string | ''} - 解析出的组件路径或null。 18 | */ 19 | export function parseComponentPath(line: string): string { 20 | const componentRegex = /component:\s*\(\)\s*=>\s*import\(['"]([^'"]+)['"]\)/ 21 | const match = line.match(componentRegex) 22 | return match ? match[1] : '' 23 | } 24 | 25 | export async function getDependencies(packageJsonPath: string): Promise { 26 | let dependencies: string[] = []; 27 | if (packageJsonPath) { 28 | try { 29 | await access(packageJsonPath); 30 | const pkg = JSON.parse(await readFile(packageJsonPath, 'utf-8')); 31 | dependencies = Object.keys(pkg.dependencies || {}).concat(Object.keys(pkg.devDependencies || {})); 32 | } catch (error) { 33 | console.error(error); 34 | } 35 | } 36 | return dependencies; 37 | } 38 | -------------------------------------------------------------------------------- /router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import abcd from './container/index' 4 | export const routers = [ 5 | ...abcd, 6 | { 7 | path: '/dashboard', 8 | name: 'dashboard', 9 | redirect: '/dashboard/workplace', 10 | meta: { title: '仪表盘', icon: 'dashboard', permission: ['dashboard'] }, 11 | children: [ 12 | { 13 | path: '/dashboard/analysis', 14 | name: 'Analysis', 15 | component: () => import('@/unuse/components/test2/HelloWorld.vue'), 16 | meta: { title: '分析页', permission: ['dashboard'] } 17 | }, 18 | { 19 | path: '/app', 20 | name: 'Monitor', 21 | hidden: true, 22 | component: () => import('@/unuse/App'), 23 | meta: { title: '监控页', permission: ['dashboard'] } 24 | } 25 | ] 26 | }, 27 | { 28 | path: '/form', 29 | redirect: '/form/basic-form', 30 | component: PageView, 31 | meta: { title: '表单页', icon: 'form', permission: ['form'] }, 32 | children: [ 33 | { 34 | path: '/form/base-form', 35 | name: 'BaseForm', 36 | component: () => import('@/unuse/components/user-rulerts.vue'), 37 | meta: { title: '基础表单', permission: ['form'] } 38 | } 39 | ] 40 | } 41 | ] 42 | Vue.use(Router) 43 | export default new Router({ 44 | mode: 'history', 45 | base: process.env.BASE_URL, 46 | scrollBehavior: () => ({ y: 0 }), 47 | routes: routers 48 | }) 49 | -------------------------------------------------------------------------------- /test/change-path-absolute.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { readFile, writeFile } from 'fs/promises' 3 | import { changeImport, writeToFile } from '../src/commands/change-path' 4 | import { nodeOne } from './utils/nodes-test' 5 | import { createConsola } from 'consola' 6 | 7 | const logger = createConsola({ level: 4 }) 8 | const rootPath = process.cwd().replace(/\\/g, '/') 9 | 10 | describe('change-path 绝对路径(@别名)测试', () => { 11 | test('changeImport--转换为 @ 别名的绝对路径', () => { 12 | const result = changeImport( 13 | "import { getRelatPath, makeSuffix, changeImport } from '../unuse/components/user-rulerts'", 14 | path.resolve('unuse/App.vue').replace(/\\/g, '/'), 15 | ['@types/node'], 16 | false, 17 | true 18 | ) 19 | expect(result).toEqual({ 20 | filePath: '../unuse/components/user-rulerts', 21 | impName: '@/unuse/components/user-rulerts.vue', 22 | absoluteImport: rootPath + '/unuse/components/user-rulerts.vue' 23 | }) 24 | }) 25 | 26 | test('writeToFile--将相对路径改为 @ 别名绝对路径', async () => { 27 | const node = nodeOne[0] 28 | const str = `` 29 | 30 | const file = path.resolve(rootPath, node.fullPath) 31 | await writeFile(file, str, { encoding: 'utf8' }) 32 | logger.success('Write successful') 33 | await writeToFile(node, true, false, true) 34 | const getStr = await readFile(file, 'utf-8') 35 | expect(getStr.includes("from '@/unuse/components/user-rulerts.vue'")).toBe(true) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /src/commands/mark-write-file.ts: -------------------------------------------------------------------------------- 1 | import { findNodes } from './mark-file' 2 | import type { ItemType } from '../types' 3 | import fs from 'fs-extra' 4 | import { createConsola } from 'consola' 5 | const logger = createConsola({ 6 | level: process.env.AGMD_SILENT === '1' ? 0 : 4 7 | }) 8 | const rootPath = process.cwd().replace(/\\/g, '/') 9 | 10 | /** 11 | * 递归文件子依赖创建文件。文件外递归。 12 | * @param nodes - 节点列表 13 | * @param name - 文件名 14 | * @param path - 绝对路径 15 | */ 16 | export async function markWriteFile(nodes: ItemType[], name: string, path: string): Promise { 17 | // logger.info('入参: ', name, path) 18 | const node = findNodes(nodes, path) 19 | // logger.info('查找的node: ', node) 20 | if (!node || node.copyed) return 21 | node.copyed = true 22 | if (node.belongTo.length > 0) { 23 | await setDispFileNew(path, name) 24 | } 25 | if (node.imports) { 26 | for (const element of node.imports) { 27 | if (await fs.pathExists(element)) { 28 | await markWriteFile(nodes, name, element) 29 | } else { 30 | logger.error(`${element} 文件不存在`) 31 | } 32 | } 33 | } 34 | } 35 | 36 | /** 37 | * 复制文件到指定位置。 38 | * @param pathN - 源文件路径 39 | * @param name - 目标文件夹名 40 | */ 41 | export async function setDispFileNew(pathN: string, name: string): Promise { 42 | const relative = pathN.replace(rootPath, '') 43 | const writeFileName = `${rootPath}/${name}${relative}` 44 | try { 45 | if (await fs.pathExists(writeFileName)) return 46 | if (process.env.AGMD_DRY_RUN === '1') { 47 | logger.info('Dry-run: would copy file to: ', writeFileName) 48 | } else { 49 | await fs.copy(pathN, writeFileName) 50 | logger.success('写入文件success! : ', writeFileName) 51 | } 52 | } catch (err) { 53 | logger.error('文件写入失败') 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/utils/deep-nodes-test.ts: -------------------------------------------------------------------------------- 1 | const rootPath = process.cwd().replace(/\\/g, '/') 2 | const nodeComponents = [ 3 | { 4 | name: 'test', 5 | isDir: true, 6 | level: 0, 7 | note: '', 8 | imports: [], 9 | belongTo: [], 10 | children: [ 11 | { 12 | name: 'deep', 13 | isDir: true, 14 | level: 1, 15 | note: '', 16 | imports: [], 17 | belongTo: [], 18 | children: [ 19 | { 20 | name: 'user.vue', 21 | isDir: false, 22 | level: 2, 23 | note: '//2工程', 24 | imports: [rootPath + '/api/user.js'], 25 | belongTo: [], 26 | size: 1791, 27 | rowSize: 109, 28 | suffix: '.vue', 29 | fullPath: rootPath + '/unuse/components/test/deep/user.vue' 30 | } 31 | ], 32 | fullPath: rootPath + '/unuse/components/test/deep' 33 | } 34 | ], 35 | fullPath: rootPath + '/unuse/components/test' 36 | }, 37 | { 38 | name: 'test2', 39 | isDir: true, 40 | level: 0, 41 | note: '', 42 | imports: [], 43 | belongTo: [], 44 | children: [ 45 | { 46 | name: 'HelloWorld.vue', 47 | isDir: false, 48 | level: 1, 49 | note: '//2工程', 50 | imports: [rootPath + '/unuse/components/test/deep/user.vue'], 51 | belongTo: [], 52 | size: 411, 53 | rowSize: 31, 54 | suffix: '.vue', 55 | fullPath: rootPath + '/unuse/components/test2/HelloWorld.vue' 56 | } 57 | ], 58 | fullPath: rootPath + '/unuse/components/test2' 59 | }, 60 | { 61 | name: 'user-rulerts.vue', 62 | isDir: false, 63 | level: 0, 64 | note: '', 65 | imports: [rootPath + '/unuse/components/test/deep/user.vue'], 66 | belongTo: [], 67 | size: 2503, 68 | rowSize: 105, 69 | suffix: '.vue', 70 | fullPath: rootPath + '/unuse/components/user-rulerts.vue' 71 | } 72 | ] 73 | 74 | export default nodeComponents 75 | -------------------------------------------------------------------------------- /test/get-router.test.ts: -------------------------------------------------------------------------------- 1 | import { getRouter, getRouterFilePath, getAllRouter } from '../src/commands/get-router' 2 | import { parseRouterPath, parseComponentPath } from '../src/utils/router-utils' 3 | const rootPath = process.cwd().replace(/\\/g, '/') 4 | const dir = rootPath + '/router' 5 | describe('getRouter的测试', () => { 6 | test('测试正则工具:parseRouterPath', () => { 7 | const st = " path: '/form/base-form'," 8 | const pathSt = parseRouterPath(st) 9 | expect(pathSt).toBe('/form/base-form') 10 | }) 11 | test('测试正则工具:parseComponentPath', () => { 12 | const st = " component: () => import('@/unuse/components/user-rulerts.vue')," 13 | const componentSt = parseComponentPath(st) 14 | expect(componentSt).toBe('@/unuse/components/user-rulerts.vue') 15 | }) 16 | test('getRouter--获取路由', async () => { 17 | const p = rootPath + '/router/index.js' 18 | const arrs = await getRouter(p) 19 | const routerArrs = [ 20 | { 21 | path: '/dashboard/analysis', 22 | component: '@/unuse/components/test2/HelloWorld.vue' 23 | }, 24 | { path: '/app', component: '@/unuse/App' }, 25 | { 26 | path: '/form/base-form', 27 | component: '@/unuse/components/user-rulerts.vue' 28 | } 29 | ] 30 | expect(arrs).toMatchObject(routerArrs) 31 | }) 32 | 33 | test('getRouterFilePath--递归获取路由数组', async () => { 34 | const arrs = await getRouterFilePath(dir) 35 | const routerArrs = [rootPath + '/router/container/index.js', rootPath + '/router/index.js'] 36 | expect(arrs).toMatchObject(routerArrs) 37 | }) 38 | 39 | test('getAllRouter--获取所有路由', async () => { 40 | const arrs = await getAllRouter(dir) 41 | const routerArrs = [ 42 | { 43 | component: '@/unuse/components/test/deep/user.vue', 44 | path: 'test/deep/user' 45 | }, 46 | { 47 | component: '@/unuse/components/test2/HelloWorld.vue', 48 | path: '/dashboard/analysis' 49 | }, 50 | { 51 | component: '@/unuse/App', 52 | path: '/app' 53 | }, 54 | { 55 | component: '@/unuse/components/user-rulerts.vue', 56 | path: '/form/base-form' 57 | } 58 | ] 59 | expect(arrs).toMatchObject(routerArrs) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /unuse/components/test/deep/user.vue: -------------------------------------------------------------------------------- 1 | //2工程 2 | //2工程 3 | ue2.0写法 */ 4 | 33 | 39 | 98 | 99 | //1工程 100 | //1工程 101 | //2工程 102 | //2工程 103 | //2工程 104 | //2工程 105 | //2工程 106 | //2工程 107 | //2工程 108 | //2工程 109 | -------------------------------------------------------------------------------- /README.EN.md: -------------------------------------------------------------------------------- 1 | # agmd (auto generate md) 2 | 3 | > A CLI and library that scans your project to generate Markdown docs for directory structures, counts code metrics, and provides utilities to normalize imports, rename paths, and classify files via routes. 4 | 5 | ## Features 6 | - Count files and total lines/characters in the project 7 | - Auto-complete missing suffixes like `.js` and `.vue` 8 | - Rename files/folders from CamelCase to Kebab-Case 9 | - Convert imports from absolute to relative paths for easy navigation 10 | - Classify files by routes and export JSON of nodes 11 | - Interactive CLI for all operations 12 | - Output the full tree as JSON 13 | - TypeScript implementation with extensive tests 14 | - Convert relative imports to absolute alias-based paths using `@` (new) 15 | 16 | ## Usage 17 | - Global: `npm i agmd -g` 18 | - Local: `npm i agmd -D` 19 | - Run: `agmd` in the directory you want to document 20 | 21 | The generated Markdown (`readme-md.md`) contains: 22 | - A “Directory Structure” section 23 | - A “Statistics” section with totals per suffix, lines and characters 24 | 25 | ## Scripts 26 | Add to `package.json`: 27 | 28 | `npx agmd --ignore lib,node_modules,dist --include .js,.ts,.vue [--dry-run] [--silent]` 29 | 30 | ## CLI Commands (interactive) 31 | - Help 32 | - Generate Structure Markdown 33 | - Change Relative Path 34 | - Change Absolute Path 35 | - Completion suffix 36 | - Rename folders to Kebab-Case 37 | - Rename files to Kebab-Case 38 | - Record nodes as JSON 39 | - Mark files for classification 40 | - Delete marks 41 | - Classification 42 | 43 | ## CLI Options 44 | - `--include string` / `-in string` Include suffixes (space-separated) 45 | - `--ignore string` / `-i string` Ignore file or directory names (space-separated) 46 | - `--dry-run` / `-d` Preview changes without writing to disk 47 | - `--silent` / `-s` Minimize logs 48 | 49 | Defaults: 50 | - `--ignore` img,styles,node_modules,LICENSE,.git,.github,dist,.husky,.vscode,readme-file.js,readme-md.js 51 | - `--include` .js,.vue,.ts 52 | 53 | Example: 54 | `agmd --ignore lib node_modules dist --include .js .ts .vue --dry-run --silent` 55 | 56 | ## Advanced 57 | Create `classify.js` at the project root to define route-based classification. Use `@` alias paths in the config. If missing, the tool scans `router/` automatically. 58 | 59 | ## API 60 | - `getFileNodes(option?)` Get detailed file info 61 | - `getMd(option?)` Get composed Markdown string and nodes 62 | 63 | `option: { ignore?: string[]; include?: string[] }` 64 | 65 | ## Notes 66 | - Prefer running path operations inside `src` due to alias resolution conventions 67 | - Dry-run and silent modes are supported across write operations -------------------------------------------------------------------------------- /test/change-path.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { readFile, writeFile } from 'fs/promises' 3 | import { getRelatPath, makeSuffix, changeImport, writeToFile, getImportName } from '../src/commands/change-path' 4 | import { nodeOne } from './utils/nodes-test' 5 | import { createConsola } from 'consola' 6 | const logger = createConsola({ 7 | level: 4 8 | }) 9 | const rootPath = process.cwd().replace(/\\/g, '/') 10 | describe('change-path的测试', () => { 11 | test('getRelatPath--获取相对地址', () => { 12 | expect(getRelatPath('/unuse/components/user-rulerts.vue', '/unuse/App.vue')).toEqual( 13 | './components/user-rulerts.vue' 14 | ) 15 | }) 16 | 17 | test('makeSuffix--补全后缀和@替换', () => { 18 | expect(makeSuffix('@/src/commands/change-path', '@/src/commands/change-path')).toEqual( 19 | path.resolve('src/commands/change-path.ts').replace(/\\/g, '/') 20 | ) 21 | }) 22 | test('makeSuffix--得到import', () => { 23 | const arrs = getImportName( 24 | `import 25 | { getRelatPath, 26 | makeSuffix, 27 | changeImport 28 | } from '@/unuse/components/user-rulerts'`, 29 | ['@types/node'] 30 | ) 31 | logger.info('arrs: ', arrs) 32 | expect(arrs).toEqual('@/unuse/components/user-rulerts') 33 | }) 34 | 35 | test('changeImport--更改不规范path', () => { 36 | expect( 37 | changeImport( 38 | "import { getRelatPath, makeSuffix, changeImport } from '@/unuse/components/user-rulerts'", 39 | path.resolve('unuse/App.vue').replace(/\\/g, '/'), 40 | ['@types/node'] 41 | ) 42 | ).toEqual({ 43 | filePath: '@/unuse/components/user-rulerts', 44 | impName: './components/user-rulerts.vue', 45 | absoluteImport: rootPath + '/unuse/components/user-rulerts.vue' 46 | }) 47 | }) 48 | 49 | test('writeToFile--更改不规范path', (done) => { 50 | try { 51 | const node = nodeOne[0] 52 | // 1. 随机创建一个文件 53 | const str = `` 59 | //2. 预期得到内容 60 | const finalStr = `` 66 | 67 | const file = path.resolve(rootPath, node.fullPath) 68 | logger.info('file: ', file) 69 | 70 | async function get() { 71 | // 异步写入数据到文件 72 | await writeFile(file, str, { encoding: 'utf8' }) 73 | logger.success('Write successful') 74 | await writeToFile(node, true) 75 | const getStr = await readFile(file, 'utf-8') 76 | expect(getStr).toEqual(finalStr) 77 | done() 78 | } 79 | get() 80 | } catch (error) { 81 | done(error) 82 | } 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agmd", 3 | "version": "0.4.0", 4 | "description": "Automatically generate a directory structure for any file or folder, create Markdown documentation for nodes, classify engineering documents, and convert absolute paths to relative paths.", 5 | "keywords": [ 6 | "fs", 7 | "filesystem", 8 | "fs.js", 9 | "memory-fs", 10 | "file", 11 | "file system", 12 | "mount", 13 | "memory", 14 | "mock", 15 | "in-memory", 16 | "directory", 17 | "auto", 18 | "generate", 19 | "relative path", 20 | "md", 21 | "classification", 22 | "markdown" 23 | ], 24 | "main": "lib/src/index.js", 25 | "module": "es6/src/index.js", 26 | "bin": { 27 | "agmd": "lib/src/bin.js" 28 | }, 29 | "scripts": { 30 | "build": "tsc&&tsc --project tsconfig.es6.json", 31 | "devtsc": "tsc --watch --noEmit --strict src/commands/agmd.ts", 32 | "cli": "ts-node src/bin.ts", 33 | "dev": "ts-node-dev src/commands/agmd.ts", 34 | "agmd": "npx agmd --ignore lib,node_modules,dist --include .js,.ts,.vue", 35 | "lint": "eslint --fix --ext .js,.ts ./src", 36 | "prepare": "husky install", 37 | "test": "jest", 38 | "test:coverage": "jest --coverage", 39 | "testdev": "jest --watch ", 40 | "lint-fix": "eslint --fix --ext .js,.ts" 41 | }, 42 | "author": "kakajun <253495832@qq.com>", 43 | "repository": { 44 | "type": "git", 45 | "url": "git@github.com:kakajun/auto-generate-md.git" 46 | }, 47 | "dependencies":{ 48 | "consola": "^3.2.3", 49 | "fs-extra": "^11.2.0", 50 | "arg": "5.0.2", 51 | "commander": "^11.1.0", 52 | "node-environment": "^0.5.1", 53 | "prompts": "^2.4.2", 54 | "@types/prompts": "^2.4.9" 55 | }, 56 | "devDependencies": { 57 | "@types/debug": "^4.1.12", 58 | "@types/fs-extra": "^11.0.4", 59 | "@types/jest": "^29.5.11", 60 | "@types/node": "^20.11.10", 61 | "eslint-plugin-import": "^2.29.1", 62 | "@typescript-eslint/eslint-plugin": "^6.19.1", 63 | "@typescript-eslint/parser": "^6.19.1", 64 | "eslint": "^8.56.0", 65 | "eslint-config-prettier": "^9.1.0", 66 | "husky": "^9.0.6", 67 | "jest": "^29.7.0", 68 | "lint-staged": "^15.2.0", 69 | "nodemon": "^3.0.3", 70 | "prettier": "^3.2.4", 71 | "ts-jest": "^29.1.2", 72 | "ts-node": "^10.9.2", 73 | "typescript": "^5.3.3", 74 | "ts-node-dev":"^2.0.0", 75 | "memfs":"^4.9.2", 76 | "raf-stub":"3.0.0" 77 | }, 78 | "lint-staged": { 79 | "*.{ts,tsx,js}": "prettier --write", 80 | "*.{ts,tsx}": "eslint --fix" 81 | }, 82 | "license": "MIT", 83 | "bugs": { 84 | "url": "https://github.com/kakajun/auto-generate-md/issues" 85 | }, 86 | "homepage": "https://github.com/kakajun/auto-generate-md", 87 | "files": [ 88 | "es6", 89 | "lib" 90 | ] 91 | } 92 | -------------------------------------------------------------------------------- /unuse/test/user-rulerts.vue: -------------------------------------------------------------------------------- 1 | 2 | 104 | //2工程 105 | -------------------------------------------------------------------------------- /unuse/components/user-rulerts.vue: -------------------------------------------------------------------------------- 1 | 2 | 104 | //2工程 105 | -------------------------------------------------------------------------------- /src/commands/get-router.ts: -------------------------------------------------------------------------------- 1 | import { readdir, readFile, stat, access } from 'fs/promises' 2 | import { createConsola } from 'consola' 3 | import path from 'path' 4 | import { parseRouterPath, parseComponentPath } from '../utils/router-utils' 5 | import type { Router, RouterItem } from '../types' 6 | const logger = createConsola({ 7 | level: process.env.AGMD_SILENT === '1' ? 0 : 4 8 | }) 9 | 10 | const rootPath = process.cwd().replace(/\\/g, '/') 11 | 12 | /** 13 | * @desc: 递归获取路由数组 14 | */ 15 | export async function getRouterFilePath(dir: string): Promise { 16 | const routes: string[] = [] 17 | 18 | async function finder(p: string): Promise { 19 | const files = await readdir(p) 20 | for (const val of files) { 21 | const fPath = path.join(p, val).replace(/\\/g, '/') 22 | const stats = await stat(fPath) 23 | if (stats.isDirectory()) { 24 | await finder(fPath) 25 | } else if (stats.isFile()) { 26 | routes.push(fPath) 27 | } 28 | } 29 | } 30 | 31 | await finder(dir) 32 | return routes 33 | } 34 | 35 | /** 36 | * @desc: 获取所有路由 37 | 38 | */ 39 | export async function getAllRouter(dir: string): Promise { 40 | const filePaths = await getRouterFilePath(dir) 41 | const routers: Router[] = [] 42 | 43 | for (const filePath of filePaths) { 44 | const routerItems = await getRouter(filePath) 45 | routers.push(...routerItems) 46 | } 47 | 48 | return routers 49 | } 50 | /** 51 | * @desc: 得到路由 52 | 53 | */ 54 | export async function getRouter(routerPath: string): Promise { 55 | const routers: Router[] = [] 56 | try { 57 | // 检查文件是否存在 58 | await access(routerPath) 59 | const fileContent = await readFile(routerPath, 'utf-8') 60 | const lines = fileContent.split(/\n/g) 61 | let currentPath = '' 62 | let currentComponent = '' 63 | lines.forEach((line) => { 64 | if (line.includes('//')) return // 跳过注释行 65 | const tempPath = parseRouterPath(line) 66 | if (tempPath) currentPath = tempPath 67 | const tempComponent = parseComponentPath(line) 68 | if (tempComponent) currentComponent = tempComponent 69 | if (currentPath && currentComponent) { 70 | routers.push({ path: currentPath, component: currentComponent }) 71 | currentPath = '' 72 | currentComponent = '' 73 | } 74 | }) 75 | } catch (error) { 76 | console.error('读取路由配置时出错:', error) 77 | // 可以根据需要处理或抛出错误 78 | } 79 | 80 | return routers 81 | } 82 | 83 | /** 84 | * @desc: 获取要操作的路由 85 | */ 86 | export async function getRouterArrs(): Promise { 87 | const pathName = `${rootPath}/classify.js` 88 | const dir = `${rootPath}/router` 89 | let routers: RouterItem[] | null = null 90 | try { 91 | if (await stat(pathName)) { 92 | const mod: any = await import(pathName) 93 | routers = (mod && mod.default) ? mod.default : mod 94 | } else { 95 | // 如果没有classify.js,则直接找路由 96 | routers = [ 97 | { 98 | name: 'mark', 99 | router: await getAllRouter(dir) 100 | } 101 | ] 102 | } 103 | } catch (error) { 104 | logger.error('根路径没有发现 classify.js,并且 src 里面没有 router 文件,现在退出') 105 | process.exit(1) 106 | } 107 | return routers 108 | } 109 | -------------------------------------------------------------------------------- /test/get-file.test.ts: -------------------------------------------------------------------------------- 1 | import { getFile, getImport, getFileNodes, getNote, setMd } from '../src/commands/get-file' 2 | import { creatFile } from './utils/utils' 3 | import type { ItemType } from '../src/types' 4 | import deepNodes from './utils/deep-nodes-test' 5 | import { createConsola } from 'consola' 6 | const rootPath = process.cwd().replace(/\\/g, '/') 7 | const logger = createConsola({ 8 | level: 4 9 | }) 10 | 11 | // 由于linux的空格数和window的空格数不一样, 所以size始终不一样, 无法测试, 所以这里干掉size 12 | // 递归树结构设置size为0 13 | function setSize(temparrs: any[]) { 14 | temparrs.forEach((item) => { 15 | item.size = 0 16 | if (item.children) { 17 | setSize(item.children) 18 | } 19 | }) 20 | } 21 | 22 | describe('setMd', () => { 23 | it('should correctly format the string for a directory', () => { 24 | const obj: ItemType = { 25 | name: 'dir', 26 | isDir: true, 27 | level: 1, 28 | note: '', 29 | fullPath: '', 30 | belongTo: [], 31 | imports: [] 32 | } 33 | 34 | const result = setMd(obj, false) 35 | 36 | expect(result).toEqual('│ ├── dir\n') 37 | }) 38 | 39 | it('should correctly format the string for a file', () => { 40 | const obj: ItemType = { 41 | name: 'file.js', 42 | isDir: false, 43 | level: 1, 44 | note: 'note', 45 | fullPath: '', 46 | belongTo: [], 47 | imports: [] 48 | } 49 | const result = setMd(obj, true) 50 | expect(result).toEqual('│ └── file.js note\n') 51 | }) 52 | }) 53 | 54 | describe('get-file的测试', () => { 55 | test('getFile--获取注释', (done) => { 56 | const file = rootPath + '/temp/app-file-test.vue' 57 | const file2 = rootPath + '/temp/aa.vue' 58 | try { 59 | async function get() { 60 | await creatFile(file) 61 | await creatFile(file2) 62 | const obj = await getFile(file) 63 | expect(obj).toEqual({ 64 | note: '// 我就是个注释', 65 | rowSize: 4, 66 | size: 63, 67 | imports: [rootPath + '/temp/aa.vue'] 68 | }) 69 | done() 70 | } 71 | get() 72 | } catch (error) { 73 | done(error) 74 | } 75 | }) 76 | 77 | test('getImport--获取每个文件依赖的方法', (done) => { 78 | const str = `` 81 | try { 82 | async function get() { 83 | const sarr = str.split(/[\n]/g) 84 | const arrs = await getImport(sarr, rootPath + '/temp/bb.vue') 85 | expect(arrs).toMatchObject([rootPath + '/unuse/components/user-rulerts.vue']) 86 | done() 87 | } 88 | get() 89 | } catch (error) { 90 | done(error) 91 | } 92 | }) 93 | 94 | test('getFileNodes--生成所有文件的node信息', (done) => { 95 | try { 96 | async function get() { 97 | const arrs = await getFileNodes(rootPath + '/unuse/components') 98 | setSize(arrs) 99 | setSize(deepNodes) 100 | // console.log(JSON.stringify(deepNodes), 'arrs') 101 | expect(arrs).toMatchObject(deepNodes) 102 | 103 | done() 104 | } 105 | get() 106 | } catch (error) { 107 | logger.error(error) 108 | done(error) 109 | } 110 | }) 111 | 112 | test('getImport--获取每个文件依赖的方法', (done) => { 113 | try { 114 | async function get() { 115 | const notes = await getFileNodes(rootPath + '/unuse/components') 116 | setSize(notes) 117 | const arrs = getNote(notes) 118 | const final = [ 119 | '├── test\n', 120 | '│ └── deep\n', 121 | '│ │ └── user.vue //2工程\n', 122 | '├── test2\n', 123 | '│ └── HelloWorld.vue //2工程\n', 124 | '└── user-rulerts.vue \n' 125 | ] 126 | // console.log(JSON.stringify(arrs), 'arrs') 127 | expect(arrs).toMatchObject(final) 128 | done() 129 | } 130 | get() 131 | } catch (error) { 132 | logger.error(error) 133 | done(error) 134 | } 135 | }) 136 | }) 137 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | //=========================================== 4 | //============= Editor ====================== 5 | //=========================================== 6 | "explorer.openEditors.visible": 0, 7 | "editor.tabSize": 2, 8 | "editor.defaultFormatter": "esbenp.prettier-vscode", 9 | "diffEditor.ignoreTrimWhitespace": false, 10 | "editor.trimAutoWhitespace": true, 11 | //=========================================== 12 | //============= Other ======================= 13 | //=========================================== 14 | "breadcrumbs.enabled": true, 15 | //=========================================== 16 | //============= emmet ======================= 17 | //=========================================== 18 | "emmet.triggerExpansionOnTab": true, 19 | "emmet.showAbbreviationSuggestions": true, 20 | //=========================================== 21 | //============= files ======================= 22 | //=========================================== 23 | "files.trimTrailingWhitespace": true, 24 | "files.insertFinalNewline": true, 25 | "files.trimFinalNewlines": true, 26 | "files.eol": "\n", 27 | "search.exclude": { 28 | "**/docs": true, 29 | "**/node_modules": true, 30 | "**/*.log": true, 31 | "**/*.log*": true, 32 | "**/bower_components": true, 33 | "**/dist": true, 34 | "**/elehukouben": true, 35 | "**/.git": true, 36 | "**/.gitignore": true, 37 | "**/.svn": true, 38 | "**/.DS_Store": true, 39 | "**/.idea": true, 40 | "**/.vscode": false, 41 | "**/yarn.lock": true, 42 | "**/tmp": true, 43 | "out": true, 44 | "dist": true, 45 | "node_modules": true, 46 | "CHANGELOG.md": true, 47 | "examples": true, 48 | "res": true, 49 | "screenshots": true 50 | }, 51 | "files.exclude": { 52 | "**/bower_components": true, 53 | "**/.idea": true, 54 | "**/tmp": true, 55 | "**/.git": true, 56 | "**/.svn": true, 57 | "**/.hg": true, 58 | "**/CVS": true, 59 | "**/.DS_Store": true 60 | }, 61 | "files.watcherExclude": { 62 | "**/.git/objects/**": true, 63 | "**/.git/subtree-cache/**": true, 64 | "**/.vscode/**": true, 65 | "**/node_modules/**": true, 66 | "**/tmp/**": true, 67 | "**/bower_components/**": true, 68 | "**/dist/**": true, 69 | "**/yarn.lock": true 70 | }, 71 | "stylelint.enable": true, 72 | "stylelint.packageManager": "yarn", 73 | "telemetry.enableCrashReporter": false, 74 | "workbench.settings.enableNaturalLanguageSearch": false, 75 | "path-intellisense.mappings": { 76 | "@/": "${workspaceRoot}/src" 77 | }, 78 | "prettier.requireConfig": true, 79 | "typescript.updateImportsOnFileMove.enabled": "always", 80 | "workbench.sideBar.location": "left", 81 | "[javascriptreact]": { 82 | "editor.defaultFormatter": "esbenp.prettier-vscode" 83 | }, 84 | "[typescript]": { 85 | "editor.defaultFormatter": "esbenp.prettier-vscode" 86 | }, 87 | "[typescriptreact]": { 88 | "editor.defaultFormatter": "esbenp.prettier-vscode" 89 | }, 90 | "[html]": { 91 | "editor.defaultFormatter": "esbenp.prettier-vscode" 92 | }, 93 | "[css]": { 94 | "editor.defaultFormatter": "esbenp.prettier-vscode" 95 | }, 96 | "[less]": { 97 | "editor.defaultFormatter": "esbenp.prettier-vscode" 98 | }, 99 | "[scss]": { 100 | "editor.defaultFormatter": "esbenp.prettier-vscode", 101 | "editor.codeActionsOnSave": { 102 | "source.fixAll.eslint": "never", 103 | "source.fixAll.stylelint": "explicit" 104 | } 105 | }, 106 | "[json]": { 107 | "editor.defaultFormatter": "esbenp.prettier-vscode" 108 | }, 109 | "[markdown]": { 110 | "editor.defaultFormatter": "esbenp.prettier-vscode" 111 | }, 112 | "editor.codeActionsOnSave": { 113 | "source.fixAll.eslint": "explicit" 114 | }, 115 | "[vue]": { 116 | "editor.codeActionsOnSave": { 117 | "source.fixAll.eslint": "never", 118 | "source.fixAll.stylelint": "explicit" 119 | } 120 | }, 121 | "svn.ignoreMissingSvnWarning": true, 122 | "devchat.defaultModel": "gpt-4" 123 | } 124 | -------------------------------------------------------------------------------- /test/rename.test.ts: -------------------------------------------------------------------------------- 1 | import { foldNode, fileNode, nodesTwo, nodesThree } from './utils/nodes-test' 2 | import fs from 'fs-extra' 3 | import { 4 | renameFilePath, 5 | changePathFold, 6 | changePathName, 7 | renameFoldPath, 8 | replaceName, 9 | checkCamelFile 10 | } from '../src/commands/rename-path' 11 | import { creatFile } from './utils/utils' 12 | import { createConsola } from 'consola' 13 | const rootPath = process.cwd().replace(/\\/g, '/') 14 | const logger = createConsola({ 15 | level: 4 16 | }) 17 | describe('rename.test的测试', () => { 18 | test('checkCamelFile --检测kebab-case', () => { 19 | const flag = checkCamelFile('MyTemplate.vue') 20 | logger.info('flag:', flag) 21 | expect(flag).toEqual(true) 22 | }) 23 | 24 | test('changePathFold --递归修改文件夹node的path', () => { 25 | changePathFold(foldNode, { newName: 'check-test-kable-case', filename: 'checkTestKableCase' }) 26 | const obj = { 27 | name: 'check-test-kable-case', 28 | isDir: true, 29 | level: 1, 30 | note: '', 31 | copyed: false, 32 | imports: [], 33 | belongTo: [], 34 | fullPath: rootPath + '/temp/check-test-kable-case', 35 | children: [ 36 | { 37 | name: 'check-test-kable-caseInner', 38 | isDir: true, 39 | level: 1, 40 | note: '', 41 | copyed: false, 42 | imports: [], 43 | belongTo: [], 44 | fullPath: rootPath + '/temp/check-test-kable-case/checkTestKableCaseInner' 45 | } 46 | ] 47 | } 48 | const str = JSON.stringify(obj) 49 | expect(JSON.stringify(foldNode)).toEqual(str) 50 | }) 51 | 52 | test('changePathName --递归修改文件里面的import', () => { 53 | changePathName(fileNode, { newName: 'you-template', filename: 'youTemplate' }) 54 | // logger.info('tempNode', JSON.stringify(fileNode)) 55 | const finalObj = { 56 | name: 'you-template', 57 | isDir: false, 58 | level: 2, 59 | note: ' // 我就是个注释', 60 | imports: [rootPath.toLowerCase() + '/temp/my-template.vue'], 61 | belongTo: [], 62 | size: 96, 63 | copyed: false, 64 | rowSize: 4, 65 | suffix: '.vue', 66 | fullPath: rootPath + '/temp/TestKableCase/you-template.vue' 67 | } 68 | expect(fileNode).toMatchObject(finalObj) 69 | }) 70 | 71 | test('replaceName --改文件名', (done) => { 72 | const foldPath2 = rootPath + '/temp/checkTestKableCase2' 73 | const file = rootPath + '/temp/checkTestKableCase2/testTemplate.vue' 74 | async function get() { 75 | try { 76 | fs.ensureDirSync(foldPath2) 77 | await creatFile(file) 78 | await replaceName(foldPath2) 79 | const flag = fs.existsSync(rootPath + '/temp/check-test-kable-case2') 80 | expect(flag).toEqual(true) 81 | done() 82 | } catch (error) { 83 | done(error) 84 | } 85 | } 86 | get() 87 | }) 88 | 89 | test('renameFoldPath --改所有文件夹名', (done) => { 90 | // 自备独立测试数据 91 | const foldPath = rootPath + '/temp/TestKableCase' 92 | const file = rootPath + '/temp/TestKableCase/youTemplate.vue' 93 | const file2 = rootPath + '/temp/test-kable-case/youTemplate.vue' 94 | const foldPath2 = rootPath + '/temp/TestKableCase/TestKableCase2' 95 | const finalPath = rootPath + '/temp/test-kable-case' 96 | async function get() { 97 | try { 98 | fs.ensureDirSync(foldPath) 99 | fs.ensureDirSync(foldPath2) 100 | await creatFile(file) 101 | await renameFoldPath(nodesTwo) 102 | const flag = fs.existsSync(finalPath) 103 | const flag2 = fs.existsSync(file2) 104 | expect(flag && flag2).toEqual(true) 105 | done() 106 | } catch (error) { 107 | done(error) 108 | } 109 | } 110 | get() 111 | }) 112 | 113 | test('renameFoldPath --改所有文件名', (done) => { 114 | // 自备独立测试数据 115 | const foldPath = rootPath + '/temp/myVue/myTable' 116 | const file = rootPath + '/temp/myVue/myTable/testTemplate.vue' 117 | const finalPath = rootPath + '/temp/myVue/myTable/test-template.vue' 118 | async function get() { 119 | try { 120 | fs.ensureDirSync(foldPath) 121 | await creatFile(file) 122 | await renameFilePath(nodesThree) 123 | const flag = fs.existsSync(finalPath) 124 | expect(flag).toEqual(true) 125 | done() 126 | } catch (error) { 127 | done(error) 128 | } 129 | } 130 | get() 131 | }) 132 | }) 133 | -------------------------------------------------------------------------------- /test/mark-file.test.ts: -------------------------------------------------------------------------------- 1 | import { findNodes, deletMark, setNodeMark, witeMarkFile, setmark, deletMarkAll } from '../src/commands/mark-file' 2 | import { nodeOne, nodesMark, routersMarg } from './utils/nodes-test' 3 | import fs from 'fs-extra' 4 | import { 5 | readFile 6 | // writeFile 7 | } from 'fs/promises' 8 | import { createConsola } from 'consola' 9 | import { creatFile, creatFileNoimport } from './utils/utils' 10 | const rootPath = process.cwd().replace(/\\/g, '/') 11 | const logger = createConsola({ 12 | level: 4 13 | }) 14 | 15 | describe('mark-file.test的测试', () => { 16 | test('findNodes--查node', () => { 17 | const node = findNodes(nodeOne, rootPath + '/temp/app-file-test.vue') 18 | expect(node).toMatchObject(nodeOne[0]) 19 | }) 20 | 21 | test('setmark--给节点标记', (done) => { 22 | const file = rootPath + '/temp/mark-setmark.vue' 23 | try { 24 | async function get() { 25 | await creatFile(file) 26 | await setmark(file, 'setmark') 27 | const str = await readFile(file, 'utf-8') 28 | logger.info(str, '444') 29 | const flag = str.indexOf('setmark') > -1 30 | expect(flag).toEqual(true) 31 | done() 32 | } 33 | get() 34 | } catch (error) { 35 | done(error) 36 | console.error(error) 37 | } 38 | }) 39 | 40 | test('deletMarkAll--递归所有文件,删除所有标记', (done) => { 41 | const file = rootPath + '/temp/delet-mark-all.vue' 42 | try { 43 | async function get() { 44 | await creatFile(file) 45 | await setmark(file, 'setmark') 46 | const nodes = [ 47 | { 48 | name: 'mark-setmark', 49 | isDir: false, 50 | level: 2, 51 | note: ' // 我就是个注释', 52 | imports: [], 53 | belongTo: ['setmark'], 54 | size: 96, 55 | copyed: false, 56 | rowSize: 4, 57 | suffix: '.vue', 58 | fullPath: rootPath + '/temp/delet-mark-all.vue' 59 | } 60 | ] 61 | await deletMarkAll(nodes, 'setmark') 62 | const str = await readFile(file, 'utf-8') 63 | logger.info(str, '444') 64 | const flag = str.indexOf('setmark') > -1 65 | expect(flag).toEqual(false) 66 | done() 67 | } 68 | get() 69 | } catch (error) { 70 | done(error) 71 | console.error(error) 72 | } 73 | }) 74 | 75 | test('deletMark--测试删除标记', (done) => { 76 | const str = `//mark 77 | //mark 78 | ` 81 | const file = rootPath + '/temp/bb.vue' 82 | const finalStr = `` 85 | try { 86 | fs.writeFile(file, str, { encoding: 'utf8' }, async () => { 87 | const receive = await deletMark(file, 'mark') 88 | done() 89 | expect(receive).toEqual(finalStr) 90 | }) 91 | } catch (error) { 92 | done(error) 93 | } 94 | }) 95 | 96 | test('setNodeMark--给节点标记', (done) => { 97 | async function get() { 98 | const file = rootPath + '/temp/app2-file-test.vue' 99 | await creatFile(file) 100 | try { 101 | await deletMark(file, 'mark') 102 | await setNodeMark(nodeOne, 'mark', file) 103 | const str = await readFile(file, 'utf-8') 104 | const index = str.indexOf('//mark') 105 | expect(index).toEqual(0) 106 | done() 107 | } catch (error) { 108 | done(error) 109 | } 110 | } 111 | get() 112 | }) 113 | 114 | test('witeMarkFile--标记文件主程序写入分类', (done) => { 115 | async function get() { 116 | const foldPath = rootPath + '/test2' 117 | fs.removeSync(foldPath) // 先清空目录 118 | const file = rootPath + '/temp/wite-file-test.vue' 119 | await creatFileNoimport(file) 120 | const fold = rootPath + '/temp/my' 121 | fs.ensureDirSync(fold) 122 | const file2 = rootPath + '/temp/my/wite-file2.vue' 123 | await creatFile(file2) 124 | const file3 = rootPath + '/temp/my/aa.vue' 125 | await creatFileNoimport(file3) 126 | try { 127 | await witeMarkFile(nodesMark, routersMarg) 128 | const finalPath = rootPath + '/test2/temp/wite-file-test.vue' 129 | const flag = fs.existsSync(finalPath) 130 | expect(flag).toEqual(true) 131 | done() 132 | } catch (error) { 133 | done(error) 134 | } 135 | } 136 | get() 137 | }) 138 | }) 139 | -------------------------------------------------------------------------------- /src/commands/wirte-md.ts: -------------------------------------------------------------------------------- 1 | /* 生成md说明文档 */ 2 | 3 | import path from 'path' 4 | import { getFileNodes, getNote } from './get-file' 5 | import type { ItemType } from '../types' 6 | import { createConsola } from 'consola' 7 | import { readFile, writeFile as nodeWriteFile } from 'fs/promises' 8 | const logger = createConsola({ 9 | level: process.env.AGMD_SILENT === '1' ? 0 : 4 10 | }) 11 | const rootPath = process.cwd().replace(/\\/g, '/') 12 | 13 | type secoutType = { rowTotleNumber: number; sizeTotleNumber: number; coutObj: { [key: string]: number } } 14 | /** 15 | * @description :Write the result to JS file 16 | * @param {data} data 17 | */ 18 | export async function wirteMd(data: string, filePath: string): Promise { 19 | const file = path.resolve(rootPath, filePath) 20 | // 异步写入数据到文件 21 | if (process.env.AGMD_DRY_RUN === '1') { 22 | logger.info(`Dry-run: would write file ${file}`) 23 | } else { 24 | await nodeWriteFile(file, data, { encoding: 'utf8' }) 25 | logger.success('Write successful') 26 | } 27 | } 28 | 29 | /** 30 | * @description: Get statistics 31 | * @param {Array} datas 32 | * @return {Object} 33 | */ 34 | export function getCountMd(datas: ItemType[]): secoutType { 35 | let rowTotleNumber = 0 36 | let sizeTotleNumber = 0 37 | const coutObj: { [key: string]: number } = {} 38 | function getDeatle(nodes: ItemType[]) { 39 | nodes.forEach((obj: ItemType) => { 40 | if (obj.children) getDeatle(obj.children) 41 | else if (obj.suffix && obj.rowSize && obj.size) { 42 | if (!coutObj.hasOwnProperty(obj.suffix)) coutObj[obj.suffix] = 0 43 | coutObj[obj.suffix]++ 44 | rowTotleNumber += obj.rowSize 45 | sizeTotleNumber += obj.size 46 | } 47 | }) 48 | } 49 | getDeatle(datas) 50 | return { 51 | rowTotleNumber, 52 | sizeTotleNumber, 53 | coutObj 54 | } 55 | } 56 | 57 | /** 58 | * @description:Thousands format 千分位格式化 59 | * @param {num} num format a number 要格式化数字 60 | * @return {string} 61 | */ 62 | function format(num: number): string { 63 | var reg = /\d{1,3}(?=(\d{3})+$)/g 64 | return (num + '').replace(reg, '$&,') 65 | } 66 | 67 | /** 68 | * @description: Generate statistics MD 生成统计md 69 | * @param {object} obj 70 | * @return {string} 71 | */ 72 | export function setCountMd(obj: secoutType): string { 73 | const { rowTotleNumber, sizeTotleNumber, coutObj } = obj 74 | let countMd = '😍 代码总数统计:\n' 75 | let totle = 0 76 | for (const key in coutObj) { 77 | const ele = coutObj[key] 78 | totle += ele 79 | countMd += `后缀是 ${key} 的文件有 ${ele} 个\n` 80 | } 81 | countMd += `总共有 ${totle} 个文件\n` 82 | let md = `总代码行数有: ${format(rowTotleNumber)}行, 83 | 总代码字数有: ${format(sizeTotleNumber)}个\n` 84 | md = countMd + md 85 | return md 86 | } 87 | /** 88 | * @description: Generate MD 生成md 89 | * @param {object} option 90 | */ 91 | export async function getMd(option?: { ignore?: string[]; include?: string[] }) { 92 | logger.success('👉 命令运行位置: ' + process.cwd() + '\n') 93 | const nodes = await getFileNodes(rootPath, option) 94 | const countMdObj = getCountMd(nodes) 95 | const coutMd = setCountMd(countMdObj) 96 | logger.success(coutMd) 97 | const note = getNote(nodes) 98 | const md = note.join('') + '\n' 99 | const composed = `# 目录结构\n${md}\n## 统计\n${coutMd}` 100 | return { md: composed, nodes } 101 | } 102 | 103 | /** 104 | * @description: 获取代码及结构作为提示 105 | * @param {string} data 106 | * @param {ItemType} nodes 107 | */ 108 | export async function witeCodeAndPrompt(inRootPath: string, data: string, nodes: ItemType[]): Promise { 109 | const menuSt = '下面是整个工程的目录文件结构\n' + data 110 | let content = '下面是整个代码内容,其中path:是文件路径,其他是文件内容\n' 111 | async function find(objs: ItemType[]) { 112 | for (let index = 0; index < objs.length; index++) { 113 | const element = objs[index] 114 | if (element.children) find(element.children) 115 | else { 116 | // 文件,读取内容 117 | const fileStr = await readFile(element.fullPath, 'utf-8') 118 | const file = 'path:' + element.fullPath.replace(inRootPath, '') + '\n' + fileStr + '\n' 119 | content = content + file 120 | } 121 | } 122 | } 123 | try { 124 | await find(nodes) 125 | } catch (error) { 126 | console.error(error) 127 | } 128 | const out = `${inRootPath}/codeAndPrompt.md` 129 | if (process.env.AGMD_DRY_RUN === '1') { 130 | logger.info(`Dry-run: would write file ${out}`) 131 | } else { 132 | await nodeWriteFile(out, menuSt + content, { encoding: 'utf8' }) 133 | logger.success('🀄️ 生成codeAndPrompt.md完毕 !') 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/commands/command-handler.ts: -------------------------------------------------------------------------------- 1 | // 命令处理逻辑 2 | import prompts from 'prompts' 3 | import { 4 | getMdAction, 5 | changePathAction, 6 | changeAbsolutePathActionRun, 7 | changesuffixAction, 8 | markFileAction, 9 | witeFileAction, 10 | deletMarkAction, 11 | renameKebFoldAction, 12 | renameFileAction, 13 | renameCamFoldAction, 14 | renameUpperCamelCaseAction 15 | } from './command-actions' 16 | import { VERSION, PKG_NAME } from '../shared/constant' 17 | import help from '../../script/help/index' 18 | import stringToArgs from '../../script/cli' 19 | import { wirteJsNodes } from './change-path' 20 | import { getMd, witeCodeAndPrompt } from './wirte-md' 21 | import handle from '../../script/cli/handle' 22 | 23 | import { createConsola } from 'consola' 24 | const logger = createConsola({ 25 | level: process.env.AGMD_SILENT === '1' ? 0 : 4 26 | }) 27 | // 为什么要加process.cwd()的replace 是为了抹平window和linux生成的路径不一样的问题 28 | const rootPath = process.cwd().replace(/\\/g, '/') 29 | const options = stringToArgs(process.argv) 30 | const { ignores: ignore, includes: include } = handle(options) 31 | 32 | export async function selectCommand() { 33 | const actionMap = new Map() 34 | 35 | const { md, nodes } = await getMd({ ignore, include }) 36 | 37 | actionMap.set('Generate MD', { 38 | title: '📅 生成结构树文档', 39 | value: 'Generate MD', 40 | selected: true, 41 | action: () => getMdAction(md) 42 | }) 43 | actionMap.set('Change Relative Path', { 44 | title: '🔑 修改为相对路径', 45 | value: 'Change Relative Path', 46 | action: () => changePathAction(nodes) 47 | }) 48 | actionMap.set('Change Absolute Path', { 49 | title: '💎 修改为绝对路径(暂未实现)', 50 | value: 'Change Absolute Path', 51 | action: () => changeAbsolutePathActionRun(nodes) 52 | }) 53 | actionMap.set('Completion suffix', { 54 | title: '💯 补全文件后缀', 55 | value: 'Completion suffix', 56 | action: () => changesuffixAction(nodes, true) 57 | }) 58 | 59 | actionMap.set('RenameFoldKebabCase', { 60 | title: '🎁 统一命名文件夹为 Kebab-Case', 61 | value: 'RenameFoldKebabCase', 62 | action: () => renameKebFoldAction(nodes) 63 | }) 64 | actionMap.set('RenameFileKebabCase', { 65 | title: '🍰 统一命名文件为 Kebab-Case', 66 | value: 'RenameFileKebabCase', 67 | action: () => renameFileAction(nodes) 68 | }) 69 | 70 | actionMap.set('RenameFoldCameCase', { 71 | title: '🎁 统一命名文件夹为 CamelCase', 72 | value: 'RenameFoldCameCase', 73 | action: () => renameCamFoldAction(nodes) 74 | }) 75 | 76 | actionMap.set('RenameFoldUpperCamelCase', { 77 | title: '🦄 统一命名文件为 UpperCamelCase', 78 | value: 'RenameFoldUpperCamelCase', 79 | action: () => renameUpperCamelCaseAction(nodes) 80 | }) 81 | 82 | actionMap.set('Write JSON Nodes', { 83 | title: '🔱 记录节点 JSON', 84 | value: 'Write JSON Nodes', 85 | action: () => wirteJsNodes(JSON.stringify(nodes), rootPath + '/readme-file.js') 86 | }) 87 | 88 | actionMap.set('Mark File', { 89 | title: '🎊 给需要分类的都打上标记', 90 | value: 'Mark File', 91 | action: () => markFileAction(nodes) 92 | }) 93 | actionMap.set('Delete Mark', { 94 | title: '💥 删除标记', 95 | value: 'Delete Mark', 96 | action: () => deletMarkAction(nodes) 97 | }) 98 | actionMap.set('Classification', { 99 | title: '💫 分类', 100 | value: 'Classification', 101 | action: () => witeFileAction(nodes) 102 | }) 103 | 104 | actionMap.set('codeAndPrompt', { 105 | title: '🌈 输出结构及代码', 106 | value: 'codeAndPrompt', 107 | action: () => witeCodeAndPrompt(rootPath, md, nodes) 108 | }) 109 | 110 | actionMap.set('help', { 111 | title: '🙏 帮助', 112 | value: 'help', 113 | selected: true, 114 | action: () => help() 115 | }) 116 | return actionMap 117 | } 118 | 119 | export type BaseCmd = { 120 | init?: boolean 121 | config?: string 122 | } 123 | export async function handleCommand(cmd: BaseCmd) { 124 | if (cmd.init) { 125 | logger.info(`${PKG_NAME}:version is :${VERSION}`) 126 | } 127 | const actions = await selectCommand() 128 | let result: any = {} 129 | try { 130 | result = await prompts( 131 | [ 132 | { 133 | name: 'command', 134 | type: 'select', 135 | message: '请使用上下键选择一个操作命令:', 136 | choices: Array.from(actions.values()) 137 | } 138 | ], 139 | { 140 | onCancel: () => { 141 | throw new Error('操作取消!') 142 | } 143 | } 144 | ) 145 | } catch (e: any) { 146 | logger.error(e.message) 147 | process.exit(1) 148 | } 149 | actions.get(result.command)!.action() 150 | } 151 | -------------------------------------------------------------------------------- /src/commands/command-actions.ts: -------------------------------------------------------------------------------- 1 | /* 界面命令注册在这里 */ 2 | import type { ItemType } from '../types' 3 | import { wirteMd } from './wirte-md' 4 | import { writeFile } from 'fs/promises' 5 | import { renameFoldPath, renameFilePath, renameCamelCaseFilePath } from './rename-path' 6 | import { createConsola } from 'consola' 7 | import { changePath, wirteJsNodes } from './change-path' 8 | import { markFile, deletMarkAll, witeMarkFile } from './mark-file' 9 | import { getRouterArrs } from './get-router' 10 | import path from 'path' 11 | 12 | const logger = createConsola({ 13 | level: process.env.AGMD_SILENT === '1' ? 0 : 4 14 | }) 15 | // 为什么要加process.cwd()的replace 是为了抹平window和linux生成的路径不一样的问题 16 | const rootPath = process.cwd().replace(/\\/g, '/') 17 | 18 | /** 19 | * @desc: //2. 得到md文档,------------>会写(只生成一个md) 20 | * @param {string} md 21 | */ 22 | export function getMdAction(md: string) { 23 | console.log('\x1B[36m%s\x1B[0m', '*** location: ', `${rootPath}/readme-md.md`) 24 | wirteMd(md, `${rootPath}/readme-md.md`) 25 | } 26 | 27 | /** 28 | * @desc: 这里做一个前置判断, 如果父路径不是src, 报错, 因为有changepath@符号是指向src的 29 | */ 30 | function checkFold() { 31 | const foldPath = path.resolve('./').replace(/\\/g, '/') 32 | const foldArrs = foldPath.split('/') 33 | const foldName = foldArrs.pop() 34 | if (foldName === 'pages') { 35 | return 36 | } 37 | if (foldName !== 'src') { 38 | logger.error('changePath需要在src目录下运行命令! ') 39 | process.exit(1) 40 | } 41 | } 42 | 43 | /** 44 | * @desc: //3. 更改所有为绝对路径+ 后缀补全------------>会写(会操作代码) 45 | * @param {Array} nodes 46 | */ 47 | export async function changePathAction(nodes: ItemType[]) { 48 | checkFold() 49 | await changePath(nodes) 50 | } 51 | 52 | /** 53 | * @desc: 修改绝对路径 54 | */ 55 | export async function changeAbsolutePathActionRun(nodes: ItemType[]) { 56 | await changePath(nodes) 57 | // 第二次写入,将相对路径改为使用 @ 别名的绝对路径 58 | await changePath(nodes, false, true) 59 | } 60 | 61 | export async function changesuffixAction(nodes: ItemType[], nochangePath: Boolean) { 62 | checkFold() 63 | await changePath(nodes, nochangePath) 64 | } 65 | 66 | /** 67 | * @desc: //4. 打标记 ------------> 会写(会操作代码) //5. 分文件 ------------> 会写(会另外生成包文件) 68 | 69 | * @param {Array} nodes 70 | */ 71 | export async function markFileAction(nodes: ItemType[]) { 72 | checkFold() 73 | const routers = await getRouterArrs() 74 | await writeFile(rootPath + '/router-file.js', 'const router=' + JSON.stringify(routers), { encoding: 'utf8' }) 75 | if (routers) { 76 | await markFile(nodes, routers) 77 | await wirteJsNodes(JSON.stringify(nodes), rootPath + '/readme-file.js') 78 | } 79 | } 80 | 81 | /** 82 | * @desc: 5,将打标记的进行copy 83 | * @param {Array} nodes 84 | */ 85 | export async function witeFileAction(nodes: ItemType[]) { 86 | const routers = await getRouterArrs() 87 | if (routers) { 88 | await markFile(nodes, routers) 89 | // copy文件一定是建立在打标记的基础上 90 | await witeMarkFile(nodes, routers) 91 | } 92 | } 93 | // /** 94 | // * @desc://6. 得到md对象(只生成一个md) 95 | // * @param {Array} nodes 96 | // */ 97 | // async function wirteJsNodesAction(nodes: ItemType[]) { 98 | // // 要先改路径后缀,否则依赖收集不到 99 | // await changePathAction(nodes) 100 | // wirteJsNodes(JSON.stringify(nodes), rootPath + '/readme-file.js') 101 | // } 102 | 103 | /** 104 | * @desc://7. 删除标记 105 | 106 | * @param {Array} nodes 107 | */ 108 | export async function deletMarkAction(nodes: ItemType[]) { 109 | await deletMarkAll(nodes, 'mark') 110 | } 111 | 112 | /** 113 | * @desc://8. 规范命名文件夹kabel-case 114 | * @param {Array} nodes 115 | */ 116 | export async function renameKebFoldAction(nodes: ItemType[]) { 117 | renameFoldPath(nodes) 118 | } 119 | 120 | /** 121 | * @desc://9. 规范命名文件kabel-case 122 | * @param {Array} nodes 123 | */ 124 | export async function renameFileAction(nodes: ItemType[]) { 125 | renameFilePath(nodes) 126 | } 127 | 128 | /** 129 | * @desc://10. 规范命名文件夹Upercamecase 130 | * @param {Array} nodes 131 | */ 132 | export async function renameCamFoldAction(nodes: ItemType[]) { 133 | renameFoldPath(nodes, true) 134 | } 135 | 136 | export async function renameUpperCamelCaseAction(nodes: ItemType[]) { 137 | renameCamelCaseFilePath(nodes) 138 | } 139 | 140 | /** 141 | * @desc: 执行所有操作 142 | * @param {Array} nodes 143 | * @param {string} md 144 | */ 145 | export async function generateAllAction(nodes: ItemType[], md: string) { 146 | checkFold() 147 | const routers = await getRouterArrs() 148 | if (routers) { 149 | getMdAction(md) 150 | await changePathAction(nodes) 151 | await markFileAction(nodes) 152 | // copy文件一定是建立在打标记的基础上 153 | await witeMarkFile(nodes, routers) 154 | await wirteJsNodes(JSON.stringify(nodes), rootPath + '/readme-file.js') 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /test/utils/nodes-test.ts: -------------------------------------------------------------------------------- 1 | const rootPath = process.cwd().replace(/\\/g, '/') 2 | export const nodeOne = [ 3 | { 4 | name: 'app-file-test.vue', 5 | isDir: false, 6 | level: 2, 7 | note: ' // 我就是个注释', 8 | imports: [rootPath + '/temp/aa.vue'], 9 | belongTo: ['mark'], 10 | size: 96, 11 | copyed: false, 12 | rowSize: 4, 13 | suffix: '.vue', 14 | fullPath: rootPath + '/temp/app-file-test.vue' 15 | }, 16 | { 17 | name: 'app2-file-test.vue', 18 | isDir: false, 19 | level: 2, 20 | note: ' // 我就是个注释', 21 | imports: [rootPath + '/temp/aa.vue'], 22 | belongTo: ['mark'], 23 | size: 96, 24 | copyed: false, 25 | rowSize: 4, 26 | suffix: '.vue', 27 | fullPath: rootPath + '/temp/app2-file-test.vue' 28 | }, 29 | { 30 | name: 'aa.vue', 31 | isDir: false, 32 | copyed: false, 33 | level: 2, 34 | note: ' // 我就是个注释', 35 | imports: [], 36 | belongTo: ['mark'], 37 | size: 96, 38 | rowSize: 4, 39 | suffix: '.vue', 40 | fullPath: rootPath + '/temp/aa.vue' 41 | } 42 | ] 43 | 44 | export const foldNode = { 45 | name: 'checkTestKableCase', 46 | isDir: true, 47 | level: 1, 48 | note: '', 49 | copyed: false, 50 | imports: [], 51 | belongTo: [], 52 | fullPath: rootPath + '/temp/checkTestKableCase', 53 | children: [ 54 | { 55 | name: 'checkTestKableCaseInner', 56 | isDir: true, 57 | level: 1, 58 | note: '', 59 | copyed: false, 60 | imports: [], 61 | belongTo: [], 62 | fullPath: rootPath + '/temp/checkTestKableCase/checkTestKableCaseInner' 63 | } 64 | ] 65 | } 66 | 67 | export const fileNode = { 68 | name: 'youTemplate', 69 | isDir: false, 70 | level: 2, 71 | note: ' // 我就是个注释', 72 | imports: [rootPath + '/temp/myTemplate.vue'], 73 | belongTo: [], 74 | size: 96, 75 | copyed: false, 76 | rowSize: 4, 77 | suffix: '.vue', 78 | fullPath: rootPath + '/temp/TestKableCase/youTemplate.vue' 79 | } 80 | 81 | export const nodesTwo = [ 82 | { 83 | name: 'TestKableCase', 84 | isDir: true, 85 | level: 1, 86 | note: '', 87 | copyed: false, 88 | imports: [], 89 | belongTo: [], 90 | fullPath: rootPath + '/temp/TestKableCase', 91 | children: [ 92 | fileNode, 93 | { 94 | name: 'TestKableCase2', 95 | isDir: true, 96 | level: 1, 97 | note: '', 98 | copyed: false, 99 | imports: [], 100 | belongTo: [], 101 | fullPath: rootPath + '/temp/TestKableCase/TestKableCase2' 102 | } 103 | ] 104 | } 105 | ] 106 | 107 | export const nodesMark = [ 108 | { 109 | name: 'wite-file-test', 110 | isDir: false, 111 | level: 2, 112 | note: '', 113 | imports: [], 114 | belongTo: ['test2'], 115 | size: 96, 116 | copyed: false, 117 | rowSize: 4, 118 | suffix: '.vue', 119 | fullPath: rootPath + '/temp/wite-file-test.vue' 120 | }, 121 | { 122 | name: 'my', 123 | isDir: true, 124 | level: 1, 125 | note: '', 126 | copyed: false, 127 | imports: [], 128 | belongTo: [], 129 | fullPath: rootPath + '/temp/my', 130 | children: [ 131 | { 132 | name: 'aa', 133 | isDir: false, 134 | level: 2, 135 | note: ' // 我就是个注释', 136 | imports: [], 137 | belongTo: ['test2'], 138 | size: 96, 139 | copyed: false, 140 | rowSize: 4, 141 | suffix: '.vue', 142 | fullPath: rootPath + '/temp/my/aa.vue' 143 | }, 144 | { 145 | name: 'wite-file2', 146 | isDir: false, 147 | level: 2, 148 | note: ' // 我就是个注释', 149 | imports: [rootPath + '/temp/my/aa.vue'], 150 | belongTo: ['test2'], 151 | size: 96, 152 | copyed: false, 153 | rowSize: 4, 154 | suffix: '.vue', 155 | fullPath: rootPath + '/temp/my/wite-file2.vue' 156 | } 157 | ] 158 | } 159 | ] 160 | 161 | export const routersMarg = [ 162 | { 163 | name: 'test2', 164 | router: [ 165 | { 166 | path: '/wite-file-test', 167 | component: '@/temp/wite-file-test.vue' 168 | }, 169 | { 170 | path: '/wite-file2', 171 | component: '@/temp/my/wite-file2.vue' 172 | } 173 | ] 174 | } 175 | ] 176 | 177 | export const nodesThree = [ 178 | { 179 | name: 'myVue', 180 | isDir: true, 181 | level: 1, 182 | note: '', 183 | copyed: false, 184 | imports: [], 185 | belongTo: [], 186 | fullPath: rootPath + '/temp/myVue', 187 | children: [ 188 | { 189 | name: 'myTable', 190 | isDir: true, 191 | level: 1, 192 | note: '', 193 | copyed: false, 194 | imports: [], 195 | belongTo: [], 196 | fullPath: rootPath + '/temp/myVue/myTable', 197 | children: [ 198 | { 199 | name: 'testTemplate', 200 | isDir: false, 201 | level: 2, 202 | note: ' // 我就是个注释', 203 | imports: ['/temp/myTemplate.vue'], 204 | belongTo: [], 205 | size: 96, 206 | copyed: false, 207 | rowSize: 4, 208 | suffix: '.vue', 209 | fullPath: rootPath + '/temp/myVue/myTable/testTemplate.vue' 210 | } 211 | ] 212 | } 213 | ] 214 | } 215 | ] 216 | -------------------------------------------------------------------------------- /src/commands/mark-file.ts: -------------------------------------------------------------------------------- 1 | /* 给路由文件打标记, 把标记打到最后,因为头部已经给了注释 */ 2 | import fs from 'fs' 3 | import { readFile, writeFile } from 'fs/promises' 4 | import type { ItemType, RouterItem } from '../types' 5 | import { markWriteFile } from './mark-write-file' 6 | import { createConsola } from 'consola' 7 | const logger = createConsola({ 8 | level: process.env.AGMD_SILENT === '1' ? 0 : 4 9 | }) 10 | const rootPath = process.cwd().replace(/\\/g, '/') 11 | type Routers = Array 12 | /** 13 | * @desc: 标记文件主程序 14 | * @param {ItemType} nodes 15 | * @param {string} routers 16 | */ 17 | export async function markFile(nodes: ItemType[], routers: Routers) { 18 | for (let i = 0; i < routers.length; i++) { 19 | const ele = routers[i] 20 | for (let j = 0; j < ele.router.length; j++) { 21 | const obj = ele.router[j] 22 | const pathN = obj.component 23 | logger.info(`准备处理${obj.path}`) 24 | // 路径转绝对路径 25 | const absolutePath = pathN.replace('@', rootPath) 26 | // 递归打上子集所有 27 | await setNodeMark(nodes, ele.name, absolutePath) 28 | } 29 | } 30 | } 31 | 32 | /** 33 | * @desc: 标记文件主程序 34 | * @param {ItemType} nodes 35 | * @param {string} rootPath 36 | */ 37 | export async function witeMarkFile(nodes: ItemType[], routers: Routers) { 38 | for (let index = 0; index < routers.length; index++) { 39 | const ele = routers[index] 40 | // 这里循环打标记的路由 41 | for (let j = 0; j < ele.router.length; j++) { 42 | const obj = ele.router[j] 43 | const pathN = obj.component 44 | // 路径转绝对路径 45 | const absolutePath = pathN.replace('@', rootPath) 46 | // 对打上标记的文件进行分类写入 47 | await markWriteFile(nodes, ele.name, absolutePath) 48 | } 49 | } 50 | } 51 | 52 | /** 53 | * @desc: 分离一个递归调用的mark函数 54 | */ 55 | export async function setNodeMark(nodes: ItemType[], name: string, path: string) { 56 | logger.info('setNodeMark入参: ', name, path) 57 | // 通过文件地址, 找到nodes的依赖地址, 把依赖文件也打标记 58 | const node = findNodes(nodes, path) 59 | if (node) { 60 | // 打标记 61 | await setmark(path, name) 62 | } 63 | // logger.info('查找的node: ', node) 64 | if (node && node.imports) { 65 | // 标记归属设置 66 | if (node.belongTo.indexOf(name) > -1) return // 已经分析过该文件了, 就不再分析,否则会死循环 67 | node.belongTo.push(name) 68 | // 找到有子文件了,循环它 69 | for (let index = 0; index < node.imports.length; index++) { 70 | const element = node.imports[index] 71 | // logger.info('依赖文件: ', element) 72 | // 如果文件存在 73 | if (fs.existsSync(element)) { 74 | // 继续递归,直到子文件没有子文件 75 | await setNodeMark(nodes, name, element) 76 | } else { 77 | logger.error(`文件不存在: ${element}`) 78 | } 79 | } 80 | } 81 | } 82 | 83 | /** 84 | * @desc: 递归通过文件全名找节点 85 | * @param {*} nodes 86 | * @param {*} path 87 | */ 88 | export function findNodes(nodes: ItemType[], path: string): ItemType | null { 89 | let node = null 90 | function find(objs: ItemType[]) { 91 | for (let index = 0; index < objs.length; index++) { 92 | const element = objs[index] 93 | if (element.children) find(element.children) 94 | if (element.fullPath === path) node = element 95 | } 96 | } 97 | find(nodes) 98 | return node 99 | } 100 | 101 | /** 102 | * 给文件添加标记 103 | * @param {string} file - 文件路径 104 | * @param {string} name - 标记名称 105 | */ 106 | export async function setmark(file: string, name: string): Promise { 107 | try { 108 | // 读取文件内容 109 | let fileStr = await readFile(file, 'utf-8') 110 | const mark = `//${name}\n` 111 | 112 | // 检查文件是否已经包含标记 113 | if (!fileStr.startsWith(mark)) { 114 | // 在文件内容前添加标记 115 | fileStr = mark + fileStr 116 | if (process.env.AGMD_DRY_RUN === '1') { 117 | logger.info(`Dry-run: would add mark to ${file}`) 118 | } else { 119 | await writeFile(file, fileStr) 120 | logger.info(`Mark added successfully to: ${file}`) 121 | } 122 | } 123 | } catch (error) { 124 | // 提供详细的错误信息 125 | logger.error(`Error marking file: ${file}, Error: ${error}`) 126 | } 127 | } 128 | 129 | /** 130 | * @desc: 递归所有文件,删除所有标记 131 | 132 | * @param {Array} nodes 133 | */ 134 | export async function deletMarkAll(nodes: ItemType[], name: string): Promise { 135 | async function find(objs: ItemType[]) { 136 | for (let index = 0; index < objs.length; index++) { 137 | const element = objs[index] 138 | if (element.children) find(element.children) 139 | else await deletMark(element.fullPath, name) 140 | } 141 | } 142 | await find(nodes) 143 | } 144 | 145 | /** 146 | * @desc: 给文件标记 147 | 148 | * @param {string} file 149 | * @param {string} name 150 | */ 151 | export async function deletMark(file: string, name: string): Promise { 152 | let fileStr = '' 153 | try { 154 | fileStr = await readFile(file, 'utf-8') 155 | const sarr = fileStr.split(/[\n]/g) 156 | for (let index = 0; index < sarr.length; index++) { 157 | const ele = sarr[index] 158 | if (ele.indexOf('//' + name) > -1) { 159 | sarr.splice(index, 1) 160 | index-- //i需要自减,否则每次删除都会讲原数组索引发生变化 161 | } 162 | } 163 | fileStr = sarr.join('\n') 164 | if (process.env.AGMD_DRY_RUN === '1') { 165 | logger.info(`Dry-run: would delete mark in ${file}`) 166 | } else { 167 | await writeFile(file, fileStr, { encoding: 'utf8' }) 168 | logger.success('delete mark successful-------' + file) 169 | } 170 | return fileStr 171 | } catch (error) { 172 | logger.error('删除标记的文件不存在: ', file) 173 | } 174 | return '' 175 | } 176 | -------------------------------------------------------------------------------- /src/commands/change-path.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { readFile, writeFile } from 'fs/promises' 4 | import { createConsola } from 'consola' 5 | import { getDependencies } from '../utils/router-utils' 6 | import type { ItemType } from '../types' 7 | const logger = createConsola({ 8 | level: process.env.AGMD_SILENT === '1' ? 0 : 4 9 | }) 10 | 11 | const rootPath = process.cwd().replace(/\\/g, '/') 12 | 13 | /** 14 | * 检查当前目录是否为项目根目录。 15 | * 根据是否存在 package.json 文件来判断。 16 | */ 17 | function isRootDirectory(): boolean { 18 | const packageJsonPath = path.join(process.cwd(), 'package.json') 19 | try { 20 | fs.accessSync(packageJsonPath, fs.constants.R_OK) 21 | return true 22 | } catch (error) { 23 | return false 24 | } 25 | } 26 | 27 | /** 28 | * @desc: 递归循环所有文件 29 | 30 | * @param {Array} nodes 整个文件的nodes 31 | */ 32 | export async function changePath(nodes: ItemType[], nochangePath?: Boolean, toAbsoluteAlias?: Boolean) { 33 | async function getNode(objs: ItemType[]) { 34 | for (const ele of objs) { 35 | if (ele.children) { 36 | await getNode(ele.children) 37 | } else { 38 | if (isRootDirectory()) { 39 | await writeToFile(ele, true, nochangePath, toAbsoluteAlias) 40 | } 41 | } 42 | } 43 | } 44 | await getNode(nodes) 45 | } 46 | 47 | /** 48 | * @desc: 这里返回没有@ 符号的路径 49 | * @param {string} absoluteImport 依赖本身名字 50 | * @param {string} fullPath 文件本身绝对地址 51 | */ 52 | export function getRelatPath(absoluteImport: string, fullPath: string) { 53 | let relatPath = path.relative(path.dirname(fullPath), absoluteImport).replace(/\\/g, '/') 54 | if (!relatPath.startsWith('.')) { 55 | relatPath = './' + relatPath 56 | } 57 | return relatPath 58 | } 59 | 60 | /** 61 | * @desc: 补后缀的方法+替换前缀 62 | * @param {string} filePath 正则匹配到的依赖路径 63 | * @param {string} fullPath 本身文件名路径 64 | * @param {string} impName 正确的名字 65 | */ 66 | export function makeSuffix(filePath: string, fullPath: string) { 67 | let absoluteImport = filePath.includes('@') 68 | ? filePath.replace('@', process.cwd()) 69 | : path.resolve(path.dirname(fullPath), filePath) 70 | 71 | const lastName = path.extname(absoluteImport) 72 | 73 | if (!lastName) { 74 | const suffixes = ['.ts', '.vue', '.tsx', '.js', '/index.js', '/index.vue'] 75 | for (const suffix of suffixes) { 76 | if (fs.existsSync(absoluteImport + suffix)) { 77 | absoluteImport += suffix 78 | // logger.info('补充后缀:', absoluteImport + suffix) 79 | break 80 | } 81 | } 82 | } 83 | return absoluteImport.replace(/\\/g, '/') 84 | } 85 | 86 | /** 87 | * @desc: 根据一行代码匹配import的详细内容 TODO 这里还得优化 88 | 89 | */ 90 | export function getImportName(ele: string, dependencies: string[]) { 91 | let str = '' 92 | const flag = dependencies.some((item) => ele.indexOf(item) > -1) 93 | const reg = / from [\"|\'](.*)[\'|\"]/ 94 | // 这里只收集组件依赖, 插件依赖排除掉 95 | if (!flag && ele.indexOf('/') > -1 && ele.indexOf('//') !== 0) { 96 | const impStr = ele.match(reg) 97 | // 没有import的不转 98 | if (impStr && impStr[1]) str = impStr[1] 99 | } 100 | return str 101 | } 102 | 103 | /** 104 | * @desc: 找到import并返回全路径和原始路径 105 | * @param {string} ele 找到的行引入 106 | * @param {string} fullPath 文件的全路径 107 | */ 108 | export function changeImport( 109 | ele: string, 110 | fullPath: string, 111 | dependencies: string[], 112 | nochangePath?: Boolean, 113 | toAbsoluteAlias?: Boolean 114 | ) { 115 | const impName = getImportName(ele, dependencies) 116 | if (!impName) return null 117 | 118 | const absoluteImport = makeSuffix(impName, fullPath) 119 | const aliasPath = absoluteImport.replace(rootPath, '@') 120 | const obj = { 121 | impName: nochangePath ? impName : toAbsoluteAlias ? aliasPath : getRelatPath(absoluteImport, fullPath), 122 | filePath: impName, 123 | absoluteImport 124 | } 125 | return obj 126 | } 127 | 128 | /** 129 | * @desc: 写文件 130 | * @param {string} file 目标地址 131 | */ 132 | export async function writeToFile( 133 | node: ItemType, 134 | isRelative?: Boolean, 135 | nochangePath?: Boolean, 136 | toAbsoluteAlias?: Boolean 137 | ) { 138 | const { fullPath } = node 139 | const packageJsonPath = path.join(rootPath, 'package.json') 140 | const dependencies = await getDependencies(packageJsonPath) 141 | 142 | try { 143 | const fileStr = await readFile(fullPath, 'utf-8') 144 | const lines = fileStr.split(/[\n]/g) 145 | 146 | // 使用 map() 来处理每一行 147 | const updatedLines = lines.map((line) => { 148 | if (line.includes('from') && isRelative) { 149 | const obj = changeImport(line, fullPath, dependencies, nochangePath, toAbsoluteAlias) 150 | if (obj && obj.impName) { 151 | // 使用模板字符串来增加可读性 152 | logger.info(`Updating import in node: ${node}`) 153 | return line.replace(obj.filePath, obj.impName) 154 | } 155 | } 156 | return line 157 | }) 158 | 159 | // 检查是否有任何变化 160 | if (updatedLines.join('\n') !== fileStr) { 161 | if (process.env.AGMD_DRY_RUN === '1') { 162 | logger.info(`Dry-run: would write file ${fullPath}`) 163 | } else { 164 | await writeFile(fullPath, updatedLines.join('\n'), 'utf-8') 165 | logger.success(`Write file successful: ${fullPath}`) 166 | } 167 | } 168 | } catch (error) { 169 | // 提供更详细的错误信息 170 | logger.error(`Error reading file: ${fullPath}, Error: ${error}`) 171 | } 172 | } 173 | /** 174 | * @description: Write the result to JS file 把结果写入到js文件 175 | * @param {data} 要写的数据 176 | * @return {fileName} 要写入文件地址 177 | */ 178 | export async function wirteJsNodes(data: string, filePath: string): Promise { 179 | const file = path.resolve(rootPath, filePath) 180 | const content = `export default ${data}` 181 | if (process.env.AGMD_DRY_RUN === '1') { 182 | logger.info(`Dry-run: would write file ${filePath}`) 183 | } else { 184 | await writeFile(file, content, { encoding: 'utf8' }) 185 | logger.success(`Write file successful: ${filePath}`) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/commands/get-file.ts: -------------------------------------------------------------------------------- 1 | /* 获取文件相关方法 */ 2 | import fs from 'fs' 3 | import path from 'path' 4 | import { readFile, readdir } from 'fs/promises' 5 | 6 | import { changeImport } from './change-path' 7 | import { getDependencies } from '../utils/router-utils' 8 | import type { ItemType, OptionType } from '../types' 9 | 10 | const rootPath = process.cwd().replace(/\\/g, '/') 11 | 12 | //File filtering -- full name with suffix required 文件过滤--需要全称带后缀 13 | const ignore = [ 14 | 'es6', 15 | 'lib', 16 | 'jest.config.js', 17 | 'router', 18 | 'img', 19 | 'styles', 20 | 'node_modules', 21 | 'LICENSE', 22 | '.git', 23 | '.github', 24 | 'dist', 25 | '.husky', 26 | '.vscode', 27 | '.eslintrc.js', 28 | 'readme-file.js', 29 | 'readme-md.js' 30 | ] 31 | 32 | /** 33 | * @description:Gets the header comment of the file 获取文件的头部注释 34 | * @param {*} fullPath 35 | * @return {*} 36 | */ 37 | export async function getFile(fullPath: string) { 38 | const str = await readFile(fullPath, 'utf-8') 39 | const size = str.length 40 | const sarr = str.split(/[\n]/g) 41 | const rowSize = sarr.length 42 | const imports = await getImport(sarr, fullPath) 43 | const f = 44 | sarr[0].indexOf('eslint') === -1 && 45 | (sarr[0].indexOf('-->') > -1 || sarr[0].indexOf('*/') > -1 || sarr[0].indexOf('//') > -1) 46 | ? sarr[0] 47 | : '' 48 | return { 49 | note: f.replace(/<\/?[^>]*>|(\n|\r)/g, ''), // 去掉尾巴换行符号 50 | size, 51 | rowSize, 52 | imports 53 | } 54 | } 55 | 56 | /** 57 | * @desc: 这是初始化时就获取每个文件依赖的方法, 但要求先补全后缀,否则不灵 58 | * @param {any} sarr 59 | * @param {string} fullPath 60 | */ 61 | export async function getImport(sarr: any[], fullPath: string) { 62 | const packageJsonPath = path.join(rootPath, 'package.json') 63 | const dependencies = await getDependencies(packageJsonPath) 64 | // 这里获取每个文件的import路径 65 | const imports: string[] = [] 66 | sarr.forEach((ele: string) => { 67 | if (ele.indexOf('from') > -1) { 68 | const obj = changeImport(ele, fullPath, dependencies) 69 | if (obj) { 70 | const { absoluteImport } = obj 71 | if (absoluteImport) { 72 | imports.push(absoluteImport) 73 | } 74 | } 75 | } 76 | }) 77 | return imports 78 | } 79 | 80 | // 获取文件或目录的信息 81 | function getFileInfo(dir: string, item: string, level: number): ItemType { 82 | const fullPath = path.join(dir, item) 83 | const isDir = fs.lstatSync(fullPath).isDirectory() 84 | return { 85 | name: item, 86 | isDir, 87 | level, 88 | note: '', 89 | imports: new Array(), 90 | belongTo: new Array() 91 | } as ItemType 92 | } 93 | 94 | // 对文件和目录进行排序 95 | function sortFiles(files: ItemType[]): ItemType[] { 96 | return files.sort((a, b) => { 97 | if (!a.isDir && b.isDir) return 1 98 | if (a.isDir && !b.isDir) return -1 99 | return 0 100 | }) 101 | } 102 | 103 | // 处理目录 104 | async function handleDirectory( 105 | dir: string, 106 | item: ItemType, 107 | option: OptionType | undefined, 108 | level: number, 109 | nodes: ItemType[] 110 | ): Promise { 111 | await getFileNodes(path.join(dir, item.name), option, (item.children = []), level + 1) 112 | item.fullPath = path.join(dir, item.name).replace(/\\/g, '/') 113 | nodes.push(item) 114 | } 115 | 116 | // 处理文件 117 | async function handleFile(dir: string, item: ItemType, include: string[], nodes: ItemType[]): Promise { 118 | const fullPath = path.join(dir, item.name) 119 | const suffix = path.extname(fullPath) 120 | if (include.includes(suffix)) { 121 | const obj = await getFile(fullPath) 122 | Object.assign(item, obj) 123 | item.suffix = suffix 124 | item.fullPath = fullPath.replace(/\\/g, '/') 125 | nodes.push(item) 126 | } 127 | } 128 | 129 | /** 130 | * @description:Generate node information for all files 生成所有文件的node信息 131 | * @param {*} dir 要解析的路径 132 | * @param {Array} nodes 133 | * @param {Number} level 134 | * @return {*} 135 | */ 136 | export async function getFileNodes( 137 | dir: string = process.cwd(), 138 | option?: OptionType, 139 | nodes: ItemType[] = [], 140 | level: number = 0 141 | ): Promise { 142 | let include = ['.js', '.vue', '.ts', '.tsx'] 143 | let finalIgnore: string[] = ignore 144 | if (option) { 145 | finalIgnore = option.ignore || ignore 146 | include = option.include || include 147 | } 148 | 149 | const files = await readdir(dir) 150 | const tempFiles = await Promise.all(files.map((item) => getFileInfo(dir, item, level))) 151 | sortFiles(tempFiles) 152 | 153 | for (const item of tempFiles) { 154 | if (!finalIgnore.includes(item.name)) { 155 | if (item.isDir) { 156 | await handleDirectory(dir, item, option, level, nodes) 157 | } else { 158 | await handleFile(dir, item, include, nodes) 159 | } 160 | } 161 | } 162 | // logger.info('nodes: ', nodes) 163 | return nodes 164 | } 165 | 166 | /** 167 | * @description:Recursive file name + note 递归得到文件名+note 168 | * @param {Array} datas 169 | * @param {string} keys 170 | * @return {*} 171 | */ 172 | export function getNote(datas: ItemType[], keys?: string[]): string[] { 173 | const nodes = keys || [] 174 | for (let index = 0; index < datas.length; index++) { 175 | const obj = datas[index] 176 | const last = index === datas.length - 1 177 | const md = setMd(obj, last) 178 | nodes.push(md) 179 | if (obj.children) { 180 | //fold 181 | getNote(obj.children, nodes) 182 | } 183 | } 184 | return nodes 185 | } 186 | 187 | /** 188 | * @description:One obj generates one line of text 一个obj生成一个一行文字 189 | * @param {ItemType} obj 190 | * @param {Boolean} last Is it the last one 是不是最后一个 191 | * @return {*} 192 | */ 193 | export function setMd(obj: ItemType, last: Boolean): string { 194 | let filesString = '' 195 | const blank = '│ '.repeat(obj.level) // 重复空白 196 | const pre = `${blank}${last ? '└──' : '├──'} ${obj.name}` 197 | if (obj.isDir) { 198 | filesString += `${pre}\n` 199 | } else { 200 | filesString += `${pre} ${obj.note}\n` 201 | } 202 | return filesString 203 | } 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # agmd(auto generate md) 2 | 3 | > 这是一个前端代码管理的辅助工具,在任何需要生成文档的,文件夹下的控制台中输入`agmd`, 就能自动生成目录 md 说明(部分功能需要在src目录下), 同时能够统计分析当前工程的各类型文件总量和代码总量,还提供一些实用的工具,具体看下面功能特征 4 | 5 | [![]( https://camo.githubusercontent.com/28479a7a834310a667f36760a27283f7389e864a/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f6c2f76322d646174657069636b65722e737667)]( https://camo.githubusercontent.com/28479a7a834310a667f36760a27283f7389e864a/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f6c2f76322d646174657069636b65722e737667) 6 | [![]( https://github.com/kakajun/auto-generate-md/actions/workflows/test.yml/badge.svg?branch=master)]( https://github.com/kakajun/auto-generate-md/actions/workflows/test.yml) 7 | [![]( https://app.circleci.com/pipelines/github/kakajun/auto-generate-md)]( https://app.circleci.com/pipelines/github/kakajun/auto-generate-md) 8 | 9 | 简体中文 | [English](https://github.com/kakajun/auto-generate-md/blob/master/README.EN.md) 10 | 11 | ## 🚀 功能特性 12 | 13 | 😍 一键统计工程的文件数和代码总量 14 | 15 | 📦 一键补充缺省后缀名 .js .vue 这样子能方便vscode编辑器点击直接跳转查看代码 16 | 17 | 🚘 一键更改文件或者文件名,驼峰命名为kable-case 18 | 19 | ⛵ 把工程所有引用更改绝对路径为相对路径(方便点击下钻查看文件) 20 | 🔧 支持将相对路径统一转换为基于 '@' 别名的绝对路径 21 | 22 | ♨️ 把工程按路由标记分类(对拆分工程很重要) 23 | 24 | ☝️ 把工程按分类对拆分工程(自动拆分的错误可控, 手动拆分会有各种问题) 25 | 26 | ✈️ 全程界面命令选择操作, 不用记命令 27 | 28 | 😍 得到一个包含整个工程结构树的json 29 | 30 | 💡 一键拿到文件和文件夹名字, 并生成JSON输出 31 | 32 | 🔥 用TypeScript书写,85%的代码全部书写了测试用例 33 | 34 | ## 设计初衷 35 | 36 | 1. 统计工程量, 看看咱们工程究竟有多少个文件, 多少代码量 37 | 2. 如果仅仅只是为了重命文件和文件夹, 那么其实这个库没有存在的必要, 因为我们可以自己随手写个node代码, 就全部重命名了, 问题的关键是, 我们内部工程有很多依赖, 像这样子 `import { replaceName } from '../src/commands/rename-path'` , 我们需要一个工具, 能够帮我们自动生成目录, 快速定位到文件,也进行重命名来减少重复劳动, 提升效率. 38 | 3. 如果想把工程1分2做成按路由拆分的微服务, 那么需要知道哪些文件被工程1用到了, 哪些文件被工程2用到了, 那么就需要一个工具, 来进行标记,甚至清除没有被标记的文件. 39 | 40 | 41 | ### 操作界面 42 | 43 | ![image](https://github.com/kakajun/auto-generate-md/blob/master/md3.png) 44 | ### 案例 45 | 46 | ![image](https://github.com/kakajun/auto-generate-md/blob/master/md2.png) 47 | 48 | ### 使用方法 49 | 需要有node环境 50 | 1. 全局安装 51 | > npm i agmd -g 52 | 53 | 安装完成后,在需要记录 md 的文件夹下面输入`agmd`,会自动生成相对路径下的文件夹和文件的名字,如果文件里面还有在头部写注释的话,那么会一并带过来自动生成 md 文件。生成的文件名为`readme-md.md`, 路径为刚刚输入命名的路径同级别下,对于工程比较大的开发来说,这个脚本或许会帮你省下些许时间。 54 | 55 | 2. 作为依赖安装 56 | > npm i agmd -D 57 | 58 | 在package.json的scripts 中配置 agmd: npx agmd --ignore lib,node_modules,dist --include .js,.ts,.vue [--dry-run] [--silent] 可以在每次启动或打包时,带上命令行来自动更新文档 59 | 60 | 61 | example,是我为演示准备的一些文件,并没有其他用 62 | 63 | 64 | 3. 命令说明 65 | - 帮助 66 | - 生成结构树文档 67 | - 修改为相对路径 68 | - 修改为绝对路径 69 | - 补全文件后缀 70 | - 统一命名文件夹为 Kebab-Case 71 | - 统一命名文件为 Kebab-Case 72 | - 记录节点 JSON 73 | - 给需要分类的都打上标记 74 | - 删除标记 75 | - 分类 76 | 77 | 78 | 4. 代码结构说明(由本插件agmd生成) 79 | ``` 80 | ├── bin 81 | │ └── bin.js 82 | ├── lib 83 | │ ├── commands 84 | │ │ ├── get-file.d.ts 85 | │ │ └── wirte-md.d.ts 86 | │ ├── index.cjs.js 87 | │ ├── index.d.ts 88 | │ └── index.esm.js 89 | ├── script 90 | │ ├── cli 91 | │ │ ├── handle.ts 92 | │ │ └── index.ts 93 | │ ├── help 94 | │ │ └── index.ts 95 | ├── src 96 | │ ├── commands 97 | │ │ ├── agmd.ts 98 | │ │ ├── base.ts /* 界面命令注册在这里 */ 99 | │ │ ├── change-path.ts /* 整个文件主要把绝对路径修改为相对路径 */ 100 | │ │ ├── get-file.ts /* 获取文件相关方法 */ 101 | │ │ ├── mark-file.ts 102 | │ │ ├── mark-write-file.ts 103 | │ │ └── wirte-md.ts /* 生成md说明文档 */ 104 | │ ├── shared 105 | │ │ ├── constant.ts 106 | │ ├── bin.ts 107 | │ └── index.ts /* 这里抛出一些高级操作方法 */ 108 | ├── test 109 | │ └── index.js 110 | └── unuse 111 | ``` 112 | 113 | 5. 高级用法 114 | 给文件打标记分类, 需要在src的同级目录下, 设置一个文件叫classify.js, 从里面读取需要配置的信息, 注意路径一定是带@符号的绝对路径, 没有配置, 那么程序会自动退出 115 | 116 | 117 | 有些需要把自动生成的文档插入到某个自动生成的 md 当中, 该插件导出了自动生成的 md 数据方法, 还有`getFileNodes`获得所有文件的具体信息, 可以 DIY 做出不同的文档( 方法名不用记忆, 由于是ts写的,所以会自动点出来) 118 | >const agmd = require('agmd') 119 | 120 | es中: 121 | >import agmd from 'agmd' 122 | 123 | - 其中 agmd.getFileNodes() 可以获得具体文件相关的信息, 该函数可传一个参数 124 | 125 | - agmd.getMd() 得到最终输出的信息 126 | note: 上面两个方法均可传一个option入参,其格式为: 127 | option: { ignore: string[] | undefined; include: string[] | undefined } 128 | #### 命令行参数说明 129 | 1. 使用agmd -h 来查看帮助 130 | 2. 可以带上 --ignore 忽略输出文件或文件夹, 默认为: img,styles,node_modules,LICENSE,.git,.github,dist,.husky,.vscode,readme-file.js,readme-md.js 131 | 3. 可以带上 --include 要求只输出带此后缀文件, 默认只输出 .js,.vue,.ts, 可自己加jsx,json 等 132 | 133 | ### 创作背景 134 | 135 | 1. 大家有没有被要求写一个目录文件的 md 说明呢? 136 | 2. 或者工程目录和文件被移动位置重构了,这时还需要重新修改 md 文件里面的目录说明 137 | 3. 接手老工程,看了 md 说明,能对文件夹里面的文件功能做到一目了然,而不是点开对应文件去看 138 | 4. 分析源码工程需要做点笔记 139 | 5. 拆分老代码工程, 手工量大还容易出错, 程序控制又快又好 140 | 6. 为什么绝对路径要改相对路径? 大家用vscode重命名文件时, 有没发现, 引用文件是绝对路径时, 文件没变化.....而且点击下钻查看文件详情还点不下去????但是相对路径不会有这个问题 141 | 7. 文件有后缀能一目了然文件是js文件还是vue文件 142 | 8. 一切需要手工重复操作的, 都可以用插件脚本搞定, 留时间学新知识更好 143 | 9. 补全缀是刚需, 很多伙伴就不喜欢写文件后缀, 所以import引入的是组件还是js 得去查看, 很不方便 144 | 145 | ### 功能 146 | 147 | 1. 自动生成匹配目录的文件夹名和文件(已经按名称进行排序) 148 | 2. 自动进行层级目录判断进行缩进 149 | 3. 如果文件顶部有注释, 那么会自动进行判断 150 | 4. 支持在任意文件目录下递归查找下级文件(不要在很大目录下执行啊!!!递归直到该级目录下没有文件为止) 151 | 5. 支持命令行参数配置, 可以自定义忽略文件和过滤后缀名文件 152 | 6. 命令行解析 153 | 154 | 控制台命令: agmd --include --ignore [--dry-run] [--silent] 155 | 156 | 可选项: 157 | --include string / -in string.......... 包含解析的后缀 (以空格分隔) 158 | --ignore string / -i string........... 忽略文件名或目录 (以空格分隔) 159 | --dry-run / -d.................. 预演模式, 不对文件系统进行写入 160 | --silent / -s.................. 静默模式, 最小化日志输出 161 | 162 | 例子: 163 | --ignore / -i img,styles,node_modules,LICENSE,.git,.github,dist,.husky,.vscode,readme-file.js,readme-md.js 164 | --include / -in .js,.vue,.ts 165 | 166 | 注意: 167 | 配置中的字符串之间不应有空格 168 | 169 | 命令行例子: 170 | $ agmd --ignore lib node_modules dist --include .js .ts .vue --dry-run --silent 171 | 172 | ### 相关文章 173 | 174 | [掘金-自动生成目录 md 文件](https://juejin.cn/post/7030030599268073508) 175 | 176 | ### 写在最后 177 | 178 | 本工程有36个测试, 大家如果想扩展什么功能, 测试代码跑起来, 很方便, 也欢迎大家克隆本工程然后提交进行PR! 179 | 180 | ### 更新记录 181 | 0.1.3 182 | 1. 采用esbuild 进行打包 183 | 2. 并且用eslint, preter规范写法, 规范 184 | 3. 用ts进行改写 185 | 4. 支持gitee一键同步 186 | 187 | 0.2.0 188 | 支持命令行解析参数,可以动态传参 189 | 190 | 0.2.6 191 | 修复全局安装报错 192 | 193 | 0.2.9 194 | 新增文件统计功能 195 | 196 | 0.3.0 197 | ✈️ 全程界面命令选择操作 198 | 199 | ⛵ 把工程所有引用更改绝对路径为相对路径(方便点击下钻查看文件) 200 | 201 | ♨️ 把工程所有引用文件都加上后缀(方便点击下钻查看文件) 202 | 203 | 👏 把工程按路由标记分类(对拆分工程很重要) 204 | 205 | ☝️ 把工程按分类对拆分工程(自动拆分的错误可控, 手动拆分会有各种问题) 206 | 207 | 0.3.3 208 | 优化提示日志打印 209 | 对路由进行自动分析 210 | 增加单元测试到26个,覆盖率达到84%,一些没必要的方法就没测试 211 | 212 | 0.3.7 213 | 升级所有依赖到最新 214 | 215 | 0.3.8 216 | 操作界面改成中文的, 我还是做不到大爱全世界, 先给自己和中国伙伴用好就行了, 同时增加功能只补全后缀, 但不更改路径 217 | 218 | 0.3.14 219 | 重构代码, 修改打包有esbuild转为tsc编译, 同时修改里面本身为异步操作的fs.readFileSync改为await readFile ,同时新增部分测试用例使得覆盖率达到85% 220 | 221 | 0.4.1 222 | 重构代码, 新增功能: 223 | 1. 新增命令行参数 --dry-run / -d 预演模式, 不对文件系统进行写入 224 | 2. 新增命令行参数 --silent / -s 静默模式, 最小化日志输出 225 | 3. 新增命令行参数 --absolute-alias / -a 把工程所有引用文件都加上绝对路径别名(方便点击下钻查看文件) 226 | -------------------------------------------------------------------------------- /test/renamecopy.ts: -------------------------------------------------------------------------------- 1 | import { foldNode, fileNode } from './utils/nodes-test.ts' 2 | import fs from 'fs-extra' 3 | import { 4 | // renameFold, 5 | renameFilePath, 6 | changePathFold, 7 | changePathName, 8 | // renameFoldPath, 9 | replaceName, 10 | toCameCase, 11 | toKebabCase, 12 | checkCamelFile 13 | } from '../src/commands/rename-path.ts' 14 | // import { creatFile } from './utils/utils' 15 | import { createConsola } from 'consola' 16 | import type { ItemType } from '../src/types.ts' 17 | const rootPath = process.cwd().replace(/\\/g, '/') 18 | const logger = createConsola({ 19 | level: 4 20 | }) 21 | 22 | // Mock fs-extra functions for testing 23 | jest.mock('fs-extra', () => ({ 24 | pathExists: jest.fn(), 25 | copy: jest.fn(), 26 | rm: jest.fn(), 27 | rename: jest.fn() 28 | })) 29 | 30 | jest.mock('path', () => ({ 31 | ...jest.requireActual('path'), 32 | parse: jest.fn((path) => ({ base: path })) 33 | })) 34 | 35 | describe('rename.test的测试', () => { 36 | test('checkCamelFile --检测kebab-case', () => { 37 | const flag = checkCamelFile('MyTemplate.vue') 38 | logger.info('flag:', flag) 39 | expect(flag).toEqual(true) 40 | }) 41 | 42 | test('changePathFold --递归修改文件夹node的path', () => { 43 | changePathFold(foldNode, { newName: 'check-test-kable-case', filename: 'checkTestKableCase' }) 44 | const obj = { 45 | name: 'check-test-kable-case', 46 | isDir: true, 47 | level: 1, 48 | note: '', 49 | copyed: false, 50 | imports: [], 51 | belongTo: [], 52 | fullPath: rootPath + '/temp/check-test-kable-case', 53 | children: [ 54 | { 55 | name: 'check-test-kable-caseInner', 56 | isDir: true, 57 | level: 1, 58 | note: '', 59 | copyed: false, 60 | imports: [], 61 | belongTo: [], 62 | fullPath: rootPath + '/temp/check-test-kable-case/checkTestKableCaseInner' 63 | } 64 | ] 65 | } 66 | const str = JSON.stringify(obj) 67 | expect(JSON.stringify(foldNode)).toEqual(str) 68 | }) 69 | 70 | test('changePathName --递归修改文件里面的import', () => { 71 | changePathName(fileNode, { newName: 'you-template', filename: 'youTemplate' }) 72 | // logger.info('tempNode', JSON.stringify(fileNode)) 73 | const finalObj = { 74 | name: 'you-template', 75 | isDir: false, 76 | level: 2, 77 | note: ' // 我就是个注释', 78 | imports: [rootPath.toLowerCase() + '/temp/my-template.vue'], 79 | belongTo: [], 80 | size: 96, 81 | copyed: false, 82 | rowSize: 4, 83 | suffix: '.vue', 84 | fullPath: rootPath + '/temp/TestKableCase/you-template.vue' 85 | } 86 | expect(fileNode).toMatchObject(finalObj) 87 | }) 88 | }) 89 | 90 | 91 | 92 | describe('toCameCase', () => { 93 | it('should convert hyphen-separated strings to camel case', () => { 94 | expect(toCameCase('my-file-name')).toBe('MyFileName') 95 | }) 96 | }) 97 | 98 | describe('toKebabCase', () => { 99 | it('should convert camel case strings to kebab case', () => { 100 | expect(toKebabCase('MyFileName')).toBe('my-file-name') 101 | }) 102 | }) 103 | 104 | describe('renameFilePath', () => { 105 | const nodesTwo: ItemType[] = [ 106 | { 107 | name: 'TestKableCase', 108 | isDir: true, 109 | level: 1, 110 | note: '', 111 | copyed: false, 112 | imports: [], 113 | belongTo: [], 114 | fullPath: '/path/to/temp/TestKableCase', 115 | children: [ 116 | { 117 | name: 'TestKableCase2', 118 | isDir: true, 119 | level: 1, 120 | note: '', 121 | copyed: false, 122 | imports: [], 123 | belongTo: [], 124 | fullPath: '/path/to/temp/TestKableCase/TestKableCase2' 125 | } 126 | ] 127 | } 128 | ] 129 | 130 | // 假设的模拟函数,实际应根据你的逻辑实现 131 | const renameFileMock = jest.fn() 132 | const rewriteFileMock = jest.fn() 133 | 134 | beforeEach(() => { 135 | // 在每个测试开始前重置模拟函数 136 | renameFileMock.mockClear() 137 | rewriteFileMock.mockClear() 138 | // ;(fs.rename as jest.Mock).mockClear() 139 | }) 140 | 141 | it('should call renameFile and rewriteFile for each file without children', async () => { 142 | // 替换实际的 renameFile 和 rewriteFile 函数为模拟函数 143 | // 注意:在实际应用中,你可能需要在 renameFilePath 内部直接使用模拟函数,这里仅作示例 144 | ;(global as any).renameFile = renameFileMock 145 | ;(global as any).rewriteFile = rewriteFileMock 146 | 147 | await renameFilePath(nodesTwo) 148 | 149 | // 检查是否调用了 renameFile 和 rewriteFile 150 | expect(renameFileMock).toHaveBeenCalledTimes(2) 151 | expect(renameFileMock).toHaveBeenCalledWith(nodesTwo[0], true) 152 | 153 | // 检查 children 是否存在 154 | if (nodesTwo[0].children) { 155 | expect(renameFileMock).toHaveBeenCalledWith(nodesTwo[0].children[0], true) 156 | } 157 | 158 | expect(rewriteFileMock).toHaveBeenCalledTimes(2) 159 | expect(rewriteFileMock).toHaveBeenCalledWith(nodesTwo[0], true) 160 | 161 | if (nodesTwo[0].children) { 162 | expect(rewriteFileMock).toHaveBeenCalledWith(nodesTwo[0].children[0], true) 163 | } 164 | 165 | expect(fs.rename).toHaveBeenCalledTimes(2) // 假设 renameFile 内部调用了 fs.rename 166 | }) 167 | }) 168 | 169 | // describe('renameFoldPath', () => { 170 | // const nodes: ItemType[] = [ 171 | // { 172 | // name: 'TestKableCase', 173 | // isDir: true, 174 | // level: 1, 175 | // note: '', 176 | // copyed: false, 177 | // imports: [], 178 | // belongTo: [], 179 | // fullPath: '/path/to/temp/TestKableCase', 180 | // children: [ 181 | // { 182 | // name: 'TestKableCase2', 183 | // isDir: true, 184 | // level: 1, 185 | // note: '', 186 | // copyed: false, 187 | // imports: [], 188 | // belongTo: [], 189 | // fullPath: '/path/to/temp/TestKableCase/TestKableCase2' 190 | // } 191 | // ] 192 | // } 193 | // ] 194 | 195 | // beforeEach(() => { 196 | // // 在每个测试开始前重置模拟函数 197 | // // ;(renameFold as jest.Mock).mockClear() 198 | // }) 199 | 200 | // it('should call renameFold for each directory', async () => { 201 | // await renameFoldPath(nodes, true) 202 | 203 | // // 检查是否调用了 renameFold 正确次数 204 | // expect(renameFold).toHaveBeenCalledTimes(nodes.length) 205 | 206 | // // 检查具体的调用情况 207 | // nodes.forEach((node, index) => { 208 | // expect(renameFold).toHaveBeenNthCalledWith(index + 1, node, true) 209 | // }) 210 | // }) 211 | 212 | // it('should handle nested directories correctly', async () => { 213 | // await renameFoldPath(nodes, true) 214 | 215 | // // 检查嵌套目录的 renameFold 调用 216 | // const childNode = nodes[0].children![0] 217 | // expect(renameFold).toHaveBeenCalledWith(childNode, true) 218 | // }) 219 | // }) 220 | -------------------------------------------------------------------------------- /src/commands/rename-path.ts: -------------------------------------------------------------------------------- 1 | /* 给路由文件打标记, 把标记打到最后,因为头部已经给了注释 */ 2 | import fs from 'fs-extra' 3 | import type { ItemType } from '../types' 4 | import { 5 | readFile 6 | // writeFile 7 | } from 'fs/promises' 8 | import path from 'path' 9 | import { createConsola } from 'consola' 10 | import { getDependencies } from '../utils/router-utils' 11 | import { getImportName } from './change-path' 12 | const logger = createConsola({ 13 | level: process.env.AGMD_SILENT === '1' ? 0 : 4 14 | }) 15 | const rootPath = process.cwd().replace(/\\/g, '/') 16 | /** 17 | * 将单个字符串的首字母小写 18 | * @param str 字符串 19 | */ 20 | function fistLetterLower(str: string | String) { 21 | return str.charAt(0).toLowerCase() + str.slice(1) 22 | } 23 | 24 | export function toKebabCase(str: string) { 25 | const regex = /[A-Z]/g 26 | return fistLetterLower(str).replace(regex, (word: string) => { 27 | return '-' + word.toLowerCase() 28 | }) 29 | } 30 | 31 | export function toCameCase(name: string) { 32 | // 使用正则表达式匹配中划线和随后的字符,同时将它们转换为大写 33 | let formattedName = name.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase()) 34 | 35 | // 如果第一个字母不是大写,创建一个新的字符串并将其转换为大写 36 | if (formattedName[0] === formattedName[0].toLowerCase()) { 37 | formattedName = formattedName.charAt(0).toUpperCase() + formattedName.slice(1) 38 | } 39 | 40 | return formattedName 41 | } 42 | /** 43 | * 检测驼峰文件名 44 | * @param fileName 文件名 45 | */ 46 | export function checkCamelFile(fileName: string) { 47 | return /([a-z])([A-Z])/.test(fileName) || /([A-Z])/.test(fileName) 48 | } 49 | 50 | /** 51 | * 检测大驼峰文件名 52 | * @param fileName 文件名 53 | */ 54 | export function checkUperCamelFile(fileName: string) { 55 | return /([A-Z])/.test(fileName) 56 | } 57 | 58 | /** 59 | * @desc: 循环node, 改文件夹, 并把import 里面不合格的命名改合格 60 | */ 61 | export async function renameFoldPath(nodes: ItemType[], isCamelCase?: Boolean) { 62 | async function getNode(cpNodes: ItemType[]) { 63 | for (let index = 0; index < cpNodes.length; index++) { 64 | const ele = cpNodes[index] 65 | isCamelCase ? await renameFold(ele, true) : await renameFold(ele) // 下面已递归 66 | if (ele.children) { 67 | // 递归 68 | await getNode(ele.children) 69 | } 70 | } 71 | } 72 | await getNode(nodes) 73 | } 74 | 75 | /** 76 | * @desc: 循环node, 改文件, 改依赖, 思路:循环每个文件, 并把import 里面不合格的命名改合格 77 | */ 78 | export async function renameFilePath(nodes: ItemType[]) { 79 | async function getNode(cpNodes: ItemType[]) { 80 | for (let index = 0; index < cpNodes.length; index++) { 81 | const ele = cpNodes[index] 82 | if (ele.children) { 83 | // 递归 84 | await getNode(ele.children) 85 | } else { 86 | // 重命名文件 87 | await renameFile(ele) 88 | // 重写文件的import 89 | await rewriteFile(ele) 90 | } 91 | } 92 | } 93 | await getNode(nodes) 94 | } 95 | 96 | export async function renameCamelCaseFilePath(nodes: ItemType[]) { 97 | async function getNode(cpNodes: ItemType[]) { 98 | for (let index = 0; index < cpNodes.length; index++) { 99 | const ele = cpNodes[index] 100 | if (ele.children) { 101 | // 递归 102 | await getNode(ele.children) 103 | } else { 104 | // 重命名文件 105 | await renameCamelCaseFile(ele) 106 | // 重写文件的import 107 | await rewriteFile(ele, true) 108 | } 109 | } 110 | } 111 | await getNode(nodes) 112 | } 113 | 114 | async function rewriteFile(node: ItemType, isCamelCase?: Boolean) { 115 | let writeFlag = false 116 | try { 117 | const fileContent = await readFile(node.fullPath, 'utf-8') 118 | const lines = fileContent.split(/\n/g) 119 | 120 | const packageJsonPath = path.join(rootPath, 'package.json') 121 | const dependencies = await getDependencies(packageJsonPath) 122 | 123 | for (let index = 0; index < lines.length; index++) { 124 | const importLine = lines[index] 125 | if (importLine.includes('from')) { 126 | const importModuleName = getImportName(importLine, dependencies) 127 | 128 | if (isCamelCase) { 129 | if (checkUperCamelFile(importModuleName)) { 130 | const newName = toCameCase(path.parse(importModuleName).name) 131 | const [beforeFrom, afterFrom] = importLine.split('from') 132 | lines[index] = `${beforeFrom}from${afterFrom.replace(importModuleName, newName)}` 133 | writeFlag = true 134 | } 135 | } else if (checkCamelFile(importModuleName)) { 136 | const newName = toKebabCase(path.parse(importModuleName).name) 137 | const [beforeFrom, afterFrom] = importLine.split('from') 138 | lines[index] = `${beforeFrom}from${afterFrom.replace(importModuleName, newName)}` 139 | writeFlag = true 140 | } 141 | } 142 | } 143 | 144 | if (writeFlag) { 145 | const updatedFileContent = lines.join('\n') 146 | try { 147 | if (process.env.AGMD_DRY_RUN === '1') { 148 | logger.info(`Dry-run: would rewrite file ${node.fullPath}`) 149 | } else { 150 | await fs.writeFile(node.fullPath, updatedFileContent, { encoding: 'utf8' }) 151 | logger.success(`Rewrote file successfully: ${node.fullPath}`) 152 | } 153 | } catch (writeError) { 154 | logger.error(`Failed to write file: ${node.fullPath}`, writeError) 155 | } 156 | } 157 | } catch (readError) { 158 | logger.error(`Failed to read file: ${node.fullPath}`, readError) 159 | } 160 | } 161 | 162 | /** 163 | * @desc: 重命名文件夹 164 | * @param {ItemType} node 165 | */ 166 | export async function renameFold(node: ItemType, isCamelCase?: boolean) { 167 | const filename = path.parse(node.fullPath).base 168 | 169 | const shouldRename = isCamelCase ? checkUperCamelFile(filename) : checkCamelFile(filename) 170 | if (shouldRename && node.isDir) { 171 | const obj = await replaceName(node.fullPath, isCamelCase) 172 | changePathFold(node, obj) 173 | } 174 | } 175 | 176 | /** 177 | * @desc: 重命名后, 子文件都会存在路径的更改,也就要递归处理(既可以处理文件夹, 也可以处理文件) 178 | */ 179 | export function changePathFold(node: ItemType, renameInfo: { newName: string; filename: string }): void { 180 | const { newName, filename } = renameInfo 181 | 182 | // If the node has children, recursively call this function on each child. 183 | if (node.children) { 184 | for (const childNode of node.children as ItemType[]) { 185 | changePathFold(childNode, renameInfo) 186 | } 187 | } 188 | 189 | // Update the full path and name of the current node. 190 | node.fullPath = node.fullPath.replace(filename, newName) 191 | node.name = node.name.replace(filename, newName) 192 | 193 | // Optionally, log once at the outermost call instead of in every recursion. 194 | // This can be controlled by a flag or condition check if needed. 195 | // if (isOutermostCall) { 196 | // logger.info(node.fullPath, newName); 197 | // } 198 | } 199 | 200 | /** 201 | * @desc: 递归改所有路径名字 202 | * @param {ItemType} node 203 | * @param {object} obj 204 | */ 205 | export function changePathName(node: ItemType, obj: { newName: string; filename: string }, isCamelCase?: Boolean) { 206 | const { newName, filename } = obj 207 | if (node.fullPath.indexOf(filename) > -1) { 208 | if (node.imports.length > 0) { 209 | // import也要变化, 否则也会找不到路径 210 | const array = node.imports 211 | for (let j = 0; j < array.length; j++) { 212 | const ele = array[j] 213 | logger.info('import-ele: ', ele) 214 | array[j] = isCamelCase ? toCameCase(filename) : toKebabCase(ele) 215 | logger.info('更换import: ', array[j]) 216 | } 217 | } 218 | node.fullPath = node.fullPath.replace(filename, newName) 219 | node.name = node.name.replace(filename, newName) 220 | logger.success('替换后的 node.fullPath:', node.fullPath) 221 | } 222 | } 223 | 224 | /** 225 | * @desc: 重命名文件 226 | * @param {ItemType} node 227 | */ 228 | export async function renameFile(node: ItemType) { 229 | const filename = path.parse(node.fullPath).base 230 | if (checkCamelFile(filename)) { 231 | const suffix = ['.js', '.vue', '.tsx'] // 这里只重命名js和vue文件 232 | const lastName = path.extname(node.fullPath) 233 | const flag = suffix.some((item) => lastName === item) 234 | if (flag) { 235 | const obj = await replaceName(node.fullPath) 236 | // 这里一定要更新node,否则后面找不到路径 237 | changePathName(node, obj) 238 | } 239 | } 240 | } 241 | 242 | export async function renameCamelCaseFile(node: ItemType) { 243 | const filename = path.parse(node.fullPath).base 244 | if (!checkUperCamelFile(filename)) { 245 | const suffix = ['.vue'] // 这里只重命名vue文件为大驼峰 246 | const lastName = path.extname(node.fullPath) 247 | const flag = suffix.some((item) => lastName === item) 248 | if (flag) { 249 | const obj = await replaceName(node.fullPath, true) 250 | // 这里一定要更新node,否则后面找不到路径 251 | changePathName(node, obj, true) 252 | } 253 | } 254 | } 255 | 256 | /** 257 | * 重命名文件夹 CamelCase || PascalCase => kebab-case 258 | * @param fullPath 259 | * @return {newName:'my-file.txt','myFile.txt'} 260 | */ 261 | export async function replaceName(fullPath: string, isCamelCase?: Boolean) { 262 | const filename = path.parse(fullPath).base 263 | const newName = isCamelCase ? toCameCase(filename) : toKebabCase(filename) 264 | 265 | try { 266 | const oldPath = fullPath 267 | console.log('oldPath: ', oldPath) 268 | const newPath = oldPath.replace(filename, newName) 269 | const lastName = path.extname(newPath) 270 | if (!lastName) { 271 | // 处理目录 272 | if (fs.existsSync(newPath)) { 273 | if (process.env.AGMD_DRY_RUN === '1') { 274 | logger.info(`Dry-run: would copy dir ${oldPath} -> ${newPath} and remove ${oldPath}`) 275 | } else { 276 | await fs.copy(oldPath, newPath) 277 | await fs.rm(oldPath, { recursive: true }) // 删除目录 278 | } 279 | return { newName, filename } 280 | } 281 | } 282 | // 处理文件 283 | if (await fs.pathExists(oldPath)) { 284 | if (process.env.AGMD_DRY_RUN === '1') { 285 | logger.info(`Dry-run: would rename ${oldPath} -> ${newPath}`) 286 | } else { 287 | await fs.rename(oldPath, newPath) 288 | logger.success(`${oldPath} renamed to: ${newPath}`) 289 | } 290 | } else { 291 | logger.error(`File ${oldPath} does not exist.`) 292 | } 293 | logger.info(`${filename} is renamed done`) 294 | return { newName, filename } 295 | } catch (error) { 296 | logger.error(`Error renaming file/directory: ${error}`) 297 | throw error 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /codeAndPrompt.md: -------------------------------------------------------------------------------- 1 | 下面是整个工程的目录文件结构 2 | ├── api 3 | │ ├── aa.js 4 | │ └── user.js //2工程 5 | ├── base 6 | │ └── temp 7 | │ │ ├── aa.vue // 我就是个注释 8 | │ │ └── app-file-test.vue 9 | ├── script 10 | │ ├── cli 11 | │ │ ├── handle.ts 12 | │ │ └── index.ts 13 | │ └── help 14 | │ │ └── index.ts 15 | ├── src 16 | │ ├── commands 17 | │ │ ├── agmd.ts 18 | │ │ ├── change-path.ts 19 | │ │ ├── command-actions.ts /* 界面命令注册在这里 */ 20 | │ │ ├── command-handler.ts // 命令处理逻辑 21 | │ │ ├── get-file.ts /* 获取文件相关方法 */ 22 | │ │ ├── get-router.ts 23 | │ │ ├── mark-file.ts /* 给路由文件打标记, 把标记打到最后,因为头部已经给了注释 */ 24 | │ │ ├── mark-write-file.ts 25 | │ │ ├── rename-path.ts /* 给路由文件打标记, 把标记打到最后,因为头部已经给了注释 */ 26 | │ │ └── wirte-md.ts /* 生成md说明文档 */ 27 | │ ├── shared 28 | │ │ └── constant.ts /* 解析package */ 29 | │ ├── utils 30 | │ │ └── router-utils.ts 31 | │ ├── bin.ts 32 | │ ├── index.ts /* 这里抛出一些高级操作方法 */ 33 | │ └── types.ts // 定义 Router 接口 34 | ├── temp 35 | │ ├── check-test-kable-case2 36 | │ │ └── testTemplate.vue // 我就是个注释 37 | │ ├── my 38 | │ │ ├── aa.vue // 我就是个注释 39 | │ │ └── wite-file2.vue // 我就是个注释 40 | │ ├── myVue 41 | │ │ └── myTable 42 | │ │ │ └── test-template.vue // 我就是个注释 43 | │ ├── test-kable-case 44 | │ │ ├── test-kable-case2 45 | │ │ └── youTemplate.vue // 我就是个注释 46 | │ ├── aa.vue // 我就是个注释 47 | │ ├── app-file-test.vue // 我就是个注释 48 | │ ├── app2-file-test.vue //mark 49 | │ ├── bb.vue 50 | │ ├── delet-mark-all.vue // 我就是个注释 51 | │ ├── mark-setmark.vue //setmark 52 | │ └── wite-file-test.vue // 我就是个注释 53 | ├── test 54 | │ ├── config 55 | │ │ ├── jest-global-setup.ts 56 | │ │ └── jest.setup.ts 57 | │ ├── utils 58 | │ │ ├── deep-nodes-test.ts 59 | │ │ ├── function-test.ts 60 | │ │ ├── nodes-test.ts 61 | │ │ └── utils.ts /* 测试公共方法 */ 62 | │ ├── __mocks__ 63 | │ │ └── fs.ts 64 | │ ├── change-path.test.ts 65 | │ ├── get-file.test.ts 66 | │ ├── get-router.test.ts 67 | │ ├── mark-file.test.ts 68 | │ ├── mark-write-file.test.ts // import { createConsola } from 'consola' 69 | │ ├── rename-path.test.ts 70 | │ ├── rename.test.ts 71 | │ ├── renamecopy.ts 72 | │ └── wirte-md.test.ts 73 | ├── test2 74 | │ └── temp 75 | │ │ ├── my 76 | │ │ │ ├── aa.vue // 我就是个注释 77 | │ │ │ └── wite-file2.vue // 我就是个注释 78 | │ │ └── wite-file-test.vue // 我就是个注释 79 | ├── unuse 80 | │ ├── assets 81 | │ ├── components 82 | │ │ ├── test 83 | │ │ │ └── deep 84 | │ │ │ │ └── user.vue //2工程 85 | │ │ ├── test2 86 | │ │ │ └── HelloWorld.vue //2工程 87 | │ │ └── user-rulerts.vue 88 | │ ├── test 89 | │ │ ├── index.js /* 我就是个测试 */ 90 | │ │ └── user-rulerts.vue 91 | │ ├── App.vue 92 | │ ├── main.js //2工程 93 | │ └── mixins.js 94 | ├── classify.js 95 | └── jest.config.ts 96 | 97 | 😍 代码总数统计: 98 | 后缀是 .js 的文件有 6 个 99 | 后缀是 .vue 的文件有 22 个 100 | 后缀是 .ts 的文件有 35 个 101 | 总共有 63 个文件 102 | 总代码行数有: 3,406行, 103 | 总代码字数有: 88,680个 104 | path:/api/aa.js 105 | export default function name(params) { 106 | 107 | } 108 | //2工程 109 | 110 | path:/script/cli/handle.ts 111 | import help from '../help' 112 | import pkg from '../../package.json' 113 | interface parseType { 114 | version?: Boolean | undefined 115 | includes?: string[] 116 | ignores?: string[] 117 | help?: Boolean | undefined 118 | ignore?: string | undefined 119 | include?: string | undefined 120 | } 121 | function handle(settings: parseType) { 122 | if (settings.help) { 123 | help() 124 | } 125 | if (settings.version) { 126 | console.log(`agmd version is: ` + '\x1B[36m%s\x1B[0m', pkg.version) 127 | process.exit(0) 128 | } 129 | if (settings.ignore) { 130 | settings.ignores = settings.ignore.split(' ') 131 | } 132 | if (settings.include) { 133 | settings.includes = settings.include.split(' ') 134 | } 135 | return settings 136 | } 137 | 138 | export default handle 139 | 140 | path:/src/shared/constant.ts 141 | /* 解析package */ 142 | import { name, version } from '../../package.json'; 143 | 144 | export const CWD = process.cwd(); 145 | 146 | export const VERSION = version; 147 | 148 | export const PKG_NAME = name; 149 | 150 | path:/script/help/index.ts 151 | const st = `使用说明: 152 | 1. 在控制台按上下切换功能并回车进行确认, 执行相对应的操作! 153 | 2. 可以在package.json中的scripts下面配置如下,然后运行命令: 154 | agmd --include str --ignore str 155 | 选项: 156 | --include string / -i string.......... 包括文件扩展名 157 | --ignore string / -in string........... 忽略文件或者文件夹 158 | 159 | 各选项的默认配置: 160 | --ignore img,styles,node_modules,LICENSE,.git,.github,dist,.husky,.vscode,readme-file.js,readme-md.js 161 | --include .js,.vue,.ts,.tsx 162 | 163 | 说明: 164 | 配置中的字符串之间不应有空格 165 | 166 | 举例: 167 | 168 | $ agmd --ignore lib,node_modules,dist --include .js,.ts,.vue` 169 | 170 | function help() { 171 | console.log(st) 172 | process.exit(0) 173 | } 174 | export default help 175 | 176 | path:/base/temp/aa.vue 177 | // 我就是个注释 178 | 181 | path:/src/utils/router-utils.ts 182 | 183 | import { access, readFile } from 'fs/promises'; 184 | /** 185 | * 解析路由文件中的路由路径。 186 | * @param {string} line - 路由文件中的一行。 187 | * @return {string} - 解析出的路由路径。 188 | */ 189 | export function parseRouterPath(line: string): string { 190 | const pathRegex = /path:\s*['"]([^'"]+)['"]/ 191 | const match = line.match(pathRegex) 192 | return match ? match[1] : '' 193 | } 194 | 195 | /** 196 | * 解析路由文件中的组件路径。 197 | * @param {string} line - 路由文件中的一行。 198 | * @return {string | ''} - 解析出的组件路径或null。 199 | */ 200 | export function parseComponentPath(line: string): string { 201 | const componentRegex = /component:\s*\(\)\s*=>\s*import\(['"]([^'"]+)['"]\)/ 202 | const match = line.match(componentRegex) 203 | return match ? match[1] : '' 204 | } 205 | 206 | export async function getDependencies(packageJsonPath: string): Promise { 207 | let dependencies: string[] = []; 208 | if (packageJsonPath) { 209 | try { 210 | await access(packageJsonPath); 211 | const pkg = JSON.parse(await readFile(packageJsonPath, 'utf-8')); 212 | dependencies = Object.keys(pkg.dependencies || {}).concat(Object.keys(pkg.devDependencies || {})); 213 | } catch (error) { 214 | console.error(error); 215 | } 216 | } 217 | return dependencies; 218 | } 219 | 220 | path:/src/commands/agmd.ts 221 | #!/usr/bin/env node 222 | /* 搞个文件做bug测试,命令行不好调试 */ 223 | import { generateAllAction } from './command-actions' 224 | import { getMd } from './wirte-md' 225 | import stringToArgs from '../../script/cli' 226 | import handle from '../../script/cli/handle' 227 | 228 | async function main() { 229 | const options = stringToArgs(process.argv) 230 | const { ignores: ignore, includes: include } = handle(options) 231 | const { md, nodes } =await getMd({ ignore, include }) 232 | await generateAllAction(nodes, md) 233 | } 234 | 235 | main() 236 | 237 | path:/temp/check-test-kable-case2/testTemplate.vue 238 | // 我就是个注释 239 | 242 | path:/src/bin.ts 243 | #!/usr/bin/env node 244 | import { Command } from 'commander' 245 | import { handleCommand } from './commands/command-handler' 246 | const program = new Command() 247 | program.action(handleCommand) 248 | program.parse(process.argv) 249 | 250 | path:/temp/my/aa.vue 251 | // 我就是个注释 252 | 254 | path:/temp/myVue/myTable/test-template.vue 255 | // 我就是个注释 256 | 259 | path:/temp/test-kable-case/youTemplate.vue 260 | // 我就是个注释 261 | 264 | path:/test/utils/deep-nodes-test.ts 265 | const rootPath = process.cwd().replace(/\\/g, '/') 266 | const nodeComponents = [ 267 | { 268 | name: 'test', 269 | isDir: true, 270 | level: 0, 271 | note: '', 272 | imports: [], 273 | belongTo: [], 274 | children: [ 275 | { 276 | name: 'deep', 277 | isDir: true, 278 | level: 1, 279 | note: '', 280 | imports: [], 281 | belongTo: [], 282 | children: [ 283 | { 284 | name: 'user.vue', 285 | isDir: false, 286 | level: 2, 287 | note: '//2工程', 288 | imports: [rootPath + '/api/user.js'], 289 | belongTo: [], 290 | size: 1791, 291 | rowSize: 109, 292 | suffix: '.vue', 293 | fullPath: rootPath + '/unuse/components/test/deep/user.vue' 294 | } 295 | ], 296 | fullPath: rootPath + '/unuse/components/test/deep' 297 | } 298 | ], 299 | fullPath: rootPath + '/unuse/components/test' 300 | }, 301 | { 302 | name: 'test2', 303 | isDir: true, 304 | level: 0, 305 | note: '', 306 | imports: [], 307 | belongTo: [], 308 | children: [ 309 | { 310 | name: 'HelloWorld.vue', 311 | isDir: false, 312 | level: 1, 313 | note: '//2工程', 314 | imports: [rootPath + '/unuse/components/test/deep/user.vue'], 315 | belongTo: [], 316 | size: 411, 317 | rowSize: 31, 318 | suffix: '.vue', 319 | fullPath: rootPath + '/unuse/components/test2/HelloWorld.vue' 320 | } 321 | ], 322 | fullPath: rootPath + '/unuse/components/test2' 323 | }, 324 | { 325 | name: 'user-rulerts.vue', 326 | isDir: false, 327 | level: 0, 328 | note: '', 329 | imports: [rootPath + '/unuse/components/test/deep/user.vue'], 330 | belongTo: [], 331 | size: 2503, 332 | rowSize: 105, 333 | suffix: '.vue', 334 | fullPath: rootPath + '/unuse/components/user-rulerts.vue' 335 | } 336 | ] 337 | 338 | export default nodeComponents 339 | 340 | path:/test/config/jest-global-setup.ts 341 | import fs from 'fs-extra' 342 | import { createConsola } from 'consola' 343 | const rootPath = process.cwd().replace(/\\/g, '/') 344 | const logger = createConsola({ 345 | level: 4 346 | }) 347 | 348 | module.exports = async () => { 349 | logger.start('清空测试文件夹') 350 | // 你可以在这里执行一些全局初始化代码 351 | const foldPath = rootPath + '/temp' 352 | const foldPath2 = rootPath + '/test2' 353 | function deleteFolderRecursive(p: string) { 354 | if (fs.existsSync(p)) { 355 | fs.readdirSync(p).forEach((file) => { 356 | const curPath = `${p}/${file}` 357 | if (fs.lstatSync(curPath).isDirectory()) { 358 | deleteFolderRecursive(curPath) 359 | } else { 360 | fs.unlinkSync(curPath) 361 | } 362 | }) 363 | fs.rmdirSync(p) 364 | } 365 | } 366 | 367 | deleteFolderRecursive(foldPath) 368 | deleteFolderRecursive(foldPath2) 369 | } 370 | 371 | path:/temp/aa.vue 372 | // 我就是个注释 373 | 376 | path:/test/change-path.test.ts 377 | import path from 'path' 378 | import { readFile, writeFile } from 'fs/promises' 379 | import { getRelatPath, makeSuffix, changeImport, writeToFile, getImportName } from '../src/commands/change-path' 380 | import { nodeOne } from './utils/nodes-test' 381 | import { createConsola } from 'consola' 382 | const logger = createConsola({ 383 | level: 4 384 | }) 385 | const rootPath = process.cwd().replace(/\\/g, '/') 386 | describe('change-path的测试', () => { 387 | test('getRelatPath--获取相对地址', () => { 388 | expect(getRelatPath('/unuse/components/user-rulerts.vue', '/unuse/App.vue')).toEqual( 389 | './components/user-rulerts.vue' 390 | ) 391 | }) 392 | 393 | test('makeSuffix--补全后缀和@替换', () => { 394 | expect(makeSuffix('@/src/commands/change-path', '@/src/commands/change-path')).toEqual( 395 | path.resolve('src/commands/change-path.ts').replace(/\\/g, '/') 396 | ) 397 | }) 398 | test('makeSuffix--得到import', () => { 399 | const arrs = getImportName( 400 | `import 401 | { getRelatPath, 402 | makeSuffix, 403 | changeImport 404 | } from '@/unuse/components/user-rulerts'`, 405 | ['@types/node'] 406 | ) 407 | logger.info('arrs: ', arrs) 408 | expect(arrs).toEqual('@/unuse/components/user-rulerts') 409 | }) 410 | 411 | test('changeImport--更改不规范path', () => { 412 | expect( 413 | changeImport( 414 | "import { getRelatPath, makeSuffix, changeImport } from '@/unuse/components/user-rulerts'", 415 | path.resolve('unuse/App.vue').replace(/\\/g, '/'), 416 | ['@types/node'] 417 | ) 418 | ).toEqual({ 419 | filePath: '@/unuse/components/user-rulerts', 420 | impName: './components/user-rulerts.vue', 421 | absoluteImport: rootPath + '/unuse/components/user-rulerts.vue' 422 | }) 423 | }) 424 | 425 | test('writeToFile--更改不规范path', (done) => { 426 | try { 427 | const node = nodeOne[0] 428 | // 1. 随机创建一个文件 429 | const str = `` 435 | //2. 预期得到内容 436 | const finalStr = `` 442 | 443 | const file = path.resolve(rootPath, node.fullPath) 444 | logger.info('file: ', file) 445 | 446 | async function get() { 447 | // 异步写入数据到文件 448 | await writeFile(file, str, { encoding: 'utf8' }) 449 | logger.success('Write successful') 450 | await writeToFile(node, true) 451 | const getStr = await readFile(file, 'utf-8') 452 | expect(getStr).toEqual(finalStr) 453 | done() 454 | } 455 | get() 456 | } catch (error) { 457 | done(error) 458 | } 459 | }) 460 | }) 461 | 462 | path:/test/__mocks__/fs.ts 463 | import { fs } from 'memfs' 464 | 465 | fs.mkdirSync('/tmp') 466 | if (process.env.TMPDIR) { 467 | fs.mkdirSync(process.env.TMPDIR, { recursive: true }) 468 | } 469 | 470 | const fsRealpath = fs.realpath 471 | ;(fsRealpath as any).native = fsRealpath 472 | 473 | module.exports = { ...fs, realpath: fsRealpath } 474 | 475 | path:/test2/temp/my/aa.vue 476 | // 我就是个注释 477 | 479 | path:/test2/temp/wite-file-test.vue 480 | // 我就是个注释 481 | 483 | path:/unuse/components/test/deep/user.vue 484 | //2工程 485 | //2工程 486 | ue2.0写法 */ 487 | 516 | 522 | 581 | 582 | //1工程 583 | //1工程 584 | //2工程 585 | //2工程 586 | //2工程 587 | //2工程 588 | //2工程 589 | //2工程 590 | //2工程 591 | //2工程 592 | 593 | path:/unuse/test/index.js 594 | /* 我就是个测试 */ 595 | import app from '../app.vue' 596 | console.log('main') 597 | 598 | path:/unuse/components/user-rulerts.vue 599 | 600 | 702 | //2工程 703 | 704 | path:/unuse/App.vue 705 | 706 | 709 | 710 | 720 | 721 | 731 | //2工程 732 | 733 | path:/unuse/components/test2/HelloWorld.vue 734 | //2工程 735 | 736 | 737 | 746 | 747 | 748 | 764 | 765 | path:/classify.js 766 | export default [ 767 | { 768 | name: '2工程', 769 | router: [ 770 | { 771 | path: '/spc/list', 772 | component: '@/unuse/App.vue' 773 | }, 774 | { 775 | path: '/spc/list', 776 | component: '@/unuse/main.js' 777 | }, 778 | ] 779 | } 780 | ] 781 | 782 | path:/script/cli/index.ts 783 | import arg from 'arg' 784 | const stringToArgs = (rawArgs: string[]) => { 785 | const args = arg( 786 | { 787 | '--ignore': String, 788 | '--include': String, 789 | '--version': Boolean, 790 | '--help': Boolean, 791 | '-h': '--help', 792 | '-i': '--ignore', 793 | '-in': '--include', 794 | '-v': '--version' 795 | }, 796 | { 797 | argv: rawArgs.slice(2) 798 | } 799 | ) 800 | return { 801 | help: args['--help'], 802 | ignore: args['--ignore'], 803 | include: args['--include'], 804 | version: args['--version'] 805 | } 806 | } 807 | 808 | export default stringToArgs 809 | 810 | path:/api/user.js 811 | //2工程 812 | export default function name(params) {} 813 | //2工程 814 | 815 | path:/base/temp/app-file-test.vue 816 | 822 | path:/src/commands/change-path.ts 823 | import fs from 'fs' 824 | import path from 'path' 825 | import { readFile, writeFile } from 'fs/promises' 826 | import { createConsola } from 'consola' 827 | import { getDependencies } from '../utils/router-utils' 828 | import type { ItemType } from '../types' 829 | const logger = createConsola({ 830 | level: 4 831 | }) 832 | 833 | const rootPath = process.cwd().replace(/\\/g, '/') 834 | 835 | /** 836 | * 检查当前目录是否为项目根目录。 837 | * 根据是否存在 package.json 文件来判断。 838 | */ 839 | function isRootDirectory(): boolean { 840 | const packageJsonPath = path.join(process.cwd(), 'package.json') 841 | try { 842 | fs.accessSync(packageJsonPath, fs.constants.R_OK) 843 | return true 844 | } catch (error) { 845 | return false 846 | } 847 | } 848 | 849 | /** 850 | * @desc: 递归循环所有文件 851 | 852 | * @param {Array} nodes 整个文件的nodes 853 | */ 854 | export async function changePath(nodes: ItemType[], nochangePath?: Boolean) { 855 | async function getNode(objs: ItemType[]) { 856 | for (const ele of objs) { 857 | if (ele.children) { 858 | await getNode(ele.children) 859 | } else { 860 | if (isRootDirectory()) { 861 | await writeToFile(ele, true, nochangePath) 862 | } 863 | } 864 | } 865 | } 866 | await getNode(nodes) 867 | } 868 | 869 | /** 870 | * @desc: 这里返回没有@ 符号的路径 871 | * @param {string} absoluteImport 依赖本身名字 872 | * @param {string} fullPath 文件本身绝对地址 873 | */ 874 | export function getRelatPath(absoluteImport: string, fullPath: string) { 875 | let relatPath = path.relative(path.dirname(fullPath), absoluteImport).replace(/\\/g, '/') 876 | if (!relatPath.startsWith('.')) { 877 | relatPath = './' + relatPath 878 | } 879 | return relatPath 880 | } 881 | 882 | /** 883 | * @desc: 补后缀的方法+替换前缀 884 | * @param {string} filePath 正则匹配到的依赖路径 885 | * @param {string} fullPath 本身文件名路径 886 | * @param {string} impName 正确的名字 887 | */ 888 | export function makeSuffix(filePath: string, fullPath: string) { 889 | let absoluteImport = filePath.includes('@') 890 | ? filePath.replace('@', process.cwd()) 891 | : path.resolve(path.dirname(fullPath), filePath) 892 | 893 | const lastName = path.extname(absoluteImport) 894 | 895 | if (!lastName) { 896 | const suffixes = ['.ts', '.vue', '.tsx', '.js', '/index.js', '/index.vue'] 897 | for (const suffix of suffixes) { 898 | if (fs.existsSync(absoluteImport + suffix)) { 899 | absoluteImport += suffix 900 | // logger.info('补充后缀:', absoluteImport + suffix) 901 | break 902 | } 903 | } 904 | } 905 | return absoluteImport.replace(/\\/g, '/') 906 | } 907 | 908 | /** 909 | * @desc: 根据一行代码匹配import的详细内容 TODO 这里还得优化 910 | 911 | */ 912 | export function getImportName(ele: string, dependencies: string[]) { 913 | let str = '' 914 | const flag = dependencies.some((item) => ele.indexOf(item) > -1) 915 | const reg = / from [\"|\'](.*)[\'|\"]/ 916 | // 这里只收集组件依赖, 插件依赖排除掉 917 | if (!flag && ele.indexOf('/') > -1 && ele.indexOf('//') !== 0) { 918 | const impStr = ele.match(reg) 919 | // 没有import的不转 920 | if (impStr && impStr[1]) str = impStr[1] 921 | } 922 | return str 923 | } 924 | 925 | /** 926 | * @desc: 找到import并返回全路径和原始路径 927 | * @param {string} ele 找到的行引入 928 | * @param {string} fullPath 文件的全路径 929 | */ 930 | export function changeImport(ele: string, fullPath: string, dependencies: string[], nochangePath?: Boolean) { 931 | const impName = getImportName(ele, dependencies) 932 | if (!impName) return null 933 | 934 | const absoluteImport = makeSuffix(impName, fullPath) 935 | const obj = { 936 | impName: nochangePath ? impName : getRelatPath(absoluteImport, fullPath), 937 | filePath: impName, 938 | absoluteImport 939 | } 940 | return obj 941 | } 942 | 943 | /** 944 | * @desc: 写文件 945 | * @param {string} file 目标地址 946 | */ 947 | export async function writeToFile(node: ItemType, isRelative?: Boolean, nochangePath?: Boolean) { 948 | const { fullPath } = node 949 | const packageJsonPath = path.join(rootPath, 'package.json') 950 | const dependencies = await getDependencies(packageJsonPath) 951 | 952 | try { 953 | const fileStr = await readFile(fullPath, 'utf-8') 954 | const lines = fileStr.split(/[\n]/g) 955 | 956 | // 使用 map() 来处理每一行 957 | const updatedLines = lines.map((line) => { 958 | if (line.includes('from') && isRelative) { 959 | const obj = changeImport(line, fullPath, dependencies, nochangePath) 960 | if (obj && obj.impName) { 961 | // 使用模板字符串来增加可读性 962 | logger.info(`Updating import in node: ${node}`) 963 | return line.replace(obj.filePath, obj.impName) 964 | } 965 | } 966 | return line 967 | }) 968 | 969 | // 检查是否有任何变化 970 | if (updatedLines.join('\n') !== fileStr) { 971 | await writeFile(fullPath, updatedLines.join('\n'), 'utf-8') 972 | logger.success(`Write file successful: ${fullPath}`) 973 | } 974 | } catch (error) { 975 | // 提供更详细的错误信息 976 | logger.error(`Error reading file: ${fullPath}, Error: ${error}`) 977 | } 978 | } 979 | /** 980 | * @description: Write the result to JS file 把结果写入到js文件 981 | * @param {data} 要写的数据 982 | * @return {fileName} 要写入文件地址 983 | */ 984 | export async function wirteJsNodes(data: string, filePath: string): Promise { 985 | const file = path.resolve(rootPath, filePath) 986 | const content = `export default ${data}` 987 | await writeFile(file, content, { encoding: 'utf8' }) 988 | logger.success(`Write file successful: ${filePath}`) 989 | } 990 | 991 | path:/src/index.ts 992 | /* 这里抛出一些高级操作方法 */ 993 | import { getMd } from './commands/wirte-md' 994 | import { getFileNodes } from './commands/get-file' 995 | export { getMd, getFileNodes } 996 | 997 | path:/temp/my/wite-file2.vue 998 | // 我就是个注释 999 | 1002 | path:/test/utils/function-test.ts 1003 | import { replaceName } from '../../src/commands/rename-path' 1004 | import { createConsola } from 'consola' 1005 | // const rootPath = process.cwd().replace(/\\/g, '/') 1006 | const logger = createConsola({ 1007 | level: 4 1008 | }) 1009 | 1010 | async function get() { 1011 | const p = await replaceName('/path/to/myFile.txt') 1012 | logger.info('p: ', p) 1013 | logger.info('我这里来了!!!') 1014 | } 1015 | get() 1016 | 1017 | path:/test/config/jest.setup.ts 1018 | import fs from 'fs-extra' 1019 | import { createConsola } from 'consola' 1020 | const rootPath = process.cwd().replace(/\\/g, '/') 1021 | const foldPath = rootPath + '/temp' 1022 | const logger = createConsola({ 1023 | level: 4 1024 | }) 1025 | 1026 | beforeAll(() => { 1027 | logger.info('new unit test start') 1028 | fs.ensureDirSync(foldPath) 1029 | // 你可以在这里执行一些全局初始化代码 1030 | }) 1031 | 1032 | path:/temp/app-file-test.vue 1033 | // 我就是个注释 1034 | 1037 | path:/test/get-file.test.ts 1038 | import { getFile, getImport, getFileNodes, getNote, setMd } from '../src/commands/get-file' 1039 | import { creatFile } from './utils/utils' 1040 | import type { ItemType } from '../src/types' 1041 | import deepNodes from './utils/deep-nodes-test' 1042 | import { createConsola } from 'consola' 1043 | const rootPath = process.cwd().replace(/\\/g, '/') 1044 | const logger = createConsola({ 1045 | level: 4 1046 | }) 1047 | 1048 | // 由于linux的空格数和window的空格数不一样, 所以size始终不一样, 无法测试, 所以这里干掉size 1049 | // 递归树结构设置size为0 1050 | function setSize(temparrs: any[]) { 1051 | temparrs.forEach((item) => { 1052 | item.size = 0 1053 | if (item.children) { 1054 | setSize(item.children) 1055 | } 1056 | }) 1057 | } 1058 | 1059 | describe('setMd', () => { 1060 | it('should correctly format the string for a directory', () => { 1061 | const obj: ItemType = { 1062 | name: 'dir', 1063 | isDir: true, 1064 | level: 1, 1065 | note: '', 1066 | fullPath: '', 1067 | belongTo: [], 1068 | imports: [] 1069 | } 1070 | 1071 | const result = setMd(obj, false) 1072 | 1073 | expect(result).toEqual('│ ├── dir\n') 1074 | }) 1075 | 1076 | it('should correctly format the string for a file', () => { 1077 | const obj: ItemType = { 1078 | name: 'file.js', 1079 | isDir: false, 1080 | level: 1, 1081 | note: 'note', 1082 | fullPath: '', 1083 | belongTo: [], 1084 | imports: [] 1085 | } 1086 | const result = setMd(obj, true) 1087 | expect(result).toEqual('│ └── file.js note\n') 1088 | }) 1089 | }) 1090 | 1091 | describe('get-file的测试', () => { 1092 | test('getFile--获取注释', (done) => { 1093 | const file = rootPath + '/temp/app-file-test.vue' 1094 | const file2 = rootPath + '/temp/aa.vue' 1095 | try { 1096 | async function get() { 1097 | await creatFile(file) 1098 | await creatFile(file2) 1099 | const obj = await getFile(file) 1100 | expect(obj).toEqual({ 1101 | note: '// 我就是个注释', 1102 | rowSize: 4, 1103 | size: 63, 1104 | imports: [rootPath + '/temp/aa.vue'] 1105 | }) 1106 | done() 1107 | } 1108 | get() 1109 | } catch (error) { 1110 | done(error) 1111 | } 1112 | }) 1113 | 1114 | test('getImport--获取每个文件依赖的方法', (done) => { 1115 | const str = `` 1118 | try { 1119 | async function get() { 1120 | const sarr = str.split(/[\n]/g) 1121 | const arrs = await getImport(sarr, rootPath + '/temp/bb.vue') 1122 | expect(arrs).toMatchObject([rootPath + '/unuse/components/user-rulerts.vue']) 1123 | done() 1124 | } 1125 | get() 1126 | } catch (error) { 1127 | done(error) 1128 | } 1129 | }) 1130 | 1131 | test('getFileNodes--生成所有文件的node信息', (done) => { 1132 | try { 1133 | async function get() { 1134 | const arrs = await getFileNodes(rootPath + '/unuse/components') 1135 | setSize(arrs) 1136 | setSize(deepNodes) 1137 | // console.log(JSON.stringify(deepNodes), 'arrs') 1138 | expect(arrs).toMatchObject(deepNodes) 1139 | 1140 | done() 1141 | } 1142 | get() 1143 | } catch (error) { 1144 | logger.error(error) 1145 | done(error) 1146 | } 1147 | }) 1148 | 1149 | test('getImport--获取每个文件依赖的方法', (done) => { 1150 | try { 1151 | async function get() { 1152 | const notes = await getFileNodes(rootPath + '/unuse/components') 1153 | setSize(notes) 1154 | const arrs = getNote(notes) 1155 | const final = [ 1156 | '├── test\n', 1157 | '│ └── deep\n', 1158 | '│ │ └── user.vue //2工程\n', 1159 | '├── test2\n', 1160 | '│ └── HelloWorld.vue //2工程\n', 1161 | '└── user-rulerts.vue \n' 1162 | ] 1163 | // console.log(JSON.stringify(arrs), 'arrs') 1164 | expect(arrs).toMatchObject(final) 1165 | done() 1166 | } 1167 | get() 1168 | } catch (error) { 1169 | logger.error(error) 1170 | done(error) 1171 | } 1172 | }) 1173 | }) 1174 | 1175 | path:/test2/temp/my/wite-file2.vue 1176 | // 我就是个注释 1177 | 1180 | path:/unuse/test/user-rulerts.vue 1181 | 1182 | 1284 | //2工程 1285 | 1286 | path:/unuse/main.js 1287 | //2工程 1288 | import { createApp } from 'vue' 1289 | // import '../lib/style.css' 1290 | import SketchRule from './components/test2/HelloWorld.vue' 1291 | // import moduleName from '../api/aa.js'; 1292 | const app = createApp(App) 1293 | // app.use(SketchRule); 1294 | import './mixins.js' 1295 | // const MyComponent = app.component('SketchRule') 1296 | // console.log(MyComponent, 'MyComponentMyComponent') 1297 | app.mount('#app') 1298 | 1299 | path:/jest.config.ts 1300 | export default { 1301 | // 指定 Jest 环境 1302 | testEnvironment: 'node', 1303 | // 指定处理 TypeScript 的转换器 1304 | transform: { 1305 | '^.+\\.ts$': 'ts-jest' 1306 | // 'ts-jest': { 1307 | // useESM: true, 1308 | // }, 1309 | }, 1310 | // 设置模块文件的扩展名 1311 | moduleFileExtensions: ['ts', 'js', 'json', 'node'], 1312 | 1313 | // 设置需要忽略的文件或目录 1314 | testPathIgnorePatterns: ['/node_modules/'], 1315 | 1316 | // 如果使用 ESM,则设置此选项 1317 | extensionsToTreatAsEsm: ['.ts'], 1318 | globalSetup: './test/config/jest-global-setup.ts', // 全局 1319 | setupFilesAfterEnv: ['./test/config/jest.setup.ts'], 1320 | clearMocks: true, 1321 | // 配置 Jest 如何解析模块,特别是对于 ESM 1322 | moduleNameMapper: { 1323 | '^(\\.{1,2}/.*)\\.js$': '$1' 1324 | } 1325 | } 1326 | 1327 | --------------------------------------------------------------------------------