├── .cursor └── rules │ ├── structure-web.mdc │ └── structure.mdc ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── release-demo-pages.yml │ ├── release-npm.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── README.md ├── README_jp.md ├── README_zh.md ├── build.sh ├── lerna.json ├── materials ├── export.json ├── files.json ├── import-files.json ├── import-package.json ├── import-unknown.json └── names.json ├── nx.json ├── package.json ├── packages ├── core │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .gitignore │ ├── .releaserc.js │ ├── LICENSE │ ├── logger.ts │ ├── package.json │ ├── rollup.config.ts │ ├── src │ │ ├── config.ts │ │ ├── html-parser.ts │ │ ├── index.ts │ │ ├── inject-export-quote-num.ts │ │ ├── script-parser.ts │ │ ├── style-parser.ts │ │ ├── test.js │ │ └── utils.ts │ ├── test │ │ └── test.less │ ├── tsconfig.json │ └── types │ │ └── index.d.ts ├── server │ ├── .npmrc │ ├── .releaserc.js │ ├── CHANGELOG.md │ ├── README.md │ ├── README_EN.md │ ├── bin │ │ └── service.js │ ├── config.js │ ├── libs │ │ └── web-dist │ │ │ ├── assets │ │ │ ├── iconfont-4c295c33.ttf │ │ │ ├── iconfont-a30d846e.woff │ │ │ ├── iconfont-b49f17f1.woff2 │ │ │ ├── index-02d56794.css │ │ │ └── index-32fc1be9.js │ │ │ ├── favicon.png │ │ │ └── index.html │ ├── package.json │ ├── plugins │ │ └── analy-plugin-names.js │ ├── public │ │ └── data │ │ │ └── test.json │ └── src │ │ └── index.js └── web │ ├── .env │ ├── .env.demo │ ├── .env.development │ ├── .eslintrc.js │ ├── .gitignore │ ├── .prettierrc.js │ ├── .vscode │ └── extensions.json │ ├── README.md │ ├── global.d.ts │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── public │ └── favicon.png │ ├── src │ ├── App.vue │ ├── api │ │ └── remote-data.ts │ ├── components │ │ ├── CodePreview │ │ │ ├── CodePreview.vue │ │ │ └── use-code-preview.ts │ │ ├── Dialog.vue │ │ ├── Drawer.vue │ │ ├── InfoDrawer.vue │ │ ├── ProjectManage.vue │ │ ├── Select.vue │ │ ├── SplitLine.vue │ │ ├── Tree │ │ │ ├── Tree.d.ts │ │ │ └── Tree.vue │ │ ├── icon-btn.vue │ │ ├── use.ts │ │ └── z-index.ts │ ├── css │ │ ├── iconfont │ │ │ ├── iconfont.css │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ └── iconfont.woff2 │ │ └── index.less │ ├── env.d.ts │ ├── language.ts │ ├── main.ts │ ├── router.ts │ ├── types │ │ ├── chart.ts │ │ └── index.ts │ ├── utils │ │ ├── path2tree.ts │ │ └── uuid.ts │ └── views │ │ ├── chart │ │ ├── Aside.vue │ │ ├── Echart.vue │ │ ├── History.vue │ │ ├── echart.ts │ │ ├── event.ts │ │ └── index.vue │ │ ├── packages │ │ ├── echart.ts │ │ └── index.vue │ │ ├── unknowns │ │ ├── echart.ts │ │ └── index.vue │ │ └── words │ │ └── index.vue │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.cursor/rules/structure-web.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: false 5 | --- 6 | # JS-Analyzer Web 7 | 8 | ## 项目概述 9 | 10 | JS-Analyzer 是一个专业的 JavaScript 代码分析工具,用于可视化展示和分析 JavaScript 项目的依赖关系、导入导出情况以及代码结构。该工具能帮助开发人员更好地理解复杂 JavaScript 项目的架构,优化代码结构,提高代码质量。 11 | 12 | ## 功能介绍 13 | 14 | ### 1. 代码关系图(Chart) 15 | - **文件依赖关系可视化**:直观展示项目中文件之间的导入导出关系 16 | - **文件夹结构视图**:展示项目的文件夹结构和组织方式 17 | - **交互式节点**:点击节点查看详细信息,双击切换视图 18 | - **文件详情展示**:展示文件的基本信息、被引用次数及导出变量使用情况 19 | 20 | ### 2. 包管理(Packages) 21 | - **包依赖关系分析**:分析并展示项目中使用的第三方包 22 | - **包使用频率统计**:统计每个包的引用次数和使用情况 23 | - **包引用详情**:查看包在项目中的具体使用位置和方式 24 | 25 | ### 3. 热词分析(Words) 26 | - **代码热词统计**:统计并展示项目中频繁使用的关键词 27 | - **词云可视化**:以词云形式展示代码中的热词分布 28 | - **代码规范指导**:通过热词分析辅助代码规范和命名规范的建立 29 | 30 | ### 4. 隐式引用分析(Unknowns) 31 | - **隐式依赖检测**:识别项目中的隐式依赖和潜在问题 32 | - **未使用引用分析**:发现项目中未使用的导入,帮助清理冗余代码 33 | 34 | ## 核心技术 35 | 36 | ### 前端技术栈 37 | - **框架**:Vue 3 + TypeScript 38 | - **构建工具**:Vite 39 | - **UI组件**:自定义组件 + Vue JSON Pretty 40 | - **样式**:Less + TailwindCSS 41 | - **可视化**:ECharts(图表可视化)+ WordCloud(词云展示) 42 | - **状态管理**:Vue Composition API 43 | - **路由**:Vue Router 44 | - **代码高亮**:Highlight.js 45 | 46 | ### 后端/核心技术 47 | - **代码分析引擎**:@js-analyzer/core 48 | - **支持多语言**:内置多语言切换功能 49 | - **主题切换**:支持明暗主题切换 50 | 51 | ## API接口说明 52 | 53 | ### 1. 文件数据接口 54 | 55 | #### 获取所有文件列表 56 | - **请求路径**:`/data/files.json` 57 | - **方法**:GET 58 | - **响应格式**:`string[]` - 项目所有文件路径的字符串数组 59 | 60 | #### 获取导入文件信息 61 | - **请求路径**:`/data/import-files.json` 62 | - **方法**:GET 63 | - **响应格式**:`ImportDeps` 类型 64 | ```typescript 65 | { 66 | [path: string]: { 67 | num: number, // 引用次数 68 | using: UsingItem[] // 使用该文件的详细信息数组 69 | } 70 | } 71 | 72 | // UsingItem 类型定义 73 | interface UsingItem { 74 | source: string, // 导入源 75 | vars: string, // 导入的变量名 76 | fullPath?: string, // 完整文件路径 77 | loc: SourceLocation // 代码位置信息 78 | } 79 | ``` 80 | 81 | #### 获取导出信息 82 | - **请求路径**:`/data/export.json` 83 | - **方法**:GET 84 | - **响应格式**:`ExportDeps` 类型 85 | ```typescript 86 | { 87 | [path: string]: { 88 | [vars: string]: { 89 | num: number, // 变量被使用次数 90 | using: string[] // 使用该变量的文件路径列表 91 | } 92 | } 93 | } 94 | ``` 95 | 96 | #### 获取包导入信息 97 | - **请求路径**:`/data/import-package.json` 98 | - **方法**:GET 99 | - **响应格式**:`ImportDeps` 类型 - 与导入文件信息格式相同,但记录的是第三方包的引用信息 100 | 101 | #### 获取未知引用信息 102 | - **请求路径**:`/data/import-unknown.json` 103 | - **方法**:GET 104 | - **响应格式**:`ImportDeps` 类型 - 与导入文件信息格式相同,但记录的是无法解析的引用 105 | 106 | #### 获取名称列表 107 | - **请求路径**:`/data/names.json` 108 | - **方法**:GET 109 | - **响应格式**:项目中的标识符名称列表 110 | 111 | ### 2. 文件操作接口 112 | 113 | #### 打开文件 114 | - **请求路径**:`/launch/?file=文件路径` 115 | - **方法**:GET 116 | - **响应格式**:JSON对象,包含文件打开状态信息 117 | 118 | #### 获取文件内容 119 | - **请求路径**:`/code/?file=文件路径` 120 | - **方法**:GET 121 | - **响应格式**:文件的原始内容(文本格式) 122 | 123 | ### 3. 配置接口 124 | 125 | #### 获取配置 126 | - **请求路径**:`/config` 127 | - **方法**:GET 128 | - **响应格式**:包含以下字段的配置对象 129 | ```typescript 130 | { 131 | root: string, // 项目根目录 132 | ignore?: (string | RegExp)[], // 忽略的文件/目录 133 | extensions?: string[], // 支持的文件扩展名 134 | alias?: Record, // 路径别名 135 | path?: string, // 自定义路径 136 | outputPath?: string, // 输出路径 137 | plugins?: Plugin[], // 自定义插件 138 | ide?: string // 集成开发环境 139 | } 140 | ``` 141 | 142 | #### 更新配置 143 | - **请求路径**:`/config` 144 | - **方法**:PUT 145 | - **请求体**:与获取配置接口格式相同的配置对象 146 | - **响应格式**:更新后的配置对象 147 | 148 | ## 使用说明 149 | 150 | 1. 启动开发服务器:`npm run dev` 151 | 2. 构建生产版本:`npm run build` 152 | 3. 构建演示版本:`npm run build:demo` 153 | 4. 预览构建结果:`npm run serve` 154 | 155 | ## 系统特色 156 | 157 | 1. **多视图展示**:提供多种视角分析代码结构和依赖关系 158 | 2. **交互式体验**:通过点击、拖拽等交互方式直观操作 159 | 3. **主题定制**:支持明暗两种主题,适应不同使用场景 160 | 4. **多语言支持**:内置多语言切换功能,适应国际化需求 161 | 5. **项目配置管理**:提供配置界面,灵活管理分析项目 162 | 163 | JS-Analyzer 是帮助开发者理解复杂 JavaScript 项目结构,优化代码质量和依赖关系的利器,为大型前端项目的维护和重构提供了强有力的支持。 164 | -------------------------------------------------------------------------------- /.cursor/rules/structure.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: false 5 | --- 6 | # JS-Analyzer 项目结构说明文档 7 | 8 | ## 项目概述 9 | 10 | JS-Analyzer 是一个交互式可视化的前端依赖分析工具,适用于各种前端项目,如 Vue、React、Svelte、Angular、Node 等。该工具可以分析项目中的文件依赖关系,帮助开发者清理项目中的无用文件和导出变量,识别隐式引用,并提供可视化的依赖关系图表。 11 | 12 | 主要特点: 13 | - 交互式、集成的可视化依赖分析系统 14 | - 支持动态切换入口文件 15 | - 支持依赖倒置(Dependency Inversion) 16 | - 显示文件被引用的次数及引用地址 17 | - 显示文件导出变量的引用信息 18 | - 适用于 ES6、CommonJS 19 | - 支持的文件类型:JS、TS、JSX、TSX、Vue、Sass、Less、Css、html 20 | - 支持包依赖分析 21 | - 分析未导入的文件和 npm 包 22 | - 本地存储,安全性高,不涉及网络和上传 23 | 24 | ## 项目结构 25 | 26 | 该项目使用 lerna 和 pnpm 进行多包管理,主要由以下几个包组成: 27 | 28 | ``` 29 | js-analyzer/ 30 | ├── packages/ # 项目包目录 31 | │ ├── core/ # 核心分析工具包 32 | │ ├── server/ # 服务端包 33 | │ ├── web/ # 前端界面包 34 | ├── materials/ # 项目资源文件 35 | ├── .github/ # GitHub 配置文件 36 | └── ... # 其他配置文件 37 | ``` 38 | 39 | ### 核心包 (@js-analyzer/core) 40 | 41 | 该包是 JS-Analyzer 的核心工具包,负责解析和分析 JavaScript、TypeScript、CSS 等文件的依赖关系。 42 | 43 | ``` 44 | packages/core/ 45 | ├── src/ # 源代码目录 46 | │ ├── index.ts # 入口文件 47 | │ ├── script-parser.ts # 脚本解析器 48 | │ ├── style-parser.ts # 样式解析器 49 | │ ├── html-parser.ts # HTML 解析器 50 | │ ├── utils.ts # 工具函数 51 | │ ├── config.ts # 配置相关 52 | │ └── ... # 其他源文件 53 | ├── types/ # 类型定义 54 | ├── dist/ # 编译输出目录 55 | ├── test/ # 测试目录 56 | ├── package.json # 包配置信息 57 | └── ... # 其他配置文件 58 | ``` 59 | 60 | 核心依赖: 61 | - @babel/core - Babel 核心库,用于解析 JavaScript/TypeScript 62 | - @babel/parser - 代码解析器 63 | - @babel/traverse - AST 遍历工具 64 | - @vue/compiler-sfc - Vue 单文件组件解析器 65 | - postcss 相关库 - 用于解析 CSS/SCSS/LESS 等样式文件 66 | 67 | ### 服务端包 (@js-analyzer/server) 68 | 69 | 该包提供了 JS-Analyzer 的服务端功能,负责启动服务器,处理 API 请求,并与核心分析工具进行交互。 70 | 71 | ``` 72 | packages/server/ 73 | ├── src/ # 源代码目录 74 | │ └── index.js # 入口文件 75 | ├── bin/ # 可执行文件目录 76 | ├── libs/ # 库文件目录 77 | ├── plugins/ # 插件目录 78 | ├── public/ # 静态资源目录 79 | ├── package.json # 包配置信息 80 | └── ... # 其他配置文件 81 | ``` 82 | 83 | 该包可以全局安装或作为项目依赖安装,提供了 `js-analyzer` 命令行工具。 84 | 85 | ### 前端包 (@js-analyzer/web) 86 | 87 | 该包包含了 JS-Analyzer 的 Web 界面,提供了可视化的依赖分析图表和交互功能。 88 | 89 | ``` 90 | packages/web/ 91 | ├── src/ # 源代码目录 92 | │ ├── main.ts # 入口文件 93 | │ ├── App.vue # 主组件 94 | │ ├── router.ts # 路由配置 95 | │ ├── components/ # 组件目录 96 | │ ├── views/ # 视图目录 97 | │ ├── api/ # API 接口目录 98 | │ ├── utils/ # 工具函数目录 99 | │ ├── assets/ # 资源文件目录 100 | │ └── css/ # 样式目录 101 | ├── public/ # 静态资源目录 102 | ├── dist/ # 编译输出目录 103 | ├── package.json # 包配置信息 104 | └── ... # 其他配置文件 105 | ``` 106 | 107 | ## 使用方法 108 | 109 | ### 全局安装 110 | 111 | ```shell 112 | npm install @js-analyzer/server -g 113 | ``` 114 | 115 | 使用: 116 | 117 | ```shell 118 | cd /your/project 119 | js-analyzer --root ./ 120 | ``` 121 | 122 | ### 本地安装 123 | 124 | ```shell 125 | npm install @js-analyzer/server -D 126 | ``` 127 | 128 | 在 package.json 中添加脚本: 129 | 130 | ```json 131 | "scripts": { 132 | "js-analyzer": "js-analyzer --root ./" 133 | } 134 | ``` 135 | 136 | 运行: 137 | 138 | ```shell 139 | npm run js-analyzer 140 | ``` 141 | 142 | 默认访问地址:http://localhost:8088/ 143 | 144 | ## 配置文件 145 | 146 | 可以通过配置文件自定义 JS-Analyzer 的行为: 147 | 148 | ```js 149 | module.exports = { 150 | // 根目录 151 | root: "./", 152 | // 不需要分析的目录 153 | ignore: ["**/node_modules/**", "**/dist/**"], 154 | // 解析无扩展名文件时的优先顺序 155 | extensions: [".js", ".ts", ".tsx", ".vue", ".json", ".jsx"], 156 | // 项目别名的路径映射 157 | alias: { 158 | "@@/": "/", 159 | "~~/": "/", 160 | "@/": "/src/", 161 | "~/": "/src/", 162 | }, 163 | // 服务器和端口相关信息 164 | server: { 165 | port: 8088, 166 | host: "localhost", 167 | openBrowser: true, // 启动后自动在浏览器中打开 168 | }, 169 | // 自定义插件 170 | plugins: [] 171 | }; 172 | ``` 173 | 174 | ## 插件开发 175 | 176 | JS-Analyzer 支持插件开发,用户可以在分析过程中收集自定义信息。 177 | 178 | 插件示例: 179 | 180 | ```js 181 | const myCustomPlugin = { 182 | name: "MyCustomPlugin", 183 | // 输出信息 184 | output: { 185 | data: [], 186 | file: "test.json", 187 | }, 188 | // 解析脚本时运行 189 | ScriptParser({ file, content }) { 190 | const self = this; 191 | return { 192 | VariableDeclarator(tPath) { 193 | tPath.node.id && self.output.data.push(tPath.node.id.name); 194 | }, 195 | }; 196 | }, 197 | // 解析脚本后运行 198 | AfterScriptParser() {}, 199 | }; 200 | 201 | module.exports = { 202 | plugins: [myCustomPlugin], 203 | }; 204 | ``` 205 | 206 | 插件生成的数据默认可通过 `http://localhost:8087/data/test.json` 访问。 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **js-analyzer** 27 | please attach the startup configuration file of js-analyzer 28 | -------------------------------------------------------------------------------- /.github/workflows/release-demo-pages.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a Jekyll site to GitHub Pages 2 | name: Deploy Jekyll with GitHub Pages dependencies preinstalled 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Build job 26 | build: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: 签出代码 30 | uses: actions/checkout@v3 31 | - name: 安装 nodejs 32 | uses: actions/setup-node@v2.5.2 33 | with: 34 | node-version: "16.15.0" 35 | - name: 安装依赖 36 | run: | 37 | npm install -g pnpm@latest-8 38 | pnpm install --no-frozen-lockfile 39 | working-directory: packages/web 40 | - name: web打包 41 | run: pnpm run build:demo 42 | working-directory: packages/web 43 | - name: 生成 demo 文件夹 44 | run: | 45 | mkdir web 46 | cp -r ./packages/web/dist/* ./web/ 47 | cp -r ./materials ./web/data 48 | ls web 49 | 50 | - name: Setup Pages 51 | uses: actions/configure-pages@v3 52 | - name: Build with Jekyll 53 | uses: actions/jekyll-build-pages@v1 54 | with: 55 | source: ./web 56 | destination: ./_site 57 | - name: Upload artifact 58 | uses: actions/upload-pages-artifact@v1 59 | 60 | # Deployment job 61 | deploy: 62 | environment: 63 | name: github-pages 64 | url: ${{ steps.deployment.outputs.page_url }} 65 | runs-on: ubuntu-latest 66 | needs: build 67 | steps: 68 | - name: Deploy to GitHub Pages 69 | id: deployment 70 | uses: actions/deploy-pages@v2 71 | -------------------------------------------------------------------------------- /.github/workflows/release-npm.yml: -------------------------------------------------------------------------------- 1 | # 工作流名称 2 | name: Relsase and Publish 3 | 4 | on: 5 | # 指明要运行的分支,跟上面配置保持一致 6 | push: 7 | branches: [release] 8 | 9 | permissions: 10 | contents: write 11 | issues: write 12 | pull-requests: write 13 | packages: write 14 | id-token: write 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: 签出代码 22 | uses: actions/checkout@v3 23 | 24 | - name: 安装 nodejs 25 | uses: actions/setup-node@v2.5.2 26 | with: 27 | node-version: "20.8.1" 28 | 29 | - name: 打包构建 30 | run: | 31 | npm install -g pnpm@latest-8 32 | chmod +x ./build.sh 33 | ./build.sh 34 | 35 | # 执行 semantic-release 发布包 36 | - name: 发布 server npm 包 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.PUBLISH_GH_TOKEN }} 39 | NPM_TOKEN: ${{ secrets.PUBLISH_NPM_TOKEN }} 40 | run: npx semantic-release 41 | working-directory: packages/server 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | permissions: 8 | contents: write 9 | issues: write 10 | pull-requests: write 11 | packages: write 12 | id-token: write 13 | 14 | jobs: 15 | release: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: 签出代码 20 | uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | persist-credentials: false 24 | 25 | - name: 安装 nodejs 26 | uses: actions/setup-node@v2.5.2 27 | with: 28 | node-version: "20.8.1" 29 | 30 | - name: 构建 web app 31 | working-directory: ./packages/web 32 | run: | 33 | npm install -g pnpm@latest-8 34 | pnpm install --no-frozen-lockfile 35 | pnpm run build 36 | cp -r dist/* ../server/libs/web-dist/ 37 | 38 | # - name: 发布 core npm 包 39 | # env: 40 | # GITHUB_TOKEN: ${{ secrets.PUBLISH_GH_TOKEN }} 41 | # NPM_TOKEN: ${{ secrets.PUBLISH_NPM_TOKEN }} 42 | # run: | 43 | # npm cache clean --force 44 | # npx semantic-release 45 | # working-directory: packages/core 46 | 47 | - name: 发布 server npm 包 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.PUBLISH_GH_TOKEN }} 50 | NPM_TOKEN: ${{ secrets.PUBLISH_NPM_TOKEN }} 51 | run: | 52 | npm cache clean --force 53 | npx semantic-release 54 | working-directory: packages/server 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | **/server/public/data/** 7 | !**/server/public/data/test.json 8 | walk-file 9 | vscode-plugin -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | workspaces = true 2 | workspaces-update = false -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll": "never", 4 | "source.fixAll.eslint": "never" 5 | } 6 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [2.2.0](https://github.com/chennlang/js-analyzer/compare/v2.1.0...v2.2.0) (2024-06-13) 2 | 3 | ### Bug Fixes 4 | 5 | - 修复别名无法解析 ([64654e0](https://github.com/chennlang/js-analyzer/commit/64654e0f753ad7cf6abfc5bfebc2a4bb8d56c575)) 6 | 7 | ### Features 8 | 9 | - 视图切换组件优化 ([e8ef179](https://github.com/chennlang/js-analyzer/commit/e8ef179d382b02c47cec31a8f94c46c51608a863)) 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

🧬Js Analyzer

3 |

An interactive, visual front-end dependency analysis tool

4 |

Applicable for any front-end project such as Vue, React, Svelte, Angular, Node

5 |

6 | English 7 | | 8 | 简体中文 9 | | 10 | 日本語 11 |

12 |
13 | 14 | https://github.com/chennlang/js-analyzer/assets/41711206/63797bfd-440c-401e-a0d8-833a9c8caef0 15 | 16 | ## Features 17 | 18 | - Interactive, integrated `visual` dependency analysis system 19 | - Supports dynamic switching of entry files 20 | - Supports `Dependency Inversion` 21 | - Displays the number of times a file is referenced, as well as the reference address 22 | - Displays information on references to the exported variables of a file 23 | - Suitable for ES6, CommonJS 24 | - Supported file types: JS, TS, JSX, TSX, Vue, Sass, Less, Css, html 25 | - Supports package dependency analysis 26 | - Analyzes unimported files and npm packages 27 | - Local storage, `Very secure`, does not involve networking and uploading 28 | 29 | ## 🤝 Contributors 30 | 31 | Thank you to the following contributors for supporting this project! 🎉 32 | 33 | - [@mannymu](https://github.com/mannymu) 34 | - [@rxx-qingyi](https://github.com/rxx-qingyi) 35 | 36 | _✨ Thank you for your contributions!_ 37 | 38 | ## Global Installation 39 | 40 | ### 1. Installation 41 | 42 | ```shell 43 | npm install @js-analyzer/server -g 44 | # yarn add @js-analyzer/server -g 45 | # pnpm install @js-analyzer/server -g 46 | ``` 47 | 48 | ### 2. Usage 49 | 50 | Enter the console into any project root directory and execute `js-analyzer --root ./` 51 | 52 | ```shell 53 | cd /xxx/project 54 | 55 | js-analyzer --root ./ 56 | ``` 57 | 58 | ## Local Installation 59 | 60 | ### 1. Installation 61 | 62 | ```shell 63 | npm install @js-analyzer/server -D 64 | # yarn add @js-analyzer/server -D 65 | # pnpm install @js-analyzer/server -D 66 | ``` 67 | 68 | ### 2. Usage 69 | 70 | #### 1. Add the "js-analyzer" command in scripts 71 | 72 | ```json 73 | "scripts": { 74 | "js-analyzer": "js-analyzer --root ./" 75 | }, 76 | ``` 77 | 78 | #### 2. Enter "npm run js-analyzer" in the console and visit http://localhost:8088/ to see it. 79 | 80 | ```shell 81 | npm run js-analyzer 82 | # Service started:http://localhost:8088/ 83 | ``` 84 | 85 | ## Configuration File 86 | 87 | You can quickly start an analysis service using the above commands. However, each project's overall architecture is different, so if you want "js-analyzer" to be better and more accurate, you need to configure some necessary information. 88 | 89 | To specify the configuration file, simply modify the start command as follows 90 | 91 | ```json 92 | "scripts": { 93 | "js-analyzer": "js-analyzer --config ./js-analyzer.js" 94 | }, 95 | ``` 96 | 97 | js-analyzer.js 98 | 99 | ```js 100 | module.exports = { 101 | // Root directory 102 | root: "./", 103 | // Directories that do not need to be analyzed 104 | ignore: ["**/node_modules/**", "**/dist/**"], 105 | // Order of preference when parsing files without extensions 106 | extensions: [".js", ".ts", ".tsx", ".vue", ".json", ".jsx"], 107 | // Path mapping of the project's alias 108 | alias: { 109 | "@@/": "/", 110 | "~~/": "/", 111 | "@/": "/src/", 112 | "~/": "/src/", 113 | }, 114 | // Server and port related information 115 | server: { 116 | port: 8088, 117 | host: "localhost", 118 | openBrowser: true, // Automatically open in browser after startup 119 | }, 120 | }; 121 | ``` 122 | 123 | ## Updates 124 | 125 | - Support for VUE SETUP type 126 | - Customizable plugins, generate the data you want 127 | - Support for built-in project hot-word plugins 128 | - File dependency view: support for the dependency relationship view within a single folder 129 | - Analysis of Sass, Less, Css, and other style files (New, Supported) 130 | - Supports project variable hot-word map 131 | 132 | ## TODO 133 | 134 | - Shared module for project component document generation 135 | - Cycle dependency analysis 136 | - Module stability indicator analysis 137 | 138 | ## Plugin Development 139 | 140 | The principle of this tool is to parse AST collection of related dependency information, theoretically, users can also collect any information they want in this process. Therefore, a plugin approach is provided, exposing the lifecycle at various stages, allowing users to execute any logic in lifecycle functions. 141 | 142 | ### Example: A plugin that collects variable names used inside a project 143 | 144 | ```js 145 | const myCustomPlugin = { 146 | name: "MyCustomPlugin", 147 | // Output information 148 | output: { 149 | data: [], 150 | file: "test.json", 151 | }, 152 | // Run when parsing script 153 | ScriptParser({ file, content }) { 154 | const self = this; 155 | return { 156 | VariableDeclarator(tPath) { 157 | tPath.node.id && self.output.data.push(tPath.node.id.name); 158 | }, 159 | }; 160 | }, 161 | // Run after parsing script 162 | AfterScriptParser() {}, 163 | }; 164 | 165 | module.exports = { 166 | plugins: [myCustomPlugin], 167 | }; 168 | ``` 169 | 170 | Custom data generation, default access address 'http://localhost:8087/data/test.json' 171 | 172 | ## Guides 173 | 174 | ### How to clean "Garbage files" in the project 175 | 176 | What are "Garbage files"? These are files that are not referenced. 177 | 178 | - Method one: In the "Relationship Map", in the directory tree on the left, there is a "reference count" at the end of each file, the number `0` indicates no reference. 179 | - Method two: In the "Relationship Map", under the "Folder Relationship Map" view, you can see the "reference count" for each file, the number `0` indicates no reference. 180 | 181 | ![](https://cdn.jsdelivr.net/gh/chennlang/doc-images//picGo/20240508175023.png) 182 | 183 | ### How to clean "unused exports" in the project 184 | 185 | In the development project, there will be many exported but unused variables or methods in the `const` directory or `api` directory: 186 | 187 | ```ts 188 | // const.ts | api.ts | utils.ts 189 | 190 | export const STATUS = "status"; // Not used 191 | 192 | export const TEXT = "text"; // Not used 193 | 194 | export const api_fetch_data = () => {}; // Not used 195 | 196 | export const api_fetch_result = () => {}; // Not used 197 | ``` 198 | 199 | We can view the export information in "File Details", find the "unused exports", and delete it. So how do we open "File Details"? 200 | 201 | - Method one: Any node in the chart on the right side of the "Relationship Map" page corresponds to the file on the left. After clicking the node, the "File Details" will be displayed. 202 | - Method two: If you have already selected the target file in the left directory, click the file details button in the upper right corner of the chart. 203 | 204 | ### Discover "Implicit References" in the project 205 | 206 | What is an "Implicit Reference"? This refers to those third-party libraries that have not been registered in `package.json` or used in the project. 207 | 208 | > Why this scenario would occur, for we installer an npm package `A` in the project, and `A` depends on libraries `a`,`b`. Then, in `node_modules` there would be three libraries `A`,`a`,`b`. Of course, we can use `a` or `b` directly in the project, but this is very risky! 209 | 210 | We can find all such references in the "Implicit Reference" page, view the target file, and then register the library in use in `package.json`. 211 | 212 | Open "File Details" 213 | ![](https://cdn.jsdelivr.net/gh/chennlang/doc-images//picGo/20240508175202.png) 214 | 215 | "File Details" 216 | ![](https://cdn.jsdelivr.net/gh/chennlang/doc-images//picGo/20240508175411.png) 217 | 218 | ### View the dependency paths of files 219 | 220 | Scenario: Sometimes we need to find the dependency context of a file and see which file it is ultimately referenced by. This is to determine the scope of impact of modifying the file. 221 | 222 | Use: You can select a single file and switch to the "Upstream Dependency Relationship Map" to view. 223 | 224 | ![](https://cdn.jsdelivr.net/gh/chennlang/doc-images//picGo/20240508175559.png) 225 | 226 | ## Invitation 227 | 228 | Holding the will to clean code, we hope that more people can join this project. The goal is to build an assistant tool that can help all front-end programmers to refactor/clean code. 229 | -------------------------------------------------------------------------------- /README_jp.md: -------------------------------------------------------------------------------- 1 |
2 |

🧬Js Analyzer

3 |

フロントエンド依存関係分析のための視覚化、インタラクティブなツール

4 |

Vue、React、Svelte、Angular、Nodeなど、あらゆるフロントエンドプロジェクトに適用可能

5 |

6 | English 7 | | 8 | 简体中文 9 | | 10 | 日本語 11 |

12 |
13 | 14 | https://github.com/chennlang/js-analyzer/assets/41711206/63797bfd-440c-401e-a0d8-833a9c8caef0 15 | 16 | ## 機能 17 | 18 | - 一体型でインタラクティブな`可視化`依存分析システム 19 | - エントリーファイルの動的な切り替えをサポート 20 | - `依存反転`をサポート 21 | - ファイルが参照された回数と参照元のアドレスを表示するサポート 22 | - ファイルのエクスポート変数が参照された情報を表示するサポート 23 | - ES6、CommonJs に適用 24 | - サポートされているファイルタイプ:JS、TS、JSX、TSX、Vue、Sass、Less、Css、html 25 | - package 依存分析サポート 26 | - 未参照ファイル、npm パッケージの分析サポート 27 | - ローカルストレージは`非常に安全`で、ネットワークに接続したりアップロードする必要はありません 28 | 29 | ## グローバルインストール 30 | 31 | ### 1. インストール 32 | 33 | ```shell 34 | npm install @js-analyzer/server -g 35 | # yarn add @js-analyzer/server -g 36 | # pnpm install @js-analyzer/server -g 37 | ``` 38 | 39 | ```` 40 | 41 | ### 2. 使用法 42 | 43 | コンソールでプロジェクトルートディレクトリに入り、`js-analyzer --root ./`を実行します。 44 | 45 | ```shell 46 | cd /xxx/project 47 | 48 | js-analyzer --root ./ 49 | ``` 50 | 51 | ## ローカルインストール 52 | 53 | ### 1. インストール 54 | 55 | ```shell 56 | npm install @js-analyzer/server -D 57 | # yarn add @js-analyzer/server -D 58 | # pnpm install @js-analyzer/server -D 59 | ``` 60 | 61 | ### 2. 使用法 62 | 63 | #### 1. scripts に js-analyzer コマンドを追加 64 | 65 | ```json 66 | "scripts": { 67 | "js-analyzer": "js-analyzer --root ./" 68 | }, 69 | ``` 70 | 71 | #### 2. コンソールで npm run js-analyzer を入力し、http://localhost:8088/ にアクセスすると確認できます。 72 | 73 | ```shell 74 | npm run js-analyzer 75 | # Service started:http://localhost:8088/ 76 | ``` 77 | 78 | ## 設定ファイル 79 | 80 | 上記コマンドで分析サービスを迅速に起動することができますが、プロジェクトの全体的なアーキテクチャが異なるため、js-analyzer がより正確に分析するためには、いくつかの必要な情報を設定する必要があります。 81 | 82 | 設定ファイルを指定するには、上記の起動コマンドを次のように修正します。 83 | 84 | ```json 85 | "scripts": { 86 | "js-analyzer": "js-analyzer --config ./js-analyzer.js" 87 | }, 88 | ``` 89 | 90 | js-analyzer.js 91 | 92 | ```js 93 | module.exports = { 94 | // ルートディレクトリ 95 | root: "./", 96 | // 分析不要なディレクトリ 97 | ignore: ["**/node_modules/**", "**/dist/**"], 98 | // 拡張子なしのファイルを解析する場合の優先順位 99 | extensions: [".js", ".ts", ".tsx", ".vue", ".json", ".jsx"], 100 | // プロジェクトのエイリアスマッピングパス 101 | alias: { 102 | "@@/": "/", 103 | "~~/": "/", 104 | "@/": "/src/", 105 | "~/": "/src/", 106 | }, 107 | // サーバーとポートに関連する 108 | server: { 109 | port: 8088, 110 | host: "localhost", 111 | openBrowser: true, // 起動後にブラウザを自動的に開く 112 | }, 113 | }; 114 | ``` 115 | 116 | ## アップデート 117 | 118 | - VUE SETUP タイプをサポート 119 | - カスタムプラグインを自由に定義し、希望のデータを生成 120 | - 内蔵されているプロジェクトのホットワードプラグインのサポート 121 | - ファイル依存のビュー:単一のフォルダ内の依存関係ビューのサポート 122 | - Sass、Less、Css などのスタイルファイル分析(新機能、サポート済み) 123 | - プロジェクト変数のホットワード図のサポート 124 | 125 | ## TODO 126 | 127 | - プロジェクトコンポーネント文書の生成共有モジュール 128 | - 循環依存分析 129 | - モジュール安定性指標分析 130 | 131 | ## プラグイン開発 132 | 133 | このツールは AST を解析し、関連依存情報を収集することで原理が成り立っています。理論上、ユーザーはこのプロセスで任意の情報を収集できます。そのため、プラグインを提供し、各フェーズのライフサイクルを公開し、ライフサイクル関数内で任意のロジックを実行できるようにします。 134 | 135 | ### 例:プロジェクト内で使用される変数名を収集するカスタムプラグイン 136 | 137 | ```js 138 | const myCustomPlugin = { 139 | name: "MyCustomPlugin", 140 | // 出力情報 141 | output: { 142 | data: [], 143 | file: "test.json", 144 | }, 145 | // script解析時に実行 146 | ScriptParser({ file, content }) { 147 | const self = this; 148 | return { 149 | VariableDeclarator(tPath) { 150 | tPath.node.id && self.output.data.push(tPath.node.id.name); 151 | }, 152 | }; 153 | }, 154 | // script解析完了後に実行 155 | AfterScriptParser() {}, 156 | }; 157 | 158 | module.exports = { 159 | plugins: [myCustomPlugin], 160 | }; 161 | ``` 162 | 163 | デフォルトで生成されたデータにアクセスするには 'http://localhost:8087/data/test.json' 164 | 165 | ## ガイド 166 | 167 | ### プロジェクトの【ジャンクファイル】をクリアする方法 168 | 169 | 何が【ジャンクファイル】ですか? 参照されていないファイルを指します。 170 | 171 | - 方法 1:【リレーションシップ図】では、左側のディレクトリツリーの各ファイルの後ろに【参照数】があり、その数が `0` の場合は参照されていないことを意味します。 172 | - 方法 2:【リレーションシップ図】 173 | 174 | ### プロジェクトの中の「隠れた参照」を発見 175 | 176 | 「隠れた参照」とは何か?それは`package.json`に登録されていない、またはプロジェクト内で使用されている第三者のライブラリを指します。 177 | 178 | > このような状況がどうして発生するのか、例えば私たちのプロジェクトで npm パッケージ`A`をインストールしたとします。そして、`A`はライブラリ`a`、`b`に依存しています。そうすると`node_modules`には`A`、`a`、`b`の三つのライブラリが存在することになります。確かに私たちはプロジェクト内で直接`a`、`b`を使用することができますが、これはとても危険です! 179 | 180 | 私たちは「隠れた参照」ページでこのような参照をすべて見つけることができ、使用しているライブラリを`package.json`に明示的に登録することができます。 181 | 182 | 【ファイル詳細】を開く 183 | ![](https://cdn.jsdelivr.net/gh/chennlang/doc-images//picGo/20240508175202.png) 184 | 185 | 【ファイル詳細】 186 | ![](https://cdn.jsdelivr.net/gh/chennlang/doc-images//picGo/20240508175411.png) 187 | 188 | ### ファイルの依存パスを確認 189 | 190 | シナリオ:時々、あるファイルの依存関係を探し、最終的にどのファイルによって参照されているかを確認して、ファイルの変更の影響範囲を確認する必要があります。 191 | 192 | 使用方法:単一のファイルを選択した後、【上流依存関係グラフ】に切り替えて確認できます。 193 | 194 | ![](https://cdn.jsdelivr.net/gh/chennlang/doc-images//picGo/20240508175559.png) 195 | 196 | ## 招待 197 | 198 | クリーンコードの精神を受け継ぎ、このプロジェクトに更に多くの人々が参加することを望んでいます。目標は、すべてのフロントエンドプログラマがコードをリファクタリング/整理するのを助ける助けとなるツールを構築することです。 199 | ```` 200 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 |
2 |

🧬Js Analyzer

3 |

一个可视化可交互的前端依赖分析工具

4 |

可用于 Vue React Svelte Angular Node 等任何前端项目

5 |

6 | English 7 | | 8 | 简体中文 9 | | 10 | 日本語 11 |

12 |
13 | 14 | https://github.com/chennlang/js-analyzer/assets/41711206/63797bfd-440c-401e-a0d8-833a9c8caef0 15 | 16 | ## 功能 17 | 18 | - 可交互的一体化`可视化`依赖分析系统 19 | - 支持动态切换入口文件 20 | - 支持`依赖反转` 21 | - 支持显示文件被引用次数,以及引用地址 22 | - 支持显示文件的导出变量被引用信息 23 | - 适用于 ES6、CommonJs 24 | - 支持的文件类型:JS、TS、JSX、TSX、Vue、Sass、Less、Css、html 25 | - 支持 package 依赖分析 26 | - 支持未引用 文件、npm 包分析 27 | - 本地存储 `非常安全`,不涉及联网和上传 28 | 29 | ## 全局安装 30 | 31 | ### 1. 安装 32 | 33 | ```shell 34 | npm install @js-analyzer/server -g 35 | # yarn add @js-analyzer/server -g 36 | # pnpm install @js-analyzer/server -g 37 | ``` 38 | 39 | ### 2. 使用 40 | 41 | 控制台进入到任意项目根目录下,执行 `js-analyzer --root ./` 42 | 43 | ```shell 44 | cd /xxx/project 45 | 46 | js-analyzer --root ./ 47 | ``` 48 | 49 | ## 局部安装 50 | 51 | ### 1. 安装 52 | 53 | ```shell 54 | npm install @js-analyzer/server -D 55 | # yarn add @js-analyzer/server -D 56 | # pnpm install @js-analyzer/server -D 57 | ``` 58 | 59 | ### 2. 使用 60 | 61 | #### 1.在 scripts 中添加 js-analyzer 命令 62 | 63 | ```json 64 | "scripts": { 65 | "js-analyzer": "js-analyzer --root ./" 66 | }, 67 | ``` 68 | 69 | #### 2.在控制台输入 npm run js-analyzer,访问 http://localhost:8088/ 就能看到了。 70 | 71 | ```shell 72 | npm run js-analyzer 73 | # Service started:http://localhost:8088/ 74 | ``` 75 | 76 | ## 配置文件 77 | 78 | 通过上面的命令已经能很快启动一个分析服务了,可是每个项目的整体架构不同,想要 js-analyzer 更好的更准确的分析,还需要配置一些必要信息。 79 | 80 | 指定配置文件只需要将上面的启动命令修改一下 81 | 82 | ```json 83 | "scripts": { 84 | "js-analyzer": "js-analyzer --config ./js-analyzer.js" 85 | }, 86 | ``` 87 | 88 | js-analyzer.js 89 | 90 | ```js 91 | module.exports = { 92 | // 根目录 93 | root: "./", 94 | // 不需要分析的目录 95 | ignore: ["**/node_modules/**", "**/dist/**"], 96 | // 解析没有扩展名的文件时优先查找顺序 97 | extensions: [".js", ".ts", ".tsx", ".vue", ".json", ".jsx"], 98 | // 项目的别名映射路径 99 | alias: { 100 | "@@/": "/", 101 | "~~/": "/", 102 | "@/": "/src/", 103 | "~/": "/src/", 104 | }, 105 | // 启动的服务器和端口相关 106 | server: { 107 | port: 8088, 108 | host: "localhost", 109 | openBrowser: true, // 启动后自动在浏览器打开 110 | }, 111 | }; 112 | ``` 113 | 114 | ## 更新 115 | 116 | - 支持 VUE SETUP 类型 117 | - 可自定义插件,生成你想要的数据 118 | - 内置项目热词插件支持 119 | - 文件依赖视图:支持单个文件夹内依赖关系视图 120 | - Sass、Less、Css 等样式文件分析(New, 已支持) 121 | - 支持项目变量热词图 122 | 123 | ## TODO 124 | 125 | - 项目组件文档生成共享模块 126 | - 循环依赖分析 127 | - 模块稳定性指标分析 128 | 129 | ## 插件开发 130 | 131 | 该工具原理是通过解析 AST 收集了相关依赖信息,理论上用户同样可以在这个过程中收集到自己想要的任何信息。所以提供了插件的方式,暴露出各个阶段的生命周期,允许用户在生命周期函数中执行任何逻辑。 132 | 133 | ### 示例:一个项目内使用到的变量名收集插件 134 | 135 | ```js 136 | const myCustomPlugin = { 137 | name: "MyCustomPlugin", 138 | // 输出信息 139 | output: { 140 | data: [], 141 | file: "test.json", 142 | }, 143 | // 解析 script 时执行 144 | ScriptParser({ file, content }) { 145 | const self = this; 146 | return { 147 | VariableDeclarator(tPath) { 148 | tPath.node.id && self.output.data.push(tPath.node.id.name); 149 | }, 150 | }; 151 | }, 152 | // 解析完 script 时执行 153 | AfterScriptParser() {}, 154 | }; 155 | 156 | module.exports = { 157 | plugins: [myCustomPlugin], 158 | }; 159 | ``` 160 | 161 | 自定义生成数据,默认访问地址 'http://localhost:8087/data/test.json' 162 | 163 | ## 指南 164 | 165 | ### 如何清理项目中的【垃圾文件】 166 | 167 | 什么是【垃圾文件】?指的是未被引用的文件。 168 | 169 | - 方式一:在【关系图】中,左侧的目录树中,每一个文件后面都一个【被引用数】,数量为 `0` 表示未被引用。 170 | - 方式二:在【关系图】中,在【文件夹关系图】视图下,可以看到每一个文件的【被引用数】,数量为 `0` 表示未被引用。 171 | 172 | ![](https://cdn.jsdelivr.net/gh/chennlang/doc-images//picGo/20240508175023.png) 173 | 174 | ### 如何清理项目中的【未使用导出】 175 | 176 | 在开发项目中, `const` 目录或者 `api` 目录下,会有很多导出但并未使用的变量或者方法: 177 | 178 | ```ts 179 | // const.ts | api.ts | utils.ts 180 | 181 | export const STATUS = "status"; // 未使用 182 | 183 | export const TEXT = "text"; // 未使用 184 | 185 | export const api_fetch_data = () => {}; // 未使用 186 | 187 | export const api_fetch_result = () => {}; // 未使用 188 | ``` 189 | 190 | 我们可以在【文件详情】中查看导出信息,找到【未使用导出】并删除它。那么如何打开【文件详情】? 191 | 192 | - 方式一:【关系图】页面右侧的图表中的任意节点都是对应左侧的文件,单击节点后,显示【文件详情】。 193 | - 方式二:已在左侧目录中选中目标文件,点击图表右上角的文件详情按钮。 194 | 195 | ### 发现项目中的【隐式引用】 196 | 197 | 什么是【隐式引用】?指那些并未在 `package.json` 中注册,或者项目中使用的第三方库。 198 | 199 | > 为什么会出现这样的场景,例如我们在项目中安装了一个 npm 包 `A`,而 `A` 依赖库 `a`、`b`,那么在 `node_modules`,在回存在三个库 `A`、`a`、`b`,固然我们在项目中就能直接使用 `a`、`b`,单这样是非常不安全的! 200 | 201 | 我们可以在【隐式引用】页面下找到所有这样的引用,查看目标文件。然后显示的在 `package.json` 注册有使用的库。 202 | 203 | 打开【文件详情】 204 | ![](https://cdn.jsdelivr.net/gh/chennlang/doc-images//picGo/20240508175202.png) 205 | 206 | 【文件详情】 207 | ![](https://cdn.jsdelivr.net/gh/chennlang/doc-images//picGo/20240508175411.png) 208 | 209 | ### 查看文件依赖路径 210 | 211 | 场景:有时候我们需要寻找某个文件的依赖上文,看看最终被哪个文件引用了。以确认修改文件的影响范围。 212 | 213 | 使用:可以选择单个文件后,切换【上游依赖关系图】后查看。 214 | 215 | ![](https://cdn.jsdelivr.net/gh/chennlang/doc-images//picGo/20240508175559.png) 216 | 217 | ## 邀请 218 | 219 | 秉承整洁代码意志,希望更多的人加入到这个项目中,目标是构建一个能帮助所有前端程序员重构/整洁代码的辅助工具。 220 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # ----------------- 清理历史 ----------------- 4 | # rm -rf ./packages/server/libs/core-dist/* 5 | # rm -rf ./packagesserver/libs/web-dist/* 6 | 7 | # # ----------------- 构建 core ----------------- 8 | # echo "开始构建 core" 9 | # cd packages/core 10 | # pnpm install --no-frozen-lockfile 11 | # pnpm run build 12 | # cp -r dist/* ../server/libs/core-dist/ 13 | 14 | # cd ../../ 15 | 16 | # ----------------- 构建 web ----------------- 17 | echo "开始构建 web" 18 | cd packages/web 19 | pnpm install --no-frozen-lockfile 20 | pnpm run build 21 | cp -r dist/* ../server/libs/web-dist/ 22 | 23 | cd ../../ 24 | 25 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "independent", 3 | "npmClient": "pnpm", 4 | "useWorkspaces": true, 5 | "packages": [ 6 | "packages/core", 7 | "packages/server", 8 | "packages/shared", 9 | "packages/web" 10 | ], 11 | "command": { 12 | "publish": { 13 | "ignoreChanges": [".gitignore", "*.md", ".log", "public"], 14 | "message": "chore(release): publish", 15 | "registry": "https://registry.npmjs.org/" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chennlang/js-analyzer/04bcc3c0a0f4c32fe0ba20f054e7c56ca7b4a061/nx.json -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "author": "Alang", 4 | "private": true, 5 | "workspaces": [ 6 | "packages/core", 7 | "packages/server", 8 | "packages/web" 9 | ], 10 | "scripts": { 11 | "dev:core": "lerna run dev --stream --scope @js-analyzer/core", 12 | "dev:server": "lerna run dev --stream --scope @js-analyzer/server", 13 | "dev:web": "lerna run dev --stream --scope @js-analyzer/web" 14 | }, 15 | "devDependencies": { 16 | "lerna": "^4.0.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | dist 3 | types/** -------------------------------------------------------------------------------- /packages/core/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | "@typescript-eslint/no-var-requires": "off" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | -------------------------------------------------------------------------------- /packages/core/.releaserc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: ["main"], // 指定在哪个分支下要执行发布操作 3 | repositoryUrl: "https://github.com/chennlang/js-analyzer.git", 4 | plugins: [ 5 | "@semantic-release/commit-analyzer", // 解析 commit 信息,默认就是 Angular 规范 6 | "@semantic-release/release-notes-generator", 7 | "@semantic-release/npm", // 发布到NPM 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /packages/core/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Alang 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 | -------------------------------------------------------------------------------- /packages/core/logger.ts: -------------------------------------------------------------------------------- 1 | 2 | const { createLogger, format, transports } = require("winston"); 3 | 4 | export default createLogger({ 5 | format: format.combine( 6 | format.colorize(), 7 | format.label({ label: 'Js Analyzer' }), 8 | format.timestamp({ format: "MMM-DD-YYYY HH:mm:ss" }), 9 | format.align(), 10 | format.prettyPrint(), 11 | // format.printf( 12 | // (info: any) => 13 | // `${info.label} ${[info.timestamp]} level-${info.level}, message: ${info.message}` 14 | // ) 15 | ), 16 | transports: [ 17 | new transports.Console(), 18 | ], 19 | }); 20 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-analyzer/core", 3 | "version": "2.6.2", 4 | "main": "dist/js-analyzer-core.cjs.js", 5 | "types": "dist/js-analyzer-core.d.ts", 6 | "author": "chennlang", 7 | "description": "js-analyzer 核心工具包", 8 | "license": "MIT", 9 | "scripts": { 10 | "dev": "rollup --config rollup.config.ts --watch", 11 | "build": "rollup --config rollup.config.ts", 12 | "build:watch": "rollup --config rollup.config.ts --watch", 13 | "lint": "eslint ./src/index.ts", 14 | "c-l": "npm login --registry=https://registry.npmjs.org/", 15 | "c-p": "npm publish --registry=https://registry.npmjs.org/" 16 | }, 17 | "files": [ 18 | "dist" 19 | ], 20 | "bugs": { 21 | "url": "https://github.com/chennlang/js-analyzer/issues" 22 | }, 23 | "homepage": "https://github.com/chennlang/js-analyzer#readme", 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "devDependencies": { 28 | "@rollup/plugin-babel": "^5.3.0", 29 | "@rollup/plugin-commonjs": "^21.0.1", 30 | "@rollup/plugin-json": "^4.1.0", 31 | "@rollup/plugin-node-resolve": "^13.1.3", 32 | "@rollup/plugin-typescript": "^8.3.0", 33 | "@semantic-release/npm": "^12.0.0", 34 | "@types/node": "14.x", 35 | "@typescript-eslint/eslint-plugin": "^5.26.0", 36 | "@typescript-eslint/parser": "^5.26.0", 37 | "eslint": "^8.16.0", 38 | "rollup": "^2.67.2", 39 | "rollup-plugin-cleaner": "^1.0.0", 40 | "rollup-plugin-commonjs": "^10.1.0", 41 | "rollup-plugin-dts": "^4.2.2", 42 | "rollup-plugin-node-resolve": "^5.2.0", 43 | "rollup-plugin-typescript2": "^0.31.2", 44 | "semantic-release": "^23.0.8", 45 | "tslint": "^6.1.3", 46 | "typescript": "^4.7.2", 47 | "typescript-eslint-parser": "^22.0.0" 48 | }, 49 | "dependencies": { 50 | "@babel/core": "^7.22.1", 51 | "@babel/parser": "^7.17.12", 52 | "@babel/plugin-proposal-decorators": "^7.22.3", 53 | "@babel/preset-env": "^7.22.2", 54 | "@babel/traverse": "^7.17.12", 55 | "@babel/types": "^7.17.12", 56 | "@vue/compiler-core": "^3.2.47", 57 | "@vue/compiler-dom": "^3.2.41", 58 | "@vue/compiler-sfc": "^3.2.33", 59 | "del": "^6.1.1", 60 | "fast-glob": "^3.2.11", 61 | "postcss": "^8.4.14", 62 | "postcss-less": "^6.0.0", 63 | "postcss-sass": "^0.5.0", 64 | "postcss-scss": "^4.0.4", 65 | "tslib": "^2.4.0", 66 | "upath": "^2.0.1", 67 | "winston": "^3.5.1" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/core/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import typescript from 'rollup-plugin-typescript2' 3 | import json from '@rollup/plugin-json' 4 | import cleaner from 'rollup-plugin-cleaner' 5 | import dts from 'rollup-plugin-dts' 6 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 7 | import { babel } from '@rollup/plugin-babel'; 8 | export default [{ 9 | input: 'src/index.ts', 10 | output: [ 11 | { 12 | file: 'dist/js-analyzer-core.es.js', 13 | format: 'esm', 14 | }, 15 | { 16 | file: 'dist/js-analyzer-core.cjs.js', 17 | format: 'cjs', 18 | }, 19 | ], 20 | external: [/node_modules/], 21 | watch: { 22 | include: 'src/**/*', 23 | }, 24 | plugins: [ 25 | cleaner({ 26 | targets: ['./dist/'] 27 | }), 28 | json(), 29 | nodeResolve(), 30 | commonjs(), 31 | typescript({ 32 | tsconfig: "tsconfig.json", 33 | }), 34 | babel({ 35 | include: 'src/*.ts', 36 | }), 37 | ] 38 | }, 39 | { 40 | input: 'types/index.d.ts', 41 | output: [ 42 | { 43 | file: 'dist/js-analyzer-core.d.ts', 44 | format: 'esm', 45 | }, 46 | ], 47 | plugins: [dts()], 48 | } 49 | ]; -------------------------------------------------------------------------------- /packages/core/src/config.ts: -------------------------------------------------------------------------------- 1 | 2 | const path = require("upath") 3 | import { Config } from '../types/index' 4 | 5 | const config: Config = { 6 | root: '/Users/ll/Desktop/work/deepexi/deepexi-daas-catalog-web', 7 | ignore: ['**/node_modules/**', '**/dist/**'], 8 | extensions: ['.js', '.vue', '.json', 'jsx'], 9 | alias: { 10 | '@@/': '/', 11 | '~~/': '/', 12 | '@/': '/src/', 13 | '~/': '/src/', 14 | }, 15 | outputPath: path.resolve(__dirname, './data') 16 | } 17 | export default config -------------------------------------------------------------------------------- /packages/core/src/html-parser.ts: -------------------------------------------------------------------------------- 1 | 2 | import compiler from '@vue/compiler-dom' 3 | import { NodeTypes } from '@vue/compiler-core' 4 | import { RootNode, TemplateChildNode } from '@vue/compiler-dom' 5 | import { ExportDepItem, UsingItem, Config } from '../types' 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 8 | export default function htmlParser (html: string, _file: string, _config: Config) { 9 | // 收集依赖 10 | const importDeps: UsingItem [] = [] 11 | const exportInfo: ExportDepItem = {} 12 | 13 | const { parse, transform } = compiler 14 | 15 | const visitor = (node: RootNode | TemplateChildNode) => { 16 | if (node.type !== NodeTypes.ELEMENT) { 17 | return 18 | } 19 | node.props.forEach(attr => { 20 | if (['img'].includes(node.tag) && 'value' in attr && attr.name === 'src') { // eg: src="../xxx.png" 21 | importDeps.push({ 22 | source: attr.value?.content || '', 23 | vars: 'html@src', 24 | loc: node.loc 25 | }) 26 | } 27 | 28 | if ('exp' in attr && attr.exp && 'content' in attr.exp) { // eg: :src="require('./xxx.png')" 29 | const match = attr.exp.content.match(/require\(['"].+['"]\)/g) // ['require('a')', 'require('b')'] 30 | match?.forEach(item => { 31 | importDeps.push({ 32 | source: item.match(/require\(['"](.+)['"]\)/)?.[1] ?? '', 33 | vars: 'html@src', 34 | loc: node.loc 35 | }) 36 | }) 37 | } 38 | }) 39 | } 40 | 41 | const ast = parse(html, { comments: true }) 42 | transform(ast, { 43 | nodeTransforms: [visitor] 44 | }) 45 | 46 | return { 47 | importDeps, 48 | exportInfo 49 | } 50 | } -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require("upath") 3 | 4 | const fg = require('fast-glob') 5 | import injectExportQuoteNum from './inject-export-quote-num' 6 | import scriptParser from './script-parser' 7 | import styleParser from './style-parser' 8 | import htmlParser from './html-parser' 9 | import logger from '../logger' 10 | const { parse: vueParse } = require('@vue/compiler-sfc') 11 | import { writeFile, clearDist } from './utils' 12 | 13 | import type { 14 | FileDeps, 15 | ImportDeps, 16 | FileQuote, 17 | ExportDeps, 18 | UsingItem, 19 | ExportDepItem, 20 | Config, 21 | DataCollector, 22 | MaterialPackage 23 | } from '../types' 24 | 25 | type LocalConfig = Required 26 | 27 | // --------------------------------------------------------global variable------------------------------------- 28 | 29 | const defEmptyDeps = (): FileDeps => ({ 30 | importDeps: [], 31 | exportInfo: {} 32 | }) 33 | 34 | // --------------------------------------------------------functions------------------------------------- 35 | 36 | function defDependencies (filePath: string): { 37 | dep: ImportDeps, 38 | dependencies: string [] 39 | } { 40 | const dependenciesFile = fs.readFileSync(filePath) 41 | // 生产依赖 42 | const dependencies = Object.keys(JSON.parse(dependenciesFile).dependencies || {}) 43 | 44 | const dep = dependencies.reduce((obj, key) => { 45 | obj[key] = { num: 0, using: [] } 46 | return obj 47 | }, {} as ImportDeps) 48 | 49 | return { 50 | dep, 51 | dependencies, 52 | } 53 | } 54 | 55 | 56 | /** 57 | * 解析路径中的别名 58 | * @param {String} targetPath 带解析目标文件路径 59 | * @param {String} filePath 当前所在文件路径 60 | * @returns 61 | */ 62 | function resolvePath(targetPath: string, filePath: string, config: LocalConfig): string { 63 | const alias = config.alias 64 | 65 | // 相对|绝对路径 66 | const relativePathReg = /^\.\/|^\.\.\// 67 | if (relativePathReg.test(targetPath)) { 68 | const dir = path.dirname(filePath) 69 | return path.resolve(dir, targetPath) 70 | } 71 | 72 | // 别名倒序,先匹配较长的:例如 'ant/c' | 'ant' 会先匹配 'ant/c', 匹配到就结束 73 | const aliasKeys = Object.keys(alias).sort().reverse() 74 | for(const key of aliasKeys) { 75 | const val = alias[key] 76 | if (targetPath.indexOf(key) === 0) { 77 | return path.join(config.root, val, targetPath.substring(key.length)) 78 | } 79 | } 80 | 81 | return targetPath 82 | } 83 | 84 | /** 85 | * 自动添加文件扩展名 86 | * @param {*} filePath 文件路径 87 | * @returns 待后缀名的文件路径 88 | */ 89 | function addExtension(filePath: string, config: LocalConfig) { 90 | if (path.extname(filePath) === '' && filePath.indexOf(config.root) > -1) { 91 | const extFile = config.extensions.find(ext => fs.existsSync(filePath + ext)) 92 | 93 | // 是否存在 name.ext 94 | if (!extFile) { 95 | const indexExt = config.extensions.find(ext => fs.existsSync(path.resolve(filePath, 'index' + ext))) 96 | 97 | // 是否存在 name/index.ext 98 | if (!indexExt) { 99 | const baseName = path.basename(filePath) 100 | const sameNameExt = config.extensions.find(ext => fs.existsSync(path.resolve(filePath, baseName + ext))) 101 | 102 | // 是否存在 name/name.ext 103 | if (!sameNameExt) { 104 | return filePath 105 | } 106 | return path.resolve(filePath, baseName + sameNameExt) 107 | } 108 | return path.resolve(filePath, 'index' + indexExt) 109 | } 110 | return filePath + extFile 111 | } 112 | return filePath 113 | } 114 | 115 | /** 116 | * 解析返回依赖全路径 117 | * @param {String} targetPath 需要解析的路径 118 | * @param {String} filePath 当前所在文件路径 119 | * @returns targetPath 的全路径 120 | */ 121 | function getDepFullPath(targetPath: string, filePath: string, config: LocalConfig): string { 122 | // 去掉前单引号 | 双引号 123 | const s = targetPath.replace(/(^['"])|(['"]$)/g, '') 124 | const r = resolvePath(s, filePath, config) 125 | const f = addExtension(r, config) 126 | return f 127 | } 128 | 129 | /** 130 | * 搜集文件依赖信息和导出信息 131 | * @param {String} file 文件路径 132 | * @returns importDeps, exportInfo 133 | */ 134 | function getFileDeps (file: string, config: LocalConfig): FileDeps { 135 | const extname = path.extname(file) 136 | 137 | // 目前仅支持以下文件解析 138 | const supportExts = [ 139 | '.js', 140 | '.mjs', 141 | '.jsx', 142 | '.ts', 143 | '.tsx', 144 | '.vue', 145 | '.scss', 146 | '.less', 147 | '.css', 148 | '.html' 149 | ] 150 | 151 | if (!supportExts.includes(extname)) { 152 | return defEmptyDeps() 153 | } 154 | 155 | // 读取文件 156 | const fileContent = fs.readFileSync(file, 'utf-8') 157 | 158 | if (['.js','.mjs','.jsx','.ts', '.tsx',].includes(extname)) { 159 | return scriptParser(fileContent, file, config) 160 | } 161 | 162 | if (['.css', '.less', '.scss'].includes(extname)) { 163 | return styleParser(fileContent, extname.replace('.', ''), config) 164 | } 165 | 166 | if (extname === '.html') { 167 | return htmlParser(fileContent, file, config) 168 | } 169 | 170 | if (extname === '.vue') { 171 | const descriptor = vueParse(fileContent).descriptor 172 | 173 | // script deps 174 | let scriptDeps = defEmptyDeps() 175 | if (descriptor.script || descriptor.scriptSetup) { 176 | const script = descriptor.script || descriptor.scriptSetup || {}; 177 | const content = script.content || ''; 178 | const src = script.src 179 | if( src ){ 180 | scriptDeps.importDeps.push({ 181 | source: src, 182 | vars:"script@src", 183 | loc: script.loc 184 | }); 185 | }else{ 186 | scriptDeps = scriptParser(content, file, config) 187 | } 188 | } 189 | 190 | // style deps 191 | const styleDeps = descriptor.styles.reduce((pre: FileDeps, style: any) => { 192 | if(style.src){ 193 | pre.importDeps.push({ 194 | source: style.src, 195 | vars:"style@src", 196 | loc: style.loc 197 | }); 198 | }else{ 199 | const d = styleParser(style.content, style.lang, config) 200 | pre.importDeps = pre.importDeps.concat(d.importDeps); 201 | Object.assign(pre.exportInfo, d.exportInfo) 202 | } 203 | return pre 204 | }, defEmptyDeps()) 205 | 206 | // html deps 207 | let htmlDeps = defEmptyDeps() 208 | if (descriptor.template) { 209 | if(descriptor.template.src) { 210 | htmlDeps.importDeps.push({ 211 | source: descriptor.template.src, 212 | loc: descriptor.template.loc, 213 | vars: "template@src" 214 | }); 215 | }else{ 216 | htmlDeps = htmlParser(descriptor.template.content, file, config) 217 | } 218 | } 219 | 220 | return { 221 | importDeps: Object.assign([], scriptDeps.importDeps, styleDeps.importDeps, htmlDeps.importDeps), 222 | // TODO: 由于 exportInfo 是一个对象,会导致 assign 合并时存在重名覆盖 223 | // 目前看来问题不到,因为引用了同一个文件两次只采集到一次也合理 224 | // 但是如果 source 不一样就有问题了,例如 @import './css.css', @import '~/css.css' 225 | // 上面的情况是同一个文件会被认为是两个文件 226 | exportInfo: Object.assign({}, scriptDeps.exportInfo, styleDeps.exportInfo, htmlDeps.exportInfo) 227 | } 228 | } 229 | logger.error(`getFileDeps:未被处理的文件格式${extname}`) 230 | return defEmptyDeps() 231 | } 232 | 233 | 234 | /** 235 | * 注入 export 依赖 236 | * @param {Object} info 文件导出信息 237 | * @param {String} file 文件路径 238 | */ 239 | function injectExportInfo (info: ExportDepItem, file: string, exportQuote: ExportDeps) { 240 | exportQuote[file] = info 241 | } 242 | 243 | /** 244 | * 注入依赖 245 | * @param {Array} deps 文件依赖信息 246 | * @param {String} file 文件路径 247 | */ 248 | function injectFileDeps( 249 | deps: UsingItem [], 250 | file: string, 251 | fileQuote: FileQuote, 252 | packageQuote: ImportDeps, 253 | dependencies: string [], 254 | unknownQuote: ImportDeps, 255 | config: LocalConfig 256 | ) { 257 | for (const item of deps) { 258 | const { source, vars, loc } = item 259 | const depName = getDepFullPath(source, file, config) 260 | const targetKey = dependencies.find(key => { 261 | if (depName.startsWith('@')) { // package name has namespace 262 | return depName.startsWith(key) 263 | } else { 264 | return depName.split('/')[0] === key 265 | } 266 | }) 267 | 268 | // 生产依赖 269 | if (targetKey) { 270 | packageQuote[targetKey].num += 1 271 | packageQuote[targetKey].using.push({ source: source, vars: vars, fullPath: file, loc }) 272 | } else if (fileQuote[depName]) { // 文件依赖 273 | fileQuote[depName].num += 1 274 | fileQuote[depName].using.push({ source: source, vars: vars, fullPath: file, loc }) 275 | } else { // 未知依赖 276 | if (unknownQuote[depName]) { 277 | unknownQuote[depName].num += 1 278 | unknownQuote[depName].using.push({ 279 | source: source, 280 | vars: vars, 281 | fullPath: file, 282 | loc, 283 | }) 284 | } else { 285 | unknownQuote[depName] = { 286 | num: 1, 287 | using: [{ 288 | source: source, 289 | vars: vars, 290 | fullPath: file, 291 | loc, 292 | }] 293 | } 294 | } 295 | } 296 | } 297 | } 298 | 299 | /** 300 | * 获取绝对路径,兼容 win 301 | * @param root 302 | * @returns 303 | */ 304 | function getNormalizeFullPath (root: string) { 305 | const p = path.normalize(root) 306 | return path.isAbsolute(p) ? p : path.join(process.cwd(), p) 307 | } 308 | 309 | 310 | // ---------------------------------------------execute------------------------------------------------ 311 | 312 | /** 313 | * 主程序 314 | */ 315 | async function main(config: LocalConfig): Promise { 316 | if (!config.root) { 317 | throw new Error('root must be in the config file') 318 | } 319 | 320 | const fileQuote: FileQuote = {} 321 | const exportQuote: ExportDeps = {} 322 | const unknownQuote: ImportDeps = {} 323 | const configPath = getNormalizeFullPath(config.path || config.root) 324 | config.root = configPath 325 | 326 | const searchPath = path.normalize( 327 | fs.lstatSync(configPath).isDirectory() 328 | ? configPath + '/**/*' 329 | : configPath 330 | ) 331 | 332 | // config info 333 | console.log(JSON.stringify(config, null, 2)) 334 | 335 | const files: string [] = await fg([searchPath], { 336 | ignore: config.ignore || ['**/node_modules/**', '**/dist/**'] 337 | }) 338 | 339 | // 同一个项目下可能存在多个 package.json 文件,这里主要是合并所有依赖信息 340 | const packages = files.filter(file => file.endsWith('package.json')) 341 | .map(file => defDependencies(file)) 342 | const packageQuote = packages 343 | .reduce((collector, item) => { 344 | Object.assign(collector, item.dep) 345 | return collector 346 | }, {} as ImportDeps) 347 | const dependencies = packages 348 | .reduce((collector, item) => { 349 | collector = Array.from(new Set([...collector, ...item.dependencies])) 350 | return collector 351 | }, [] as string []) 352 | 353 | files.forEach(file => { 354 | fileQuote[file] = { 355 | num: 0, 356 | using: [], 357 | deps: [] 358 | } 359 | }) 360 | 361 | files.forEach(file => { 362 | try { 363 | const { importDeps, exportInfo } = getFileDeps(file, config) 364 | fileQuote[file].deps = importDeps 365 | 366 | injectFileDeps(importDeps, file, fileQuote, packageQuote, dependencies, unknownQuote, config) 367 | injectExportInfo(exportInfo, file, exportQuote) 368 | } catch (error) { 369 | logger.error(error, { 370 | fn: 'main', 371 | file: file, 372 | }) 373 | } 374 | }) 375 | injectExportQuoteNum({ 376 | packageQuote, 377 | unknownQuote, 378 | fileQuote, 379 | }, exportQuote) 380 | return { 381 | files, 382 | fileQuote, 383 | exportQuote, 384 | packageQuote, 385 | unknownQuote, 386 | } 387 | } 388 | 389 | export class JsAnalyzer { 390 | private config: LocalConfig 391 | private materialPackage: MaterialPackage = { 392 | 'import-files': {}, 393 | 'import-package': {}, 394 | 'import-unknown': {}, 395 | 'files': [], 396 | 'export': {}, 397 | } 398 | 399 | constructor (config: LocalConfig){ 400 | this.config = config 401 | } 402 | 403 | /** 404 | * 405 | * @param config 配置文件 406 | * @returns MaterialPackage 物料包 407 | */ 408 | async init (config: Partial): Promise { 409 | this.config = Object.assign(this.config, config) 410 | this.config.outputPath && await clearDist(this.config.outputPath) 411 | 412 | return main(this.config) 413 | .then(res => { 414 | this.materialPackage = { 415 | 'import-files': res.fileQuote, 416 | 'import-package': res.packageQuote, 417 | 'import-unknown': res.unknownQuote, 418 | 'files': res.files, 419 | 'export': res.exportQuote, 420 | } 421 | 422 | this.config.outputPath && this.write() 423 | this.config.outputPath && this.writeFromPlugin() 424 | 425 | return this.materialPackage 426 | }) 427 | } 428 | 429 | 430 | /** 431 | * 获取依赖信息 432 | * @returns data 依赖信息 433 | */ 434 | getData () { 435 | return this.materialPackage 436 | } 437 | 438 | /** 439 | * 将输出信息写入文件 440 | */ 441 | write () { 442 | Object.keys(this.materialPackage).forEach(name => { 443 | writeFile( 444 | name + '.json', 445 | this.materialPackage[name as keyof MaterialPackage], 446 | this.config.outputPath 447 | ) 448 | }) 449 | } 450 | 451 | writeFromPlugin () { 452 | const plugins = this.config.plugins || [] 453 | const dataPlugins = plugins.filter(plugin => plugin.output && plugin.output.data) 454 | dataPlugins.forEach(plugin => { 455 | writeFile( 456 | plugin.output.file, 457 | plugin.output.data, 458 | this.config.outputPath 459 | ) 460 | }) 461 | } 462 | 463 | } 464 | 465 | 466 | -------------------------------------------------------------------------------- /packages/core/src/inject-export-quote-num.ts: -------------------------------------------------------------------------------- 1 | import { ExportDepItem, ImportDeps, ExportDeps } from '../types' 2 | 3 | function addNum (obj: ExportDepItem, key: string, origin: string) { 4 | if (obj[key]) { 5 | obj[key].num += 1 6 | obj[key].using.push(origin) 7 | } else { 8 | obj[key] = { 9 | num: 1, 10 | using: [origin] 11 | } 12 | } 13 | } 14 | 15 | /** 16 | * 将引用信息插入到导入物料包中 17 | * @param quoteMap 引用物料 18 | * @param exportMap 导出物料 19 | */ 20 | export default function injectExportQuoteNum (quoteMap: Record, exportMap: ExportDeps) { 21 | Object.keys(quoteMap).forEach(key => { 22 | const depMap = quoteMap[key] 23 | const files = Object.keys(depMap) 24 | for (let i = 0; i < files.length; i++) { 25 | const file = files[i] 26 | const dep = depMap[file] 27 | 28 | dep.using.forEach(varItem => { 29 | const targetFile = exportMap[file] 30 | if (targetFile) { 31 | const vars = varItem.vars.split(',') 32 | vars.forEach(m => { 33 | addNum(targetFile, m, varItem.fullPath as string) 34 | }) 35 | } 36 | }) 37 | } 38 | }) 39 | } -------------------------------------------------------------------------------- /packages/core/src/script-parser.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | const traverse = require("@babel/traverse").default; 4 | const parser = require("@babel/parser"); 5 | import logger from '../logger' 6 | const t = require('@babel/types'); 7 | import { ExportDepItem, UsingItem, FileDeps, Config } from '../types' 8 | 9 | /** 10 | * js 文件解析器 11 | * @param {String} content js 文件 source 12 | * @returns importDeps 引用信息 13 | * @returns exportInfo 导出信息 14 | */ 15 | export default function scriptParser (content: string, file: string, config: Config):FileDeps { 16 | // 收集依赖 17 | const importDeps: UsingItem [] = [] 18 | const exportInfo: ExportDepItem = {} 19 | 20 | // 转为 AST 语法树 21 | const ast = parser.parse(content, { 22 | sourceType: 'module', 23 | allowImportExportEverywhere: true, 24 | presets: ['@babel/preset-env'], 25 | plugins: [ 26 | 27 | ["decorators", { decoratorsBeforeExport: true }], 28 | 'asyncGenerators', 29 | 'bigInt', 30 | 'classProperties', 31 | 'classPrivateProperties', 32 | 'classPrivateMethods', 33 | 'legacy-decorators', 34 | 'doExpressions', 35 | 'dynamicImport', 36 | 'exportDefaultFrom', 37 | 'exportNamespaceFrom', 38 | 'functionBind', 39 | 'functionSent', 40 | 'importMeta', 41 | 'logicalAssignment', 42 | 'nullishCoalescingOperator', 43 | 'numericSeparator', 44 | 'objectRestSpread', 45 | 'optionalCatchBinding', 46 | 'optionalChaining', 47 | ['pipelineOperator', { proposal: 'minimal' }], 48 | 'throwExpressions', 49 | ['@babel/plugin-proposal-decorators', { legacy: true }], 50 | 'typescript', 51 | 'jsx', 52 | ] 53 | }) 54 | 55 | // custom plugins 56 | const plugins = config.plugins || [] 57 | const scriptsPlugins = plugins.filter(p => p.ScriptParser) 58 | scriptsPlugins.forEach(plugin => { 59 | const opt = plugin.ScriptParser ? plugin.ScriptParser({ 60 | file, 61 | content, 62 | }) : {} 63 | traverse(ast, opt) 64 | plugin.AfterScriptParser && plugin.AfterScriptParser() 65 | }) 66 | 67 | traverse(ast, { 68 | // 静态 Import 69 | ImportDeclaration(tPath: any) { 70 | const { node } = tPath 71 | const vars = node.specifiers.map((specifier: any) => { 72 | switch (specifier.type) { 73 | case 'ImportNamespaceSpecifier': { 74 | // 引入的别名 75 | // 例如:import * as api from '@/api', 获取到的就是 api 76 | const specifierName = specifier.local.name 77 | const binding = tPath.scope.getBinding(specifierName) 78 | 79 | // 遍历使用别名的地方 80 | const localVars = binding.referencePaths.map((referencePath: any)=> { 81 | // 没有引用别名的地方 82 | if (referencePath.parentPath.node && referencePath.parentPath.node.property) { 83 | return referencePath.parentPath.node.property.name 84 | } 85 | return undefined 86 | }); 87 | return localVars.filter(Boolean).join(',') 88 | } 89 | case 'ImportDefaultSpecifier': { 90 | return 'Default' 91 | } 92 | case 'ImportSpecifier': { 93 | return specifier.imported.name 94 | } 95 | } 96 | }) 97 | importDeps.push({ 98 | source: node.source.value, 99 | vars: vars.join(','), 100 | loc: node.loc, 101 | }) 102 | }, 103 | // export 104 | ExportNamedDeclaration(tPath: any) { 105 | // export { a,b,c } 106 | if (tPath.node.specifiers.length) { 107 | tPath.node.specifiers.forEach((item: any) => { 108 | exportInfo[item.local.name] = { 109 | num: 0, 110 | using: [] 111 | } 112 | }) 113 | } else { // export const a = 'aaa' / export function a 114 | if (!tPath.node.declaration) { 115 | logger.info('unknown export: ' + file) 116 | return 117 | } 118 | if (tPath.node.declaration.id) { 119 | exportInfo[tPath.node.declaration.id.name] = { 120 | num: 0, 121 | using: [] 122 | } 123 | } else if (tPath.node.declaration.declarations){ 124 | tPath.node.declaration.declarations.forEach((item: any) => { 125 | exportInfo[item.id.name] = { 126 | num: 0, 127 | using: [] 128 | } 129 | }) 130 | } else { 131 | logger.info('unknown export: ' + file) 132 | } 133 | } 134 | }, 135 | // export default 136 | ExportDefaultDeclaration() { 137 | exportInfo['Default'] = { 138 | num: 0, 139 | using: [] 140 | } 141 | }, 142 | // require | 动态 Import 143 | CallExpression(tPath: any) { 144 | const node = tPath.node 145 | if (node.callee.type === 'Import' && node.arguments[0].value) { 146 | importDeps.push({ 147 | source: node.arguments[0].value, 148 | vars: 'Default', 149 | loc: node.loc 150 | }) 151 | } 152 | if ( 153 | node.callee && 154 | node.callee.name === 'require' 155 | && node.arguments[0] 156 | && node.arguments[0].value 157 | ) { 158 | importDeps.push({ 159 | source: node.arguments[0].value, 160 | vars: 'requireDefault', 161 | loc: node.loc 162 | }) 163 | } 164 | }, 165 | // module.exports 166 | AssignmentExpression(tPath: any) { 167 | const node = tPath.node 168 | const { left } = node 169 | // module.exports = {} || module.exports.x = xxx 170 | if ( 171 | ( 172 | t.isMemberExpression(left) && 173 | left.object.name === 'module' && 174 | left.property.name === 'exports' 175 | ) || 176 | ( 177 | t.isMemberExpression(left) && 178 | left.object.object && 179 | left.object.object.name === 'module' && 180 | left.object.property.name === 'exports' 181 | ) 182 | ) { 183 | exportInfo['Default'] = { 184 | num: 0, 185 | using: [] 186 | } 187 | } 188 | } 189 | }) 190 | 191 | return { 192 | importDeps: importDeps, 193 | exportInfo: exportInfo 194 | } 195 | } -------------------------------------------------------------------------------- /packages/core/src/style-parser.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * doc 4 | * https://astexplorer.net/#/2uBU1BLuJ1 5 | * https://www.postcss.com.cn/api/#atrule-type 6 | */ 7 | const postcss = require('postcss') 8 | const postcssLess = require('postcss-less') 9 | const postcssSass = require('postcss-sass') 10 | const postcssScss = require('postcss-scss') 11 | 12 | import { ExportDepItem, UsingItem, Config } from '../types' 13 | 14 | type Lang = 'css' | 'scss' | 'less' | 'sass' 15 | /** 16 | * 17 | * @param {String} content js 文件 source 18 | * @returns importDeps 引用信息 19 | * @returns exportInfo 导出信息 20 | */ 21 | // @ts-ignore 22 | const parserRules = { 23 | 'background': (val: string) => { 24 | const res = val.match(/url\((.+)\)/i) 25 | return res ? res[1] : null 26 | }, 27 | 'background-image': (val: string) => { 28 | const res = val.match(/url\((.+)\)/i) 29 | return res ? res[1] : null 30 | }, 31 | } 32 | 33 | const getParser = (lang: Lang): any => { 34 | switch(lang){ 35 | case 'css': 36 | return postcss.parse 37 | case 'less': 38 | return postcssLess.parse 39 | case 'sass': 40 | return postcssSass.parse 41 | case 'scss': 42 | return postcssScss.parse 43 | default: 44 | return postcss.parse 45 | } 46 | } 47 | 48 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 49 | export default function styleParser (content: string, lang: Lang = 'css', _config: Config) { 50 | // 收集依赖 51 | const importDeps: UsingItem [] = [] 52 | const exportInfo: ExportDepItem = {} 53 | const parser = getParser(lang) 54 | const root = parser(content); 55 | 56 | // rule 57 | root.walkRules((rule: any) => { 58 | rule.nodes.forEach((node: any) => { 59 | const prop = node.prop as keyof typeof parserRules 60 | if (parserRules[prop]) { 61 | const path = parserRules[prop](node.value) 62 | path && importDeps.push({ 63 | source: path, 64 | vars: 'css@background', 65 | loc: node.loc 66 | }) 67 | } 68 | }) 69 | }) 70 | 71 | // at root 72 | root.walkAtRules((rule: any) => { 73 | if (rule.name === 'import') { 74 | importDeps.push({ 75 | source: rule.params.replace(/['"]/g, ''), 76 | vars: 'css@import', 77 | loc: {} 78 | }) 79 | } 80 | }) 81 | 82 | return { 83 | importDeps, 84 | exportInfo 85 | } 86 | } -------------------------------------------------------------------------------- /packages/core/src/test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const postcss = require('postcss') 3 | const path = require('path'); 4 | 5 | const css = fs.readFileSync(path.resolve(__dirname, '../test/test.less')); 6 | const root = postcss.parse(css); 7 | 8 | const parsers = { 9 | 'background': (val) => { 10 | const res = val.match(/url\((.+)\)/i) 11 | return res ? res[1] : null 12 | }, 13 | 'background-image': (val) => { 14 | const res = val.match(/url\((.+)\)/i) 15 | return res ? res[1] : null 16 | }, 17 | } 18 | 19 | const imports = [] 20 | console.log(root.nodes) 21 | root.walkRules(rule => { 22 | rule.nodes.forEach(node => { 23 | if (parsers[node.prop]) { 24 | const path = parsers[node.prop](node.value) 25 | path && imports.push(path) 26 | } 27 | }) 28 | }) 29 | 30 | // at root 31 | root.walkAtRules((rule) => { 32 | if (rule.name === 'import') { 33 | imports.push(rule.params.replace(/['"]/g, '')) 34 | } 35 | }) 36 | 37 | console.log(imports) 38 | 39 | -------------------------------------------------------------------------------- /packages/core/src/utils.ts: -------------------------------------------------------------------------------- 1 | const del = require('del') 2 | const fs = require('fs') 3 | const path = require("path") 4 | import logger from '../logger' 5 | 6 | 7 | export function isObject (v: unknown) { 8 | return Object.prototype.toString.call(v) === '[object Object]' 9 | } 10 | 11 | // function isArray (v: unknown) { 12 | // return Object.prototype.toString.call(v) === '[object Array]' 13 | // } 14 | 15 | /** 16 | * 写入文件 17 | * @param {String} name 文件名 18 | * @param {String} data 文件内容 19 | */ 20 | export function writeFile(name: string, data: any, outputPath: string | undefined) { 21 | if (!outputPath) { 22 | throw new Error('outputPath must be in the config file') 23 | } 24 | !fs.existsSync(outputPath) && fs.mkdirSync(outputPath) 25 | // logger.info('文件写入中:' + name) 26 | // 修复:data 过长导致 JSON.stringify 报错 27 | // if (isArray(data)) { 28 | // data = '[' + data.map((m: any) => JSON.stringify(m, null, 2)).join(",") +']' 29 | // } 30 | fs.writeFile(path.resolve(outputPath, name), JSON.stringify(data, null, 2), 'utf-8', (error: any) => { 31 | if (error) logger.error(`write:写入文件失败 ${name}`) 32 | }) 33 | } 34 | 35 | /** 36 | * 清理文件 37 | */ 38 | export async function clearDist(outputPath: string | undefined) { 39 | // logger.info('清除文件...:' + outputPath) 40 | if (fs.existsSync(outputPath)) { 41 | await del.sync([`${outputPath}/**`, '!publicDir'], { 42 | force: true 43 | }) 44 | } 45 | } -------------------------------------------------------------------------------- /packages/core/test/test.less: -------------------------------------------------------------------------------- 1 | @import './var.less'; 2 | a { 3 | color: red; 4 | text-align: center; 5 | } 6 | 7 | .test1 { 8 | background: url(/packages/web/public/favicon.ico); 9 | background: none; 10 | background-image: url(/packages/web/public/favicon.ico); 11 | } 12 | 13 | .test2 { 14 | color: beige; 15 | .test2-1 { 16 | color: blue; 17 | .test2-1-1 { 18 | content: 'test2-1-1'; 19 | } 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["./src/**/*", "./types/**/*"], 4 | "exclude": ["dist", "node_modules"], 5 | "compilerOptions": { 6 | "module": "esnext", 7 | "lib": ["dom", "esnext"], 8 | "importHelpers": true, 9 | // output .d.ts declaration files for consumers 10 | // "declaration": true, 11 | // "declarationDir": "types", 12 | // output .js.map sourcemap files for consumers 13 | "sourceMap": true, 14 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 15 | "rootDir": ".", 16 | // stricter type-checking for stronger correctness. Recommended by TS 17 | "strict": true, 18 | // linter checks for common issues 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | // use Node's module resolution algorithm, instead of the legacy TS one 25 | "moduleResolution": "node", 26 | // transpile JSX to React.createElement 27 | "jsx": "react", 28 | // interop between ESM and CJS modules. Recommended by TS 29 | "esModuleInterop": true, 30 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 31 | "skipLibCheck": true, 32 | // error out if import and file system have a casing mismatch. Recommended by TS 33 | "forceConsistentCasingInFileNames": true, 34 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 35 | "noEmit": true, 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/core/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { SourceLocation } from '@babel/parser' 2 | // 引用信息 3 | export interface UsingItem { 4 | source: string, 5 | vars: string, 6 | fullPath?: string, 7 | loc: SourceLocation 8 | } 9 | 10 | export interface ImportDepItem { 11 | num: number, 12 | using: UsingItem [] 13 | } 14 | 15 | export interface ImportDeps { 16 | [path: string]: ImportDepItem 17 | } 18 | 19 | // 导出信息 20 | export interface ExportDepItem { 21 | [vars: string]: { 22 | num: number, 23 | using: string [] 24 | } 25 | } 26 | 27 | export interface ExportDeps { 28 | [path: string]: ExportDepItem 29 | } 30 | 31 | export interface FileQuoteItem { 32 | num: number, 33 | using: UsingItem [] 34 | deps: UsingItem [] 35 | } 36 | 37 | export interface FileQuote { 38 | [path: string]: FileQuoteItem 39 | } 40 | 41 | // 文件依赖信息 42 | export interface FileDeps { 43 | importDeps: UsingItem [], 44 | exportInfo: ExportDepItem 45 | } 46 | 47 | // 自定义插件 48 | export interface Plugin { 49 | name: string, 50 | output: { 51 | data: Record 52 | file: string 53 | }, 54 | ScriptParser?: (data: { file: string, content: string }) => Record, 55 | AfterScriptParser?: () => void 56 | } 57 | 58 | // 配置信息 59 | export interface Config { 60 | root: string, 61 | ignore?: (string | RegExp) [], 62 | extensions?: string [], 63 | alias?: Record, 64 | path?: string, 65 | outputPath?: string, 66 | plugins?: Plugin [], 67 | ide?: string 68 | } 69 | 70 | 71 | // 核心导出信息 72 | export interface DataCollector { 73 | files: string [], 74 | fileQuote: ImportDeps, 75 | exportQuote: ExportDeps, 76 | packageQuote: ImportDeps, 77 | unknownQuote: ImportDeps, 78 | } 79 | 80 | export interface MaterialPackage { 81 | 'files': string [], 82 | 'import-files': ImportDeps, 83 | 'export': ExportDeps, 84 | 'import-package': ImportDeps, 85 | 'import-unknown': ImportDeps, 86 | } -------------------------------------------------------------------------------- /packages/server/.npmrc: -------------------------------------------------------------------------------- 1 | workspaces = true 2 | workspaces-update = false -------------------------------------------------------------------------------- /packages/server/.releaserc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | repositoryUrl: "https://github.com/chennlang/js-analyzer.git", 3 | branches: ["main"], // 指定在哪个分支下要执行发布操作 4 | plugins: [ 5 | "@semantic-release/commit-analyzer", // 解析 commit 信息,默认就是 Angular 规范 6 | "@semantic-release/release-notes-generator", 7 | [ 8 | "@semantic-release/changelog", 9 | { 10 | changelogFile: "CHANGELOG.md", // 把发布日志写入该文件 11 | }, 12 | ], 13 | "@semantic-release/npm", // 发布到NPM 14 | "@semantic-release/github", 15 | [ 16 | "@semantic-release/git", 17 | { 18 | assets: ["CHANGELOG.md", "package.json"], // 前面说到日志记录和版本好是新增修改的,需要 push 回 Git 19 | }, 20 | ], 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /packages/server/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [2.8.0](https://github.com/chennlang/js-analyzer/compare/v2.7.0...v2.8.0) (2025-01-08) 2 | 3 | 4 | ### Features 5 | 6 | * 切换打开编辑器的插件&支持在config中配置使用什么编辑器打开,默认值为vscode ([45dc22e](https://github.com/chennlang/js-analyzer/commit/45dc22e4ce6c95f399697ebe7f394130e5eec92e)) 7 | 8 | # [2.7.0](https://github.com/chennlang/js-analyzer/compare/v2.6.2...v2.7.0) (2024-11-29) 9 | 10 | 11 | ### Features 12 | 13 | * update node styles & add switch node display level ([3c6518d](https://github.com/chennlang/js-analyzer/commit/3c6518d49fd6ec308b9ffa430436a25b61b1bc58)) 14 | 15 | ## [2.6.2](https://github.com/chennlang/js-analyzer/compare/v2.6.1...v2.6.2) (2024-11-06) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * 去除 core 自动发布,改为手动发布 ([320bf93](https://github.com/chennlang/js-analyzer/commit/320bf938bb17ba1f0738fd7c6ad997b1bb0daa00)) 21 | 22 | ## [2.5.1](https://github.com/chennlang/js-analyzer/compare/v2.5.0...v2.5.1) (2024-10-26) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * 不支持 win & 局部配置支持相对路径 ([dea2ab7](https://github.com/chennlang/js-analyzer/commit/dea2ab72c57fa4d95957ebdac1e01c2e42d1782b)) 28 | 29 | # [2.5.0](https://github.com/chennlang/js-analyzer/compare/v2.4.0...v2.5.0) (2024-08-27) 30 | 31 | 32 | ### Features 33 | 34 | * application interface style optimization ([0aeb82e](https://github.com/chennlang/js-analyzer/commit/0aeb82e6ee757fce131fdc5d48bbff9c093a34e7)) 35 | 36 | # [2.4.0](https://github.com/chennlang/js-analyzer/compare/v2.3.1...v2.4.0) (2024-07-25) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * Cannot find module '../libs/core-dist/js-analyzer-core.cjs.js' ([2a1c47f](https://github.com/chennlang/js-analyzer/commit/2a1c47f0e3413a765b6fc48309f8c1e2bf509b1b)) 42 | * Cannot find module js-analyzer-core.cjs.js ([9889be4](https://github.com/chennlang/js-analyzer/commit/9889be477fdce3e00d5c3cc47a8721e243d5e34e)) 43 | * no such file or directory:public/data ([4dd5219](https://github.com/chennlang/js-analyzer/commit/4dd5219005301d362a9b41a2494e907246d94465)) 44 | * not find public/data ([86c28ab](https://github.com/chennlang/js-analyzer/commit/86c28ab1bd878746ecfd165dd2da96df2f3a9165)) 45 | * remove publish script ([77107da](https://github.com/chennlang/js-analyzer/commit/77107da09e72244badafdfc1d17a60b1daf303df)) 46 | * remove test.json ([0107ae3](https://github.com/chennlang/js-analyzer/commit/0107ae3af7d9afb36ba8ff55bf396324d029e0c6)) 47 | * web page js file 404 ([b04c1e3](https://github.com/chennlang/js-analyzer/commit/b04c1e309abd171f70125a5a27c210af73c46f18)) 48 | 49 | 50 | ### Features 51 | 52 | * 支持国际化语言 ([a778058](https://github.com/chennlang/js-analyzer/commit/a778058751f36d954ca9d16d055f446a2e43a684)) 53 | * 添加日语文档 ([58b40e7](https://github.com/chennlang/js-analyzer/commit/58b40e78579ad4e101a5ec86676340b30c83ba2e)) 54 | 55 | ## [2.3.1](https://github.com/chennlang/js-analyzer/compare/v2.3.0...v2.3.1) (2024-07-05) 56 | 57 | 58 | ### Bug Fixes 59 | 60 | * 修复本地启动异常 ([4c83592](https://github.com/chennlang/js-analyzer/commit/4c83592c78f91d033b69fa3665cbfc968e619be7)) 61 | 62 | # [2.3.0](https://github.com/chennlang/js-analyzer/compare/v2.2.6...v2.3.0) (2024-06-26) 63 | 64 | 65 | ### Bug Fixes 66 | 67 | * remove test.json ([42610ff](https://github.com/chennlang/js-analyzer/commit/42610ffce91f9f31c51044f78f538e6711a356a0)) 68 | 69 | 70 | ### Features 71 | 72 | * 支持国际化语言 ([3989179](https://github.com/chennlang/js-analyzer/commit/3989179c2b678f629d77fe9dc1a3fff19f68407b)) 73 | * 添加日语文档 ([e1adc1f](https://github.com/chennlang/js-analyzer/commit/e1adc1fe0c17e494a56ea7871619adfb5c157e40)) 74 | 75 | ## [2.2.6](https://github.com/chennlang/js-analyzer/compare/v2.2.5...v2.2.6) (2024-06-17) 76 | 77 | 78 | ### Bug Fixes 79 | 80 | * web page js file 404 ([e3fb6bd](https://github.com/chennlang/js-analyzer/commit/e3fb6bd7638334b05fd02645238b473910df4f93)) 81 | 82 | ## [2.2.5](https://github.com/chennlang/js-analyzer/compare/v2.2.4...v2.2.5) (2024-06-17) 83 | 84 | 85 | ### Bug Fixes 86 | 87 | * not find public/data ([f999c56](https://github.com/chennlang/js-analyzer/commit/f999c568b1f3f5973ac760b74dace12e8175a0cb)) 88 | 89 | ## [2.2.4](https://github.com/chennlang/js-analyzer/compare/v2.2.3...v2.2.4) (2024-06-17) 90 | 91 | 92 | ### Bug Fixes 93 | 94 | * no such file or directory:public/data ([bbd3fd2](https://github.com/chennlang/js-analyzer/commit/bbd3fd29294de5abafa5c1d13d9ea40b315e9478)) 95 | 96 | ## [2.2.3](https://github.com/chennlang/js-analyzer/compare/v2.2.2...v2.2.3) (2024-06-17) 97 | 98 | 99 | ### Bug Fixes 100 | 101 | * Cannot find module js-analyzer-core.cjs.js ([7d3fe28](https://github.com/chennlang/js-analyzer/commit/7d3fe284dc59afe41a6b3ac4296d11d495ac04b2)) 102 | 103 | ## [2.2.2](https://github.com/chennlang/js-analyzer/compare/v2.2.1...v2.2.2) (2024-06-17) 104 | 105 | 106 | ### Bug Fixes 107 | 108 | * Cannot find module '../libs/core-dist/js-analyzer-core.cjs.js' ([7409cc6](https://github.com/chennlang/js-analyzer/commit/7409cc67ec3e4da7f1fd0e2a34ac6e43a74c479e)) 109 | 110 | ## [2.2.1](https://github.com/chennlang/js-analyzer/compare/v2.2.0...v2.2.1) (2024-06-17) 111 | 112 | 113 | ### Bug Fixes 114 | 115 | * add changelog ([ac403ec](https://github.com/chennlang/js-analyzer/commit/ac403ecdcb303626520263742a0b7dcbe1644494)) 116 | 117 | # [2.2.0](https://github.com/chennlang/js-analyzer/compare/v2.1.0...v2.2.0) (2024-06-13) 118 | 119 | 120 | ### Bug Fixes 121 | 122 | * 修复别名无法解析 ([64654e0](https://github.com/chennlang/js-analyzer/commit/64654e0f753ad7cf6abfc5bfebc2a4bb8d56c575)) 123 | 124 | 125 | ### Features 126 | 127 | * 视图切换组件优化 ([e8ef179](https://github.com/chennlang/js-analyzer/commit/e8ef179d382b02c47cec31a8f94c46c51608a863)) 128 | -------------------------------------------------------------------------------- /packages/server/README.md: -------------------------------------------------------------------------------- 1 |
2 |

Js Analyzer

3 |

一个可视化可交互的前端依赖分析工具

4 |

可用于 Vue React Svelte Angular Node 等任何前端项目

5 |

https://chennlang.github.io/js-analyzer

6 |
7 | 8 | ## 为什么 9 | 10 | - 代码重构:通过分析依赖关系,我们可以更好地理解代码的结构和逻辑,从而更容易地进行代码重构和优化。 11 | - 模块化开发:通过分析依赖关系,我们可以将项目拆分成多个模块,每个模块都有清晰的职责和依赖关系,从而实现模块化开发和管理。 12 | - 代码测试:通过分析依赖关系,我们可以更容易地编写和运行单元测试,从而提高代码的质量和可靠性。 13 | - 代码维护:通过分析依赖关系,我们可以更容易地定位和解决代码中的问题,从而提高代码的可维护性和可扩展性。 14 | 15 | ## 功能 16 | 17 | - 可交互的一体化`可视化`依赖分析系统 18 | - 支持动态切换入口文件 19 | - 支持`依赖反转` 20 | - 支持显示文件被引用次数,以及引用地址 21 | - 支持显示文件的导出变量被引用信息 22 | - 适用于 ES6、CommonJs 23 | - 支持的文件类型:JS、TS、JSX、TSX、Vue、Sass、Less、Css、html 24 | - 支持 package 依赖分析 25 | - 支持未引用 文件、npm 包分析 26 | - 本地存储 `非常安全`,不涉及联网和上传 27 | 28 |

被依赖视图

29 | 30 | > 双击某个节点,可进入该节点的依赖视图 31 | 32 | ![单文件](http://oss.ailan.top/20230713103748.png) 33 | 34 |

上游依赖图

35 | 36 | > 双击某个节点后,点击左上角的正数第三个图标,切换成 上游依赖图 37 | 38 | ![上游依赖图](http://oss.ailan.top/20230713104701.png) 39 | 40 |

单个文件依赖详情信息

41 | 42 | > 单击视图中的某个节点,弹出文件依赖详情信息 43 | 44 | ![单个文件依赖详情信息](http://oss.ailan.top/20230713104922.png) 45 | 46 | ## 更新 47 | 48 | - 支持 VUE SETUP 类型 49 | - 可自定义插件,生成你想要的数据 50 | - 内置项目热词插件支持 51 | - 文件依赖视图:支持单个文件夹内依赖关系视图 52 | - Sass、Less、Css 等样式文件分析(New, 已支持) 53 | - 支持项目变量热词图 54 | 55 | ## 全局安装 56 | 57 | ### 1. 安装 58 | 59 | ```shell 60 | npm install @js-analyzer/server -g 61 | # yarn add @js-analyzer/server -g 62 | # pnpm install @js-analyzer/server -g 63 | ``` 64 | 65 | ### 2. 使用 66 | 67 | 控制台进入到任意项目根目录下,执行 `js-analyzer --root ./` 68 | 69 | ```shell 70 | cd /xxx/project 71 | 72 | js-analyzer --root ./ 73 | ``` 74 | 75 | ## 局部安装 76 | 77 | ### 1. 安装 78 | 79 | ```shell 80 | npm install @js-analyzer/server -D 81 | # yarn add @js-analyzer/server -D 82 | # pnpm install @js-analyzer/server -D 83 | ``` 84 | 85 | ### 2. 使用 86 | 87 | #### 1.在 scripts 中添加 js-analyzer 命令 88 | 89 | ```json 90 | "scripts": { 91 | "js-analyzer": "js-analyzer --root ./" 92 | }, 93 | ``` 94 | 95 | #### 2.在控制台输入 npm run js-analyzer,访问 http://localhost:8088/ 就能看到了。 96 | 97 | ```shell 98 | npm run js-analyzer 99 | # Service started:http://localhost:8088/ 100 | ``` 101 | 102 | ## 配置文件 103 | 104 | 通过上面的命令已经能很快启动一个分析服务了,可是每个项目的整体架构不同,想要 js-analyzer 更好的更准确的分析,还需要配置一些必要信息。 105 | 106 | 指定配置文件只需要将上面的启动命令修改一下 107 | 108 | ```json 109 | "scripts": { 110 | "js-analyzer": "js-analyzer --config ./js-analyzer.js" 111 | }, 112 | ``` 113 | 114 | js-analyzer.js 115 | 116 | ```js 117 | module.exports = { 118 | // 根目录 119 | root: "./", 120 | // 不需要分析的目录 121 | ignore: ["**/node_modules/**", "**/dist/**"], 122 | // 解析没有扩展名的文件时优先查找顺序 123 | extensions: [".js", ".ts", ".tsx", ".vue", ".json", "jsx"], 124 | // 项目的别名映射路径 125 | alias: { 126 | "@@/": "/", 127 | "~~/": "/", 128 | "@/": "/src/", 129 | "~/": "/src/", 130 | }, 131 | // 启动的服务器和端口相关 132 | server: { 133 | port: 8088, 134 | host: "localhost", 135 | openBrowser: true, // 启动后自动在浏览器打开 136 | }, 137 | }; 138 | ``` 139 | 140 | ## TODO 141 | 142 | - 项目组件文档生成共享模块 143 | - 循环依赖分析 144 | - 模块稳定性指标分析 145 | 146 | ## 插件开发 147 | 148 | 该工具原理是通过解析 AST 收集了相关依赖信息,理论上用户同样可以在这个过程中收集到自己想要的任何信息。所以提供了插件的方式,暴露出各个阶段的生命周期,允许用户在生命周期函数中执行任何逻辑。 149 | 150 | ### 示例:一个项目内使用到的变量名收集插件 151 | 152 | ```js 153 | const myCustomPlugin = { 154 | name: "MyCustomPlugin", 155 | // 输出信息 156 | output: { 157 | data: [], 158 | file: "test.json", 159 | }, 160 | // 解析 script 时执行 161 | ScriptParser({ file, content }) { 162 | const self = this; 163 | return { 164 | VariableDeclarator(tPath) { 165 | tPath.node.id && self.output.data.push(tPath.node.id.name); 166 | }, 167 | }; 168 | }, 169 | // 解析完 script 时执行 170 | AfterScriptParser() {}, 171 | }; 172 | 173 | module.exports = { 174 | plugins: [myCustomPlugin], 175 | }; 176 | ``` 177 | 178 | 自定义生成数据,默认访问地址 'http://localhost:8087/data/test.json' 179 | 180 | ## 邀请 181 | 182 | 秉承整洁代码意志,希望更多的人加入到这个项目中,目标是构建一个能帮助所有前端程序员重构/整洁代码的辅助工具。 183 | -------------------------------------------------------------------------------- /packages/server/README_EN.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chennlang/js-analyzer/04bcc3c0a0f4c32fe0ba20f054e7c56ca7b4a061/packages/server/README_EN.md -------------------------------------------------------------------------------- /packages/server/bin/service.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const server = require("../src/index"); 4 | const path = require("path"); 5 | const AnalyPluginNames = require("../plugins/analy-plugin-names"); 6 | const { program } = require("commander"); 7 | 8 | // 默认配置 9 | const defaultConfig = { 10 | root: "", 11 | ignore: ["**/node_modules/**", "**/dist/**"], 12 | extensions: [".js", ".ts", ".tsx", ".vue", ".json", "jsx"], 13 | alias: { 14 | "@@/": "/", 15 | "~~/": "/", 16 | "@/": "/src/", 17 | "~/": "/src/", 18 | }, 19 | outputPath: path.resolve(__dirname, "../public/data"), 20 | server: { 21 | port: 8088, 22 | host: "localhost", 23 | openBrowser: false, 24 | }, 25 | plugins: [AnalyPluginNames], 26 | ide: "code" 27 | }; 28 | 29 | program 30 | .version(`${require("../package").version}`, "-v, --version") 31 | .option("-c, --config [file]", "config file path") 32 | .option("-r, --root [file]", "root path"); 33 | 34 | program.parse(process.argv); 35 | 36 | function isObject(v) { 37 | return Object.prototype.toString.call(v) === "[object Object]"; 38 | } 39 | 40 | function isArray(v) { 41 | return Object.prototype.toString.call(v) === "[object Array]"; 42 | } 43 | 44 | function mergeConfig(config, target) { 45 | const appendAttrs = ["ignore", "plugins", "server"]; 46 | const res = {}; 47 | Object.keys(config).forEach((attr) => { 48 | // replace attrs 49 | if (!appendAttrs.includes(attr)) { 50 | res[attr] = target[attr] || config[attr]; 51 | } else { 52 | // need merger 53 | if (isObject(config[attr]) && isObject(target[attr])) { 54 | res[attr] = Object.assign(config[attr], target[attr]); 55 | } else if (isArray(config[attr]) && isArray(target[attr])) { 56 | res[attr] = config[attr].concat(target[attr]); 57 | } else { 58 | res[attr] = target[attr] || config[attr]; 59 | } 60 | } 61 | }); 62 | return res; 63 | } 64 | 65 | let config = defaultConfig; 66 | if (program._optionValues.config) { 67 | const configPath = program._optionValues.config; 68 | config = mergeConfig( 69 | config, 70 | require(path.resolve(process.cwd(), configPath)) 71 | ); 72 | } 73 | if (program._optionValues.root) { 74 | config.root = path.resolve(process.cwd(), program._optionValues.root); 75 | } 76 | 77 | server.start(config); 78 | -------------------------------------------------------------------------------- /packages/server/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // root: "/Users/ll/Desktop/work/deepexi/fastdataforflink_frontend", 3 | root: "/Users/ll/Desktop/work/deepexi/dxp-cdp-data-ui", 4 | // root: '/Users/ll/Desktop/work/deepexi/dxp-cdp-data-ui/packages/page-components/scene-module/daw-scene-detail.vue', 5 | extensions: [".js", ".ts", ".tsx", ".vue", ".json", ".jsx"], 6 | ignore: ["**/node_modules/**", "**/dist/**", "**/static/**"], 7 | server: { 8 | port: 8088, 9 | }, 10 | alias: { 11 | "@@/": "/", 12 | "~~/": "/", 13 | "@/": "/examples/", 14 | "~/": "/examples/", 15 | "@p/": "/packages/", 16 | "~@p/": "/packages/", 17 | }, 18 | plugins: [], 19 | ide: "cursor" 20 | }; 21 | -------------------------------------------------------------------------------- /packages/server/libs/web-dist/assets/iconfont-4c295c33.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chennlang/js-analyzer/04bcc3c0a0f4c32fe0ba20f054e7c56ca7b4a061/packages/server/libs/web-dist/assets/iconfont-4c295c33.ttf -------------------------------------------------------------------------------- /packages/server/libs/web-dist/assets/iconfont-a30d846e.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chennlang/js-analyzer/04bcc3c0a0f4c32fe0ba20f054e7c56ca7b4a061/packages/server/libs/web-dist/assets/iconfont-a30d846e.woff -------------------------------------------------------------------------------- /packages/server/libs/web-dist/assets/iconfont-b49f17f1.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chennlang/js-analyzer/04bcc3c0a0f4c32fe0ba20f054e7c56ca7b4a061/packages/server/libs/web-dist/assets/iconfont-b49f17f1.woff2 -------------------------------------------------------------------------------- /packages/server/libs/web-dist/assets/index-02d56794.css: -------------------------------------------------------------------------------- 1 | .ui-dialog[data-v-2eedd74e]{display:flex;align-items:center;justify-content:center;position:absolute;left:0;top:0;width:100%;height:100%;background-color:var(--an-bg-light);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.ui-dialog[data-v-2eedd74e]>div[data-v-2eedd74e]{margin-top:-5%;width:600px;background:var(--an-bg);border-radius:8px}:root{--an-c-active: #ff7f50;--an-c-active-light: #e3d6d2;--an-c-normal: #606266;--an-c-black: #1a1a1a;--an-c-gray: #f0f2f7;--an-c-white: #ffffff;--an-c-light: #606266;--an-bg: #fff;--an-bg-light: rgba(0, 0, 0, .5);--an-bg-gray: #f6f6f6;--an-active-bg: rgba(248, 140, 140, .1)}.theme-dark{--an-c-active: #ff7f50;--an-c-active-light: #54504e;--an-c-normal: #fff;--an-c-black: #1a1a1a;--an-c-gray: #2c323d;--an-bg: #242424;--an-c-white: #ffffff;--an-c-light: #5d636b;--an-bg-light: rgba(255, 255, 255, .3);--an-bg-gray: #f6f6f6;--an-active-bg: rgba(248, 140, 140, .1)}*{margin:0;padding:0;box-sizing:border-box}* ::-webkit-scrollbar{-webkit-transition:opacity .3s;transition:opacity .3s;width:4px;height:10px}* ::-webkit-scrollbar-track{background-color:transparent}* ::-webkit-scrollbar-thumb{width:4px;background-color:#f88c8c1a;border-radius:4px}li{list-style:none}html,body,#app{width:100%;height:100%;overflow:hidden;color:var(--an-text-normal);background:var(--an-bg)}button:disabled,button[disabled]{cursor:not-allowed;background-color:#ccc;color:#666}.ui-input{-webkit-appearance:none;background-color:var(--an-bg);background-image:none;border-radius:4px;border:1px solid var(--an-c-light);box-sizing:border-box;color:var(--an-c-light);display:inline-block;font-size:inherit;height:28px;line-height:28px;outline:none;padding:0 15px;transition:border-color .2s cubic-bezier(.645,.045,.355,1);width:100%;font-size:12px}.ui-textarea{display:block;resize:vertical;padding:5px 15px;line-height:1.5;box-sizing:border-box;width:100%;font-size:inherit;color:var(--an-c-light);background-color:var(--an-bg);background-image:none;border:1px solid var(--an-c-light);border-radius:4px;transition:border-color .2s cubic-bezier(.645,.045,.355,1)}.tree-container{position:relative;white-space:nowrap;height:24px;overflow:hidden;color:var(--an-c-light);cursor:pointer}.tree-container.active{color:var(--an-c-active)}.tree-container.active>.tree-container-label{background-color:var(--an-active-bg)}.tree-container-label{position:relative}.tree-container-label:hover{color:var(--an-c-active);background-color:var(--an-active-bg)}.tree-container-label .tree-container-triangle{position:absolute;left:0px;top:8px;border:4px solid transparent;border-left-color:#c0c4cc;width:0;height:0;transition:transform .3s;transform:rotate(0);transform-origin:}.tree-container.open{height:auto}.tree-container.open>.tree-container-label>.tree-container-triangle{top:10px;transform:rotate(90deg)}.tree-node{display:flex;justify-content:space-between;align-items:center;padding-left:10px;cursor:pointer;color:var(--an-c-light)}.tree-node.active,.tree-node:hover{color:var(--an-c-active);background-color:var(--an-active-bg)}.tree-node .tree-node-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tree-node .tree-node-extend{color:#ccc;font-size:14px}#codeContainer .hljs{color:#999;background:var(--an-bg)}.theme-dark .hljs-subst{color:#c1666d}.file-detail-drawer[data-v-a78892f7]{position:absolute;right:0;top:0;width:100%;height:100%;background-color:#00000080;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:999}.file-detail-drawer>div[data-v-a78892f7]{margin-top:160px;width:100%;height:calc(100vh - 160px);color:var(--an-c-normal);background:var(--an-bg);border-top-left-radius:25px;border-top-right-radius:25px}.ui-drawer{position:absolute;right:0;top:0;width:100%;height:100%;background-color:#00000080;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:999}.ui-drawer>div{width:600px;float:right;height:100%;background:#fff}.dir-list{position:relative;min-width:240px;width:240px}.dir-list .move-line{position:absolute;right:0;top:0;width:2px;height:100%;-webkit-user-select:none;-moz-user-select:none;user-select:none;background:var(--an-c-active-light)}.dir-list .move-line:hover{width:6px;background:var(--an-c-active);cursor:col-resize}.dir-list .move-line:hover:before{left:-17px;border-top:10px solid var(--an-c-active)}.dir-list .move-line:before{content:"";position:absolute;top:0;left:-19px;border-top:10px solid var(--an-c-active-light);border-left:10px solid transparent;border-right:10px solid transparent;transform:translate(50%)}.dir-list .move-line.active{width:6px;background:var(--an-c-active);cursor:col-resize}.dir-list .move-line.active:before{left:-17px;border-top:10px solid var(--an-c-active)}.chart-container{width:calc(100% - 240px)}@font-face{font-family:Finger Paint;src:url(https://fonts.googleapis.com/css?family=Finger+Paint)}/*! @import *//*! tailwindcss v2.2.19 | MIT License | https://tailwindcss.com *//*! modern-normalize v1.1.0 | MIT License | https://github.com/sindresorhus/modern-normalize */*,:before,:after{box-sizing:border-box}html{-moz-tab-size:4;-o-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"}hr{height:0;color:inherit}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}button,[type=button],[type=submit]{-webkit-appearance:button}::-moz-focus-inner{border-style:none;padding:0}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}button{background-color:transparent;background-image:none}fieldset{margin:0;padding:0}ol,ul{list-style:none;margin:0;padding:0}html{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";line-height:1.5}body{font-family:inherit;line-height:inherit}*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:currentColor}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}pre,code,kbd,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}*,:before,:after{--tw-border-opacity: 1;border-color:rgba(229,231,235,var(--tw-border-opacity))}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.absolute{position:absolute}.relative{position:relative}.top-0{top:0px}.top-4{top:1rem}.bottom-0{bottom:0px}.bottom-8{bottom:2rem}.left-0{left:0px}.z-10{z-index:10}.float-right{float:right}.float-left{float:left}.clear-both{clear:both}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-10{margin-top:2.5rem}.mr-4{margin-right:1rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-10{height:2.5rem}.h-full{height:100%}.w-4{width:1rem}.w-10{width:2.5rem}.w-40{width:10rem}.w-full{width:100%}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.table-fixed{table-layout:fixed}.transform{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{75%,to{transform:scale(2);opacity:0}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{transform:translateY(-25%);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,.2,1)}}.cursor-pointer{cursor:pointer}.resize{resize:both}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.self-start{align-self:flex-start}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.break-all{word-break:break-all}.rounded-sm{border-radius:.125rem}.rounded{border-radius:.25rem}.rounded-md{border-radius:.375rem}.rounded-lg{border-radius:.5rem}.border{border-width:1px}.border-t{border-top-width:1px}.border-r{border-right-width:1px}.border-b{border-bottom-width:1px}.border-solid{border-style:solid}.border-dashed{border-style:dashed}.border-gray{border-color:var(--an-c-gray)}.border-active{border-color:var(--an-c-active)}.bg-gray{background-color:var(--an-c-gray)}.bg-active,.hover\:bg-active:hover{background-color:var(--an-active-bg)}.p-2{padding:.5rem}.p-4{padding:1rem}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pt-2{padding-top:.5rem}.pt-12{padding-top:3rem}.pb-10{padding-bottom:2.5rem}.text-left{text-align:left}.text-center{text-align:center}.align-top{vertical-align:top}.text-xs{font-size:.75rem;line-height:1rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-base{font-size:1rem;line-height:1.5rem}.font-medium{font-weight:500}.font-bold{font-weight:700}.leading-5{line-height:1.25rem}.leading-6{line-height:1.5rem}.leading-7{line-height:1.75rem}.leading-8{line-height:2rem}.leading-10{line-height:2.5rem}.text-active{color:var(--an-c-active)}.text-normal{color:var(--an-c-normal)}.text-light{color:var(--an-c-light)}.hover\:text-active:hover{color:var(--an-c-active)}.opacity-50{opacity:.5}*,:before,:after{--tw-shadow: 0 0 #0000}.hover\:shadow-lg:hover{--tw-shadow: 0 10px 15px -3px rgba(0, 0, 0, .1), 0 4px 6px -2px rgba(0, 0, 0, .05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}*,:before,:after{--tw-ring-inset: var(--tw-empty, );--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgba(59, 130, 246, .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000}.filter{--tw-blur: var(--tw-empty, );--tw-brightness: var(--tw-empty, );--tw-contrast: var(--tw-empty, );--tw-grayscale: var(--tw-empty, );--tw-hue-rotate: var(--tw-empty, );--tw-invert: var(--tw-empty, );--tw-saturate: var(--tw-empty, );--tw-sepia: var(--tw-empty, );--tw-drop-shadow: var(--tw-empty, );filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.blur{--tw-blur: blur(8px)}.backdrop-filter{--tw-backdrop-blur: var(--tw-empty, );--tw-backdrop-brightness: var(--tw-empty, );--tw-backdrop-contrast: var(--tw-empty, );--tw-backdrop-grayscale: var(--tw-empty, );--tw-backdrop-hue-rotate: var(--tw-empty, );--tw-backdrop-invert: var(--tw-empty, );--tw-backdrop-opacity: var(--tw-empty, );--tw-backdrop-saturate: var(--tw-empty, );--tw-backdrop-sepia: var(--tw-empty, );-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}@font-face{font-family:iconfont;src:url(./iconfont-b49f17f1.woff2?t=1706499692134) format("woff2"),url(./iconfont-a30d846e.woff?t=1706499692134) format("woff"),url(./iconfont-4c295c33.ttf?t=1706499692134) format("truetype")}.iconfont{font-family:iconfont!important;font-size:16px;font-style:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-wenzi:before{content:""}.icon-preview:before{content:""}.icon-10json:before{content:""}.icon-guanxi:before{content:""}.icon-close:before{content:""}.icon-roundclosefill:before{content:""}.icon-add:before{content:""}.icon-settings-fill:before{content:""}.icon-wenjianxinxi:before{content:""}.icon-huanyuanhuabu:before{content:""}.icon-reset:before{content:""}.icon-relation-full:before{content:""}.icon-relation:before{content:""}.icon-dark:before{content:""}.icon-baitianmoshi:before{content:""}.icon-hot:before{content:""}.icon-info:before{content:""}.icon-packages:before{content:""}.icon-drxx06:before{content:""}.icon-guanxitu:before{content:""}.icon-menu-unuse:before{content:""}.icon-menu-package:before{content:""}.icon-icon-open:before{content:""}pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*! 2 | Theme: StackOverflow Light 3 | Description: Light theme as used on stackoverflow.com 4 | Author: stackoverflow.com 5 | Maintainer: @Hirse 6 | Website: https://github.com/StackExchange/Stacks 7 | License: MIT 8 | Updated: 2021-05-15 9 | 10 | Updated for @stackoverflow/stacks v0.64.0 11 | Code Blocks: /blob/v0.64.0/lib/css/components/_stacks-code-blocks.less 12 | Colors: /blob/v0.64.0/lib/css/exports/_stacks-constants-colors.less 13 | */.hljs{color:#2f3337;background:#f6f6f6}.hljs-subst{color:#2f3337}.hljs-comment{color:#656e77}.hljs-keyword,.hljs-selector-tag,.hljs-meta .hljs-keyword,.hljs-doctag,.hljs-section,.hljs-attr{color:#015692}.hljs-attribute{color:#803378}.hljs-name,.hljs-type,.hljs-number,.hljs-selector-id,.hljs-quote,.hljs-template-tag{color:#b75501}.hljs-selector-class{color:#015692}.hljs-string,.hljs-regexp,.hljs-symbol,.hljs-variable,.hljs-template-variable,.hljs-link,.hljs-selector-attr{color:#54790d}.hljs-meta,.hljs-selector-pseudo{color:#015692}.hljs-built_in,.hljs-title,.hljs-literal{color:#b75501}.hljs-bullet,.hljs-code{color:#535a60}.hljs-meta .hljs-string{color:#54790d}.hljs-deletion{color:#c02d2e}.hljs-addition{color:#2f6f44}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700} 14 | -------------------------------------------------------------------------------- /packages/server/libs/web-dist/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chennlang/js-analyzer/04bcc3c0a0f4c32fe0ba20f054e7c56ca7b4a061/packages/server/libs/web-dist/favicon.png -------------------------------------------------------------------------------- /packages/server/libs/web-dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | JsAnalyzer | 依赖分析工具 8 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-analyzer/server", 3 | "version": "2.8.0", 4 | "main": "src/index.js", 5 | "author": "chennlang", 6 | "description": "一个可视化可交互的 Web 文件依赖分析工具", 7 | "keywords": [ 8 | "utils", 9 | "analyzer", 10 | "js", 11 | "management", 12 | "dependencies" 13 | ], 14 | "license": "MIT", 15 | "scripts": { 16 | "dev": "node ./bin/service.js --config ./config.js" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/chennlang/js-analyzer/issues" 20 | }, 21 | "homepage": "https://github.com/chennlang/js-analyzer#readme", 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "bin": { 26 | "js-analyzer": "bin/service.js" 27 | }, 28 | "files": [ 29 | "bin", 30 | "libs", 31 | "public", 32 | "src", 33 | "plugins" 34 | ], 35 | "dependencies": { 36 | "@js-analyzer/core": "*", 37 | "art-template": "^4.13.2", 38 | "commander": "^9.3.0", 39 | "koa": "^2.13.4", 40 | "koa-body": "^6.0.1", 41 | "koa-router": "^10.1.1", 42 | "koa-static": "^5.0.0", 43 | "koa2-cors": "^2.0.6", 44 | "launch-editor": "^2.3.0", 45 | "open": "^8.4.0", 46 | "portfinder": "^1.0.32" 47 | }, 48 | "devDependencies": { 49 | "@semantic-release/changelog": "^6.0.3", 50 | "@semantic-release/git": "^10.0.1", 51 | "@semantic-release/github": "^10.0.3", 52 | "@semantic-release/npm": "^12.0.0", 53 | "@semantic-release/release-notes-generator": "^13.0.0", 54 | "semantic-release": "^23.0.8", 55 | "tslib": "^2.4.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/server/plugins/analy-plugin-names.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'AnalyPluginNames', 3 | output: { 4 | data: [], 5 | map: {}, 6 | file: 'names.json' 7 | }, 8 | ScriptParser ({ file, content }) { 9 | const _self = this 10 | function set (name) { 11 | if (!name) return 12 | if (_self.output.map[name]) { 13 | _self.output.map[name] += 1 14 | } else { 15 | _self.output.map[name] = 1 16 | } 17 | } 18 | return { 19 | VariableDeclarator (tPath) { 20 | const { node } = tPath 21 | set(node.id ? node.id.name : '') 22 | }, 23 | FunctionDeclaration (tPath) { 24 | const { node } = tPath 25 | set(node.id ? node.id.name : '') 26 | }, 27 | Identifier (tPath) { 28 | if (!file.endsWith('.vue')) return 29 | 30 | const vueOptions = [ 31 | 'computed', 32 | 'methods', 33 | ] 34 | if (vueOptions.includes(tPath.node.name)) { 35 | try { 36 | tPath.parent.value.properties.forEach(node => { 37 | set(node.key.name) 38 | }) 39 | } catch (error) { 40 | // 暂不处理异常 41 | } 42 | 43 | } 44 | } 45 | } 46 | }, 47 | AfterScriptParser () { 48 | this.output.data = Object.keys(this.output.map) 49 | .map(name => ([name, this.output.map[name]])) 50 | } 51 | } -------------------------------------------------------------------------------- /packages/server/public/data/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": "For testing, don't delete" 3 | } -------------------------------------------------------------------------------- /packages/server/src/index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const Koa = require("koa"); 4 | const router = require("koa-router")(); 5 | const open = require("open"); 6 | const koaStatic = require("koa-static"); 7 | const cors = require("koa2-cors"); 8 | const launch = require("launch-editor"); 9 | const template = require("art-template"); 10 | const portfinder = require("portfinder"); 11 | const { koaBody } = require("koa-body"); 12 | const { JsAnalyzer } = require("@js-analyzer/core"); 13 | 14 | const app = new Koa(); 15 | app.use(cors()); 16 | app.use(koaBody()); 17 | app.use(koaStatic(path.join(__dirname, "../public/"))); 18 | app.use(koaStatic(path.join(__dirname, "../libs/web-dist"))); 19 | 20 | // index.html 21 | router.get("/", async (ctx) => { 22 | ctx.set("Content-Type", "text/html;charset=UTF-8"); 23 | const content = template( 24 | path.resolve(__dirname, "../libs/web-dist/index.html"), 25 | { 26 | TITLE: "JsAnalyzer | 依赖分析工具", 27 | ROOT: ctx.config.root, 28 | } 29 | ); 30 | ctx.body = content; 31 | }); 32 | 33 | router.get("/config", (ctx) => { 34 | ctx.body = ctx.config; 35 | }); 36 | 37 | router.put("/config", async (ctx) => { 38 | if (!ctx.request.body) { 39 | ctx.status = 500; 40 | ctx.body = "config error or null"; 41 | return; 42 | } 43 | 44 | app.context.config = ctx.request.body; 45 | const instance = new JsAnalyzer(ctx.request.body); 46 | await instance 47 | .init() 48 | .then(() => { 49 | ctx.body = "ok"; 50 | }) 51 | .catch((error) => { 52 | console.log(error); 53 | ctx.status = 500; 54 | ctx.body = error.toString(); 55 | }); 56 | }); 57 | 58 | // open file in editor 59 | router.get("/launch", async (ctx) => { 60 | const file = ctx.query.file; 61 | launch(file, app.context.config.ide, (name, error) => { 62 | ctx.body = error; 63 | }); 64 | ctx.body = "ok"; 65 | }); 66 | 67 | router.get("/code", async (ctx) => { 68 | ctx.set("Content-Type", "text/text;charset=UTF-8"); 69 | const file = ctx.query.file; 70 | try { 71 | const data = fs.readFileSync(file, "utf-8"); 72 | ctx.body = data; 73 | } catch (error) { 74 | ctx.body = error; 75 | } 76 | }); 77 | 78 | app.use(router.routes()).use(router.allowedMethods()); 79 | 80 | function startListen(config) { 81 | portfinder.setBasePort(config.server.port); 82 | portfinder.getPort({ port: 8000, stopPort: 9000 }, function (err, port) { 83 | if (err) { 84 | console.log(err); 85 | } else { 86 | app.listen(port); 87 | const url = `http://${config.server.host}:${port}`; 88 | config.server.openBrowser && open(url); 89 | console.log("\033[32m Service started: \033[0m" + url); 90 | } 91 | }); 92 | } 93 | 94 | function startServer(c) { 95 | const config = { 96 | ...c, 97 | server: { 98 | port: 8666, 99 | host: "localhost", 100 | openBrowser: true, 101 | ...(c.server || {}), 102 | }, 103 | }; 104 | 105 | app.context.config = config; 106 | const instance = new JsAnalyzer(config); 107 | console.log("\033[32m Generating dependency information... \033[0m"); 108 | instance.init().then(() => { 109 | startListen(config); 110 | }); 111 | } 112 | 113 | module.exports = { 114 | start: startServer, 115 | }; 116 | -------------------------------------------------------------------------------- /packages/web/.env: -------------------------------------------------------------------------------- 1 | VITE_API_PROXY=http://localhost:8088 2 | TITLE='JsAnalyzer | 依赖分析工具' -------------------------------------------------------------------------------- /packages/web/.env.demo: -------------------------------------------------------------------------------- 1 | TITLE='JsAnalyzer | 依赖分析工具 Demo' 2 | VITE_HAS_API_PATH_PREFIX=true # 通常是部署后域名后面有网关路径的时候需要开启 -------------------------------------------------------------------------------- /packages/web/.env.development: -------------------------------------------------------------------------------- 1 | VITE_API_PROXY=http://localhost:8000 2 | TITLE='JsAnalyzer | 依赖分析工具 Dev' -------------------------------------------------------------------------------- /packages/web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'vue-eslint-parser', 3 | parserOptions: { 4 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 5 | ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features 6 | sourceType: 'module', // Allows for the use of imports 7 | ecmaFeatures: { 8 | // tsx: true, // Allows for the parsing of JSX 9 | jsx: true, 10 | }, 11 | }, 12 | // settings: { 13 | // tsx: { 14 | // version: "detect" // Tells eslint-plugin-react to automatically detect the version of React to use 15 | // } 16 | // }, 17 | extends: [ 18 | 'plugin:vue/vue3-recommended', 19 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 20 | 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 21 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 22 | ], 23 | rules: { 24 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 25 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 26 | }, 27 | }; -------------------------------------------------------------------------------- /packages/web/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | -------------------------------------------------------------------------------- /packages/web/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "all", 4 | singleQuote: true, 5 | printWidth: 80, 6 | tabWidth: 2, 7 | endOfLine:"auto" 8 | }; -------------------------------------------------------------------------------- /packages/web/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["johnsoncodehk.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/web/README.md: -------------------------------------------------------------------------------- 1 | # JS-Analyzer Web 2 | 3 | ## 项目概述 4 | 5 | JS-Analyzer 是一个专业的 JavaScript 代码分析工具,用于可视化展示和分析 JavaScript 项目的依赖关系、导入导出情况以及代码结构。该工具能帮助开发人员更好地理解复杂 JavaScript 项目的架构,优化代码结构,提高代码质量。 6 | 7 | ## 功能介绍 8 | 9 | ### 1. 代码关系图(Chart) 10 | - **文件依赖关系可视化**:直观展示项目中文件之间的导入导出关系 11 | - **文件夹结构视图**:展示项目的文件夹结构和组织方式 12 | - **交互式节点**:点击节点查看详细信息,双击切换视图 13 | - **文件详情展示**:展示文件的基本信息、被引用次数及导出变量使用情况 14 | 15 | ### 2. 包管理(Packages) 16 | - **包依赖关系分析**:分析并展示项目中使用的第三方包 17 | - **包使用频率统计**:统计每个包的引用次数和使用情况 18 | - **包引用详情**:查看包在项目中的具体使用位置和方式 19 | 20 | ### 3. 热词分析(Words) 21 | - **代码热词统计**:统计并展示项目中频繁使用的关键词 22 | - **词云可视化**:以词云形式展示代码中的热词分布 23 | - **代码规范指导**:通过热词分析辅助代码规范和命名规范的建立 24 | 25 | ### 4. 隐式引用分析(Unknowns) 26 | - **隐式依赖检测**:识别项目中的隐式依赖和潜在问题 27 | - **未使用引用分析**:发现项目中未使用的导入,帮助清理冗余代码 28 | 29 | ## 核心技术 30 | 31 | ### 前端技术栈 32 | - **框架**:Vue 3 + TypeScript 33 | - **构建工具**:Vite 34 | - **UI组件**:自定义组件 + Vue JSON Pretty 35 | - **样式**:Less + TailwindCSS 36 | - **可视化**:ECharts(图表可视化)+ WordCloud(词云展示) 37 | - **状态管理**:Vue Composition API 38 | - **路由**:Vue Router 39 | - **代码高亮**:Highlight.js 40 | 41 | ### 后端/核心技术 42 | - **代码分析引擎**:@js-analyzer/core 43 | - **支持多语言**:内置多语言切换功能 44 | - **主题切换**:支持明暗主题切换 45 | 46 | ## API接口说明 47 | 48 | ### 1. 文件数据接口 49 | 50 | #### 获取所有文件列表 51 | - **请求路径**:`/data/files.json` 52 | - **方法**:GET 53 | - **响应格式**:`string[]` - 项目所有文件路径的字符串数组 54 | 55 | #### 获取导入文件信息 56 | - **请求路径**:`/data/import-files.json` 57 | - **方法**:GET 58 | - **响应格式**:`ImportDeps` 类型 59 | ```typescript 60 | { 61 | [path: string]: { 62 | num: number, // 引用次数 63 | using: UsingItem[] // 使用该文件的详细信息数组 64 | } 65 | } 66 | 67 | // UsingItem 类型定义 68 | interface UsingItem { 69 | source: string, // 导入源 70 | vars: string, // 导入的变量名 71 | fullPath?: string, // 完整文件路径 72 | loc: SourceLocation // 代码位置信息 73 | } 74 | ``` 75 | 76 | #### 获取导出信息 77 | - **请求路径**:`/data/export.json` 78 | - **方法**:GET 79 | - **响应格式**:`ExportDeps` 类型 80 | ```typescript 81 | { 82 | [path: string]: { 83 | [vars: string]: { 84 | num: number, // 变量被使用次数 85 | using: string[] // 使用该变量的文件路径列表 86 | } 87 | } 88 | } 89 | ``` 90 | 91 | #### 获取包导入信息 92 | - **请求路径**:`/data/import-package.json` 93 | - **方法**:GET 94 | - **响应格式**:`ImportDeps` 类型 - 与导入文件信息格式相同,但记录的是第三方包的引用信息 95 | 96 | #### 获取未知引用信息 97 | - **请求路径**:`/data/import-unknown.json` 98 | - **方法**:GET 99 | - **响应格式**:`ImportDeps` 类型 - 与导入文件信息格式相同,但记录的是无法解析的引用 100 | 101 | #### 获取名称列表 102 | - **请求路径**:`/data/names.json` 103 | - **方法**:GET 104 | - **响应格式**:项目中的标识符名称列表 105 | 106 | ### 2. 文件操作接口 107 | 108 | #### 打开文件 109 | - **请求路径**:`/launch/?file=文件路径` 110 | - **方法**:GET 111 | - **响应格式**:JSON对象,包含文件打开状态信息 112 | 113 | #### 获取文件内容 114 | - **请求路径**:`/code/?file=文件路径` 115 | - **方法**:GET 116 | - **响应格式**:文件的原始内容(文本格式) 117 | 118 | ### 3. 配置接口 119 | 120 | #### 获取配置 121 | - **请求路径**:`/config` 122 | - **方法**:GET 123 | - **响应格式**:包含以下字段的配置对象 124 | ```typescript 125 | { 126 | root: string, // 项目根目录 127 | ignore?: (string | RegExp)[], // 忽略的文件/目录 128 | extensions?: string[], // 支持的文件扩展名 129 | alias?: Record, // 路径别名 130 | path?: string, // 自定义路径 131 | outputPath?: string, // 输出路径 132 | plugins?: Plugin[], // 自定义插件 133 | ide?: string // 集成开发环境 134 | } 135 | ``` 136 | 137 | #### 更新配置 138 | - **请求路径**:`/config` 139 | - **方法**:PUT 140 | - **请求体**:与获取配置接口格式相同的配置对象 141 | - **响应格式**:更新后的配置对象 142 | 143 | ## 使用说明 144 | 145 | 1. 启动开发服务器:`npm run dev` 146 | 2. 构建生产版本:`npm run build` 147 | 3. 构建演示版本:`npm run build:demo` 148 | 4. 预览构建结果:`npm run serve` 149 | 150 | ## 系统特色 151 | 152 | 1. **多视图展示**:提供多种视角分析代码结构和依赖关系 153 | 2. **交互式体验**:通过点击、拖拽等交互方式直观操作 154 | 3. **主题定制**:支持明暗两种主题,适应不同使用场景 155 | 4. **多语言支持**:内置多语言切换功能,适应国际化需求 156 | 5. **项目配置管理**:提供配置界面,灵活管理分析项目 157 | 158 | JS-Analyzer 是帮助开发者理解复杂 JavaScript 项目结构,优化代码质量和依赖关系的利器,为大型前端项目的维护和重构提供了强有力的支持。 159 | -------------------------------------------------------------------------------- /packages/web/global.d.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '@js-analyzer/core/types/index'; 2 | declare global { 3 | interface Window { 4 | ROOT: string 5 | CONFIG: Config 6 | } 7 | } -------------------------------------------------------------------------------- /packages/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{TITLE}} 8 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@js-analyzer/web", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vue-tsc --noEmit --skipLibCheck && vite build", 7 | "build:demo": "vue-tsc --noEmit --skipLibCheck && vite build --mode demo", 8 | "serve": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@highlightjs/vue-plugin": "^2.1.0", 12 | "echarts": "^5.2.0", 13 | "highlight.js": "^11.9.0", 14 | "jquery": "^3.6.0", 15 | "mitt": "^3.0.0", 16 | "vue": "3.2.31", 17 | "vue-json-pretty": "^2.1.0", 18 | "vue-router": "4", 19 | "wordcloud": "^1.2.2" 20 | }, 21 | "devDependencies": { 22 | "@js-analyzer/core": "*", 23 | "@types/jquery": "^3.5.6", 24 | "@types/wordcloud": "^1.2.0", 25 | "@vitejs/plugin-vue": "^4.2.3", 26 | "@vitejs/plugin-vue-jsx": "^1.1.8", 27 | "@vue/compiler-sfc": "^3.2.6", 28 | "autoprefixer": "^10.3.4", 29 | "eslint": "^8.13.0", 30 | "eslint-config-prettier": "^8.5.0", 31 | "eslint-plugin-prettier": "^4.0.0", 32 | "eslint-plugin-vue": "^8.6.0", 33 | "less": "^4.1.1", 34 | "postcss": "^8.3.6", 35 | "prettier": "^2.6.2", 36 | "tailwindcss": "^2.2.15", 37 | "typescript": "^4.3.2", 38 | "vite": "^4.4.2", 39 | "vue-tsc": "^1.4.4" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/web/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chennlang/js-analyzer/04bcc3c0a0f4c32fe0ba20f054e7c56ca7b4a061/packages/web/public/favicon.png -------------------------------------------------------------------------------- /packages/web/src/App.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 121 | 122 | 239 | -------------------------------------------------------------------------------- /packages/web/src/api/remote-data.ts: -------------------------------------------------------------------------------- 1 | import $ from 'jquery' 2 | import { MaterialPackage } from '@js-analyzer/core/types/index'; 3 | 4 | // api base 5 | const BASE_URL = import.meta.env.DEV 6 | ? import.meta.env.VITE_API_PROXY 7 | : location.origin + (import.meta.env.VITE_HAS_API_PATH_PREFIX ? location.pathname : '') 8 | 9 | console.log('BASE_URLBASE_URL:', BASE_URL) 10 | function loadJson (url: string, cacheKey?: string) { 11 | // has cache 12 | if (cacheKey && state[cacheKey]) { 13 | return Promise.resolve(state[cacheKey]) 14 | } 15 | 16 | // has sync result 17 | if (cacheKey && promiseState[cacheKey] !== undefined) { 18 | return promiseState[cacheKey] 19 | } 20 | 21 | const syncResult = new Promise((resolve, reject) => { 22 | $.getJSON(BASE_URL + url, (res: any) => { 23 | if (cacheKey) { 24 | state[cacheKey] = res 25 | // clear sync result 26 | delete promiseState[cacheKey] 27 | } 28 | resolve(res) 29 | }, err => reject(err)) 30 | }) 31 | 32 | // cache sync result 33 | if (cacheKey) { 34 | promiseState[cacheKey] = syncResult 35 | } 36 | 37 | return syncResult 38 | } 39 | 40 | function load (url: string) { 41 | return new Promise((resolve, reject) => { 42 | $.get(BASE_URL + url, {}, (res: any) => { 43 | resolve(res) 44 | }) 45 | }) 46 | } 47 | 48 | function request (method: string, url: string, data: any) { 49 | return new Promise((resolve, reject) => { 50 | $.ajax({ 51 | url: BASE_URL + url, 52 | method, 53 | data: JSON.stringify(data), 54 | dataType: 'json', 55 | success: resolve, 56 | error: reject, 57 | contentType: 'application/json; charset=utf-8', 58 | }) 59 | }) 60 | } 61 | 62 | 63 | type IState = Record 64 | 65 | type JsonResponse = Promise 66 | 67 | const state: IState = { 68 | files: null, 69 | import: null, 70 | export: null, 71 | package: null, 72 | unknown: null 73 | } 74 | 75 | const promiseState: Record> = {} 76 | 77 | export const getFiles = () => { 78 | return loadJson('/data/files.json', 'files') 79 | } 80 | 81 | export const getImport = (): JsonResponse => { 82 | return loadJson('/data/import-files.json', 'import') 83 | } 84 | 85 | export const getExport = () : JsonResponse => { 86 | return loadJson('/data/export.json', 'export') 87 | } 88 | 89 | export const getPackage = (): JsonResponse => { 90 | return loadJson('/data/import-package.json', 'package') 91 | } 92 | 93 | export const getUnknown = (): JsonResponse => { 94 | return loadJson('/data/import-unknown.json', 'unknown') 95 | } 96 | 97 | export const getNames = () => { 98 | return loadJson('/data/names.json', 'names') 99 | } 100 | 101 | export const openEditor = (path: string) => { 102 | if (path.startsWith(window.CONFIG.root)) { 103 | return loadJson(`/launch/?file=${path}`) 104 | } else { 105 | return loadJson(`/launch/?file=${window.CONFIG.root}${path}`) 106 | } 107 | } 108 | 109 | export const getFileContent = (path: string) => { 110 | return load(`/code/?file=${path}`) 111 | } 112 | 113 | export const getConfig = () => { 114 | return load(`/config`) 115 | } 116 | 117 | export const updateConfig = (config: typeof window.CONFIG) => { 118 | return request('put', '/config', config) 119 | } -------------------------------------------------------------------------------- /packages/web/src/components/CodePreview/CodePreview.vue: -------------------------------------------------------------------------------- 1 | 65 | 81 | 92 | -------------------------------------------------------------------------------- /packages/web/src/components/CodePreview/use-code-preview.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import CodePreview from './CodePreview.vue' 3 | 4 | export function useCodePreview() { 5 | function openCodePreview(file: string, propsData: any = {}) { 6 | const container = document.getElementById('preview') || document.createElement('div'); 7 | document.body.appendChild(container); 8 | 9 | const remove = () => { 10 | document.body.removeChild(container); 11 | }; 12 | 13 | const vm = createApp(CodePreview, { 14 | show: true, 15 | file, 16 | remove, 17 | ...propsData, 18 | }) 19 | vm.mount(container) 20 | return remove; 21 | }; 22 | return { 23 | openCodePreview, 24 | } 25 | } -------------------------------------------------------------------------------- /packages/web/src/components/Dialog.vue: -------------------------------------------------------------------------------- 1 | 51 | 81 | 82 | 102 | -------------------------------------------------------------------------------- /packages/web/src/components/Drawer.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 40 | 41 | 59 | -------------------------------------------------------------------------------- /packages/web/src/components/InfoDrawer.vue: -------------------------------------------------------------------------------- 1 | 6 | 128 | 195 | 216 | -------------------------------------------------------------------------------- /packages/web/src/components/ProjectManage.vue: -------------------------------------------------------------------------------- 1 | 76 |