├── 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 |
7 |
14 |
15 |
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 |
5 |
32 |
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)
6 | []( https://github.com/kakajun/auto-generate-md/actions/workflows/test.yml)
7 | []( 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 | 
44 | ### 案例
45 |
46 | 
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 |
488 |
515 |
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 |
711 |
718 |
719 |
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 |
--------------------------------------------------------------------------------