├── nx.json ├── packages ├── server │ ├── README_EN.md │ ├── .npmrc │ ├── public │ │ └── data │ │ │ └── test.json │ ├── libs │ │ └── web-dist │ │ │ ├── favicon.png │ │ │ ├── assets │ │ │ ├── iconfont-4c295c33.ttf │ │ │ ├── iconfont-a30d846e.woff │ │ │ ├── iconfont-b49f17f1.woff2 │ │ │ └── index-02d56794.css │ │ │ └── index.html │ ├── config.js │ ├── .releaserc.js │ ├── package.json │ ├── plugins │ │ └── analy-plugin-names.js │ ├── bin │ │ └── service.js │ ├── src │ │ └── index.js │ ├── README.md │ └── CHANGELOG.md ├── core │ ├── .eslintignore │ ├── .gitignore │ ├── .releaserc.js │ ├── test │ │ └── test.less │ ├── src │ │ ├── config.ts │ │ ├── test.js │ │ ├── inject-export-quote-num.ts │ │ ├── html-parser.ts │ │ ├── utils.ts │ │ ├── style-parser.ts │ │ ├── script-parser.ts │ │ └── index.ts │ ├── .eslintrc.js │ ├── logger.ts │ ├── LICENSE │ ├── rollup.config.ts │ ├── tsconfig.json │ ├── types │ │ └── index.d.ts │ └── package.json └── web │ ├── .env │ ├── .gitignore │ ├── .vscode │ └── extensions.json │ ├── .env.development │ ├── .env.demo │ ├── public │ └── favicon.png │ ├── src │ ├── css │ │ ├── index.less │ │ └── iconfont │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ ├── iconfont.woff2 │ │ │ └── iconfont.css │ ├── components │ │ ├── Tree │ │ │ ├── Tree.d.ts │ │ │ └── Tree.vue │ │ ├── SplitLine.vue │ │ ├── z-index.ts │ │ ├── use.ts │ │ ├── CodePreview │ │ │ ├── use-code-preview.ts │ │ │ └── CodePreview.vue │ │ ├── icon-btn.vue │ │ ├── Drawer.vue │ │ ├── Select.vue │ │ ├── Dialog.vue │ │ ├── ProjectManage.vue │ │ └── InfoDrawer.vue │ ├── views │ │ ├── chart │ │ │ ├── event.ts │ │ │ ├── History.vue │ │ │ ├── Aside.vue │ │ │ ├── Echart.vue │ │ │ └── index.vue │ │ ├── words │ │ │ └── index.vue │ │ ├── packages │ │ │ ├── index.vue │ │ │ └── echart.ts │ │ └── unknowns │ │ │ ├── echart.ts │ │ │ └── index.vue │ ├── env.d.ts │ ├── utils │ │ ├── uuid.ts │ │ └── path2tree.ts │ ├── types │ │ ├── index.ts │ │ └── chart.ts │ ├── main.ts │ ├── router.ts │ ├── api │ │ └── remote-data.ts │ ├── language.ts │ └── App.vue │ ├── postcss.config.js │ ├── global.d.ts │ ├── .prettierrc.js │ ├── index.html │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── package.json │ ├── .eslintrc.js │ ├── vite.config.ts │ └── README.md ├── .npmrc ├── pnpm-workspace.yaml ├── .vscode └── settings.json ├── .gitignore ├── CHANGELOG.md ├── lerna.json ├── package.json ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── release-npm.yml │ ├── release.yml │ └── release-demo-pages.yml ├── DEVELOP.md ├── .cursor └── rules │ ├── structure-web.mdc │ └── structure.mdc ├── README_zh.md ├── README_jp.md ├── scripts └── dev.js └── README.md /nx.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/server/README_EN.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | workspaces = true 2 | workspaces-update = false -------------------------------------------------------------------------------- /packages/core/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | dist 3 | types/** -------------------------------------------------------------------------------- /packages/server/.npmrc: -------------------------------------------------------------------------------- 1 | workspaces = true 2 | workspaces-update = false -------------------------------------------------------------------------------- /packages/core/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | -------------------------------------------------------------------------------- /packages/server/public/data/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": "For testing, don't delete" 3 | } -------------------------------------------------------------------------------- /packages/web/.env: -------------------------------------------------------------------------------- 1 | VITE_API_PROXY=http://localhost:8088 2 | TITLE='JsAnalyzer | 依赖分析工具' -------------------------------------------------------------------------------- /packages/web/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | -------------------------------------------------------------------------------- /packages/web/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["johnsoncodehk.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/web/.env.development: -------------------------------------------------------------------------------- 1 | VITE_API_PROXY=http://localhost:8000 2 | TITLE='JsAnalyzer | 依赖分析工具 Dev' -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/core" 3 | - "packages/server" 4 | - "packages/web" 5 | -------------------------------------------------------------------------------- /packages/web/.env.demo: -------------------------------------------------------------------------------- 1 | TITLE='JsAnalyzer | 依赖分析工具 Demo' 2 | VITE_HAS_API_PATH_PREFIX=true # 通常是部署后域名后面有网关路径的时候需要开启 -------------------------------------------------------------------------------- /packages/web/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chennlang/js-analyzer/HEAD/packages/web/public/favicon.png -------------------------------------------------------------------------------- /packages/web/src/css/index.less: -------------------------------------------------------------------------------- 1 | /*! @import */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | -------------------------------------------------------------------------------- /packages/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/server/libs/web-dist/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chennlang/js-analyzer/HEAD/packages/server/libs/web-dist/favicon.png -------------------------------------------------------------------------------- /packages/web/src/css/iconfont/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chennlang/js-analyzer/HEAD/packages/web/src/css/iconfont/iconfont.ttf -------------------------------------------------------------------------------- /packages/web/src/css/iconfont/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chennlang/js-analyzer/HEAD/packages/web/src/css/iconfont/iconfont.woff -------------------------------------------------------------------------------- /packages/web/src/css/iconfont/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chennlang/js-analyzer/HEAD/packages/web/src/css/iconfont/iconfont.woff2 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll": "never", 4 | "source.fixAll.eslint": "never" 5 | } 6 | } -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /packages/server/libs/web-dist/assets/iconfont-4c295c33.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chennlang/js-analyzer/HEAD/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/HEAD/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/HEAD/packages/server/libs/web-dist/assets/iconfont-b49f17f1.woff2 -------------------------------------------------------------------------------- /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/.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/src/components/Tree/Tree.d.ts: -------------------------------------------------------------------------------- 1 | export interface INode { 2 | label: string; 3 | path: string; 4 | value?: string; 5 | children?: INode[]; 6 | } 7 | 8 | export interface TNode { 9 | data: INode; 10 | isLeaf: boolean; 11 | } -------------------------------------------------------------------------------- /packages/web/src/views/chart/event.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt' 2 | import { CHART_VIEW_TYPE } from './echart' 3 | 4 | type Events = { 5 | viewChange: CHART_VIEW_TYPE 6 | dataChange: { 7 | nodes: any, 8 | links: any, 9 | } 10 | } 11 | 12 | export const chartEmitter = mitt() 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/web/src/components/SplitLine.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 15 | -------------------------------------------------------------------------------- /packages/web/src/components/z-index.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | 3 | let index = 999 4 | 5 | export function useIndex () { 6 | const zIndex = ref(index) 7 | 8 | function getZIndex() { 9 | index += 1 10 | zIndex.value = index 11 | } 12 | return { 13 | zIndex, 14 | getZIndex, 15 | } 16 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/server/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: "/Users/ll/Desktop/work/deepexi/textin-open-demo", 3 | extensions: [".js", ".ts", ".tsx", ".vue", ".json", ".jsx"], 4 | ignore: ["**/node_modules/**", "**/dist/**", "**/static/**"], 5 | server: { 6 | port: 8088, 7 | }, 8 | alias: { 9 | "@@/": "/src/.umi/", 10 | "@/": "/src/", 11 | }, 12 | plugins: [], 13 | ide: "cursor" 14 | }; 15 | -------------------------------------------------------------------------------- /packages/web/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | 10 | declare global { 11 | interface Window { 12 | ROOT: string 13 | } 14 | } -------------------------------------------------------------------------------- /packages/web/src/utils/uuid.ts: -------------------------------------------------------------------------------- 1 | export function generateUUID() { 2 | let d = new Date().getTime(); 3 | const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( 4 | /[xy]/g, 5 | function (c) { 6 | var r = (d + Math.random() * 16) % 16 | 0; 7 | d = Math.floor(d / 16); 8 | return (c == 'x' ? r : (r & 0x7) | 0x8).toString(16); 9 | }, 10 | ); 11 | return uuid; 12 | } -------------------------------------------------------------------------------- /packages/web/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface Position { 2 | column: number 3 | index: number 4 | line: number 5 | } 6 | export interface IQuoteListItem { 7 | source: string, 8 | vars: string, 9 | fullPath: string, 10 | loc?: { 11 | start: Position 12 | end: Position 13 | } 14 | } 15 | 16 | export interface IQuoteInfo { 17 | num: number, 18 | using: IQuoteListItem [] 19 | } 20 | 21 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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 -------------------------------------------------------------------------------- /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 | "dev": "node scripts/dev.js" 15 | }, 16 | "devDependencies": { 17 | "lerna": "^4.0.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{TITLE}} 8 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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/web/src/main.ts: -------------------------------------------------------------------------------- 1 | import App from './App.vue' 2 | import router from './router' 3 | import { createApp } from 'vue' 4 | import './css/index.less' 5 | import './css/iconfont/iconfont.css' 6 | import IconBtn from './components/icon-btn.vue' 7 | import 'highlight.js/styles/stackoverflow-light.css' 8 | import hljs from 'highlight.js/lib/core'; 9 | import javascript from 'highlight.js/lib/languages/javascript'; 10 | import hljsVuePlugin from "@highlightjs/vue-plugin"; 11 | 12 | const app = createApp(App) 13 | app.use(router) 14 | app.use(hljsVuePlugin) 15 | app.mount('#app') 16 | app.component('IconBtn', IconBtn) -------------------------------------------------------------------------------- /packages/web/src/components/use.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt' 2 | 3 | type Events = { 4 | expand: any, 5 | select: any, 6 | } 7 | type NodeId = string | number | symbol 8 | const emitter = mitt() 9 | const ALL_NODE_ID = Symbol('all') 10 | 11 | 12 | // 展开或关闭 node 13 | function expand (id: NodeId, val: boolean): void { 14 | emitter.emit('expand', { id, val }) 15 | } 16 | 17 | function select (id: NodeId) { 18 | emitter.emit('select', { id }) 19 | } 20 | 21 | const nodeMethods = { 22 | expand, 23 | select, 24 | } 25 | 26 | export { 27 | emitter, 28 | nodeMethods, 29 | ALL_NODE_ID 30 | } -------------------------------------------------------------------------------- /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/server/libs/web-dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | JsAnalyzer | 依赖分析工具 8 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/web/src/types/chart.ts: -------------------------------------------------------------------------------- 1 | 2 | import { IQuoteInfo } from '@/types/index' 3 | import { ImportDepItem } from '@js-analyzer/core/types/index'; 4 | 5 | export interface IChartExtendData extends ImportDepItem{ 6 | fullPath: string, 7 | sortPath: string, 8 | name: string | undefined, 9 | ext: string | undefined, 10 | } 11 | 12 | export interface IChartNode { 13 | id: string, 14 | x?: number, 15 | y?: number, 16 | name?: string, 17 | value: string | number, 18 | symbolSize: number, 19 | category?: number, 20 | itemStyle: Record, 21 | label?: any, 22 | 23 | // not echart option, extent data 24 | extendData: IChartExtendData, 25 | } 26 | 27 | export interface IChartLink { 28 | source: string, 29 | target: string 30 | } -------------------------------------------------------------------------------- /packages/web/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: { 6 | borderColor: { 7 | active: 'var(--an-c-active)', 8 | gray: 'var(--an-c-gray)', 9 | }, 10 | backgroundColor: { 11 | active: 'var(--an-active-bg)', 12 | gray: 'var(--an-c-gray)', 13 | light: 'var(--an-c-light)', 14 | }, 15 | }, 16 | textColor: { 17 | active: 'var(--an-c-active)', 18 | normal: 'var(--an-c-normal)', 19 | white: 'var(--an-c-white)', 20 | gray: 'var(--an-c-gray)', 21 | light: 'var(--an-c-light)', 22 | }, 23 | }, 24 | variants: { 25 | extend: {}, 26 | }, 27 | plugins: [], 28 | }; 29 | -------------------------------------------------------------------------------- /packages/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ES2018", 5 | "module": "ESNext", 6 | "useDefineForClassFields": true, 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "jsx": "preserve", 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "isolatedModules": true, 14 | "lib": ["ESNext", "ESNext.AsyncIterable", "DOM", "ES2018"], 15 | "paths": { 16 | "@/*": ["./src/*"] 17 | } 18 | }, 19 | "types": ["@types/node", "@js-analyzer/core"], 20 | "include": [ 21 | "./src/**/*.ts", 22 | "./src/**/*.d.ts", 23 | "./src/**/*.tsx", 24 | "./src/**/*.vue", 25 | "global.d.ts" 26 | ], 27 | "exclude": ["node_modules/**/*", "**/*.spec.ts"], 28 | } 29 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/web/src/views/words/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 35 | 36 | 42 | -------------------------------------------------------------------------------- /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/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/web/src/components/icon-btn.vue: -------------------------------------------------------------------------------- 1 | 30 | 55 | -------------------------------------------------------------------------------- /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/web/src/components/Drawer.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 40 | 41 | 59 | -------------------------------------------------------------------------------- /packages/web/src/views/chart/History.vue: -------------------------------------------------------------------------------- 1 | 22 | 44 | 45 | -------------------------------------------------------------------------------- /packages/web/src/router.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' 2 | import FileChart from '@/views/chart/index.vue' 3 | import PackageChart from '@/views/packages/index.vue' 4 | import UnknownChart from '@/views/unknowns/index.vue' 5 | import HotWord from '@/views/words/index.vue' 6 | 7 | const routes: RouteRecordRaw [] = [ 8 | { 9 | path: '/', 10 | redirect: '/chart' 11 | }, 12 | { 13 | path: '/chart', 14 | name: 'FileChart', 15 | component: FileChart 16 | }, 17 | { 18 | path: '/packages', 19 | name: 'PackageChart', 20 | component: PackageChart 21 | }, 22 | { 23 | path: '/unknowns', 24 | name: 'UnknownChart', 25 | component: UnknownChart 26 | }, 27 | { 28 | path: '/words', 29 | name: 'HotWord', 30 | component: HotWord 31 | }, 32 | ] 33 | 34 | const router = createRouter({ 35 | routes, 36 | history: createWebHashHistory() 37 | }) 38 | 39 | let historyQueue: any [] = [] 40 | 41 | router.beforeEach((to, from, next) => { 42 | if (to.name === 'FileChart' && to.query.file) { 43 | historyQueue.push(to.query.file) 44 | } else { 45 | historyQueue = [] 46 | } 47 | to.meta.historyQueue = historyQueue 48 | next() 49 | }) 50 | 51 | export default router -------------------------------------------------------------------------------- /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/src/views/packages/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 40 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /packages/web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import vueJsx from '@vitejs/plugin-vue-jsx' 4 | const path = require('path') 5 | 6 | const htmlPlugin = (vs: Record = {}) => { 7 | let config 8 | return { 9 | name: "html-transform", 10 | configResolved(resolvedConfig) { 11 | config = resolvedConfig 12 | }, 13 | transformIndexHtml(html: string) { 14 | // dev 15 | // if (config.command === 'serve') { 16 | if (1) { 17 | return html.replace(/{{(.*?)}}/g, function (match, p1) { 18 | return vs[p1.trim()] 19 | }) 20 | } 21 | return html 22 | 23 | }, 24 | } 25 | } 26 | 27 | // https://vitejs.dev/config/ 28 | export default defineConfig(({ mode }) => { 29 | const env = loadEnv(mode, process.cwd(), '') 30 | return { 31 | optimizeDeps: { 32 | include: ['dagre'] 33 | }, 34 | plugins: [ 35 | vue(), 36 | vueJsx(), 37 | htmlPlugin({ 38 | TITLE: env.TITLE, 39 | }), 40 | ], 41 | server: { 42 | port: 3003 43 | }, 44 | base: './', 45 | build: { 46 | outDir: 'dist', 47 | commonjsOptions: { 48 | include: [/dagre/, /node_modules/], 49 | }, 50 | // sourcemap: 'inline', 51 | // minify: false, 52 | }, 53 | resolve: { 54 | alias: { 55 | '@': path.join(__dirname, "./src"), 56 | } 57 | }, 58 | } 59 | }) -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /DEVELOP.md: -------------------------------------------------------------------------------- 1 | # 开发环境启动指南 2 | 3 | ## 快速启动 4 | 5 | ```bash 6 | pnpm dev 7 | ``` 8 | 9 | ## 启动说明 10 | 11 | 项目现在使用自定义的开发启动脚本,具有以下特性: 12 | 13 | ### 启动顺序 14 | 15 | 1. **Core 包** 首先启动,等待构建完成 16 | 2. **Server 包** 其次启动,等待服务就绪 17 | 3. **Web 包** 最后启动,提供前端界面 18 | 19 | ### 自动重启机制 20 | 21 | - **Core 包**更新时:重启所有服务(core + server + web) 22 | - **Server 包**更新时:仅重启 web 服务 23 | - **Web 包**更新时:由 Vite 的 HMR 处理 24 | 25 | ### 服务地址 26 | 27 | - Web 界面:http://localhost:3003 28 | - Server API:http://localhost:8088 29 | 30 | ### 手动启动单个服务 31 | 32 | 如果需要单独启动某个服务: 33 | 34 | ```bash 35 | # 仅启动 core 36 | pnpm dev:core 37 | 38 | # 仅启动 server 39 | pnpm dev:server 40 | 41 | # 仅启动 web 42 | pnpm dev:web 43 | ``` 44 | 45 | ### 停止服务 46 | 47 | 使用 `Ctrl+C` 停止所有服务,脚本会自动清理所有子进程。 48 | 49 | ## 配置文件说明 50 | 51 | ### packages/server/config.js 52 | 53 | 这是本地启动必须要先设置的配置文件,包含以下重要配置: 54 | 55 | ```javascript 56 | module.exports = { 57 | root: "/Users/ll/Desktop/work/deepexi/textin-open-demo", // 要分析的项目根目录 58 | extensions: [".js", ".ts", ".tsx", ".vue", ".json", ".jsx"], // 支持的文件扩展名 59 | ignore: ["**/node_modules/**", "**/dist/**", "**/static/**"], // 忽略的目录 60 | server: { 61 | port: 8088, // Server 服务端口 62 | }, 63 | alias: { 64 | "@@/": "/src/.umi/", // 路径别名配置 65 | "@/": "/src/", 66 | }, 67 | plugins: [], // 插件配置 68 | ide: "cursor", // 使用的编辑器,支持 vscode、cursor 等 69 | }; 70 | ``` 71 | 72 | **重要:** 在启动开发环境前,请确保: 73 | 74 | 1. 修改 `root` 路径为您要分析的项目的实际路径 75 | 2. 根据需要调整 `extensions` 和 `ignore` 配置 76 | 3. 确认 `server.port` 与其他服务不冲突 77 | 78 | ## 注意事项 79 | 80 | - 确保 `packages/server/public/data/test.json` 文件不会被删除(已修改 core 的清理逻辑) 81 | - 脚本会监听文件变化并自动重启相关服务 82 | - 启动过程中会有彩色日志输出,显示各服务的状态 83 | - 修改 `config.js` 配置后需要重启 server 服务才能生效 84 | -------------------------------------------------------------------------------- /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/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/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/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/web/src/css/iconfont/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "iconfont"; /* Project id 2892018 */ 3 | src: url('iconfont.woff2?t=1706499692134') format('woff2'), 4 | url('iconfont.woff?t=1706499692134') format('woff'), 5 | url('iconfont.ttf?t=1706499692134') format('truetype'); 6 | } 7 | 8 | .iconfont { 9 | font-family: "iconfont" !important; 10 | font-size: 16px; 11 | font-style: normal; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .icon-wenzi:before { 17 | content: "\e632"; 18 | } 19 | 20 | .icon-preview:before { 21 | content: "\e600"; 22 | } 23 | 24 | .icon-10json:before { 25 | content: "\e622"; 26 | } 27 | 28 | .icon-guanxi:before { 29 | content: "\e64e"; 30 | } 31 | 32 | .icon-close:before { 33 | content: "\e62c"; 34 | } 35 | 36 | .icon-roundclosefill:before { 37 | content: "\e658"; 38 | } 39 | 40 | .icon-add:before { 41 | content: "\e661"; 42 | } 43 | 44 | .icon-settings-fill:before { 45 | content: "\e840"; 46 | } 47 | 48 | .icon-wenjianxinxi:before { 49 | content: "\e62b"; 50 | } 51 | 52 | .icon-huanyuanhuabu:before { 53 | content: "\ec13"; 54 | } 55 | 56 | .icon-reset:before { 57 | content: "\e648"; 58 | } 59 | 60 | .icon-relation-full:before { 61 | content: "\e867"; 62 | } 63 | 64 | .icon-relation:before { 65 | content: "\e868"; 66 | } 67 | 68 | .icon-dark:before { 69 | content: "\e72f"; 70 | } 71 | 72 | .icon-baitianmoshi:before { 73 | content: "\e68f"; 74 | } 75 | 76 | .icon-hot:before { 77 | content: "\e64d"; 78 | } 79 | 80 | .icon-info:before { 81 | content: "\e675"; 82 | } 83 | 84 | .icon-packages:before { 85 | content: "\e68e"; 86 | } 87 | 88 | .icon-drxx06:before { 89 | content: "\e717"; 90 | } 91 | 92 | .icon-guanxitu:before { 93 | content: "\e6a8"; 94 | } 95 | 96 | .icon-menu-unuse:before { 97 | content: "\e6d0"; 98 | } 99 | 100 | .icon-menu-package:before { 101 | content: "\e604"; 102 | } 103 | 104 | .icon-icon-open:before { 105 | content: "\e6a4"; 106 | } 107 | 108 | -------------------------------------------------------------------------------- /packages/web/src/views/unknowns/echart.ts: -------------------------------------------------------------------------------- 1 | import * as Echart from 'echarts' 2 | import { getUnknown } from '../../api/remote-data'; 3 | import { ImportDeps, ImportDepItem } from '@js-analyzer/core/types/index'; 4 | import { $tf } from '@/language'; 5 | 6 | let instance: any 7 | 8 | interface INode { 9 | name: string 10 | value: number 11 | extendData: ImportDepItem & { name: string } 12 | } 13 | 14 | const getChartOption = (nodes: INode []): any => { 15 | return { 16 | title: { 17 | text: $tf('隐式引用'), 18 | subtext: $tf('引用源未注册到项目中'), 19 | top: 'center', 20 | left: 'center' 21 | }, 22 | tooltip: {}, 23 | animationDurationUpdate: 1500, 24 | animationEasingUpdate: 'quinticInOut', 25 | series: [ 26 | { 27 | name: 'Les Miserables', 28 | type: 'graph', 29 | layout: 'circular', 30 | circular: { 31 | rotateLabel: true 32 | }, 33 | data: nodes, 34 | links: [], 35 | categories: [], 36 | roam: true, 37 | label: { 38 | show: true, 39 | position: 'right', 40 | formatter: '{b}{@value}' 41 | }, 42 | lineStyle: { 43 | color: 'source', 44 | curveness: 0.3 45 | } 46 | } 47 | ] 48 | } 49 | } 50 | 51 | const createNodes = (res: ImportDeps): INode [] => { 52 | return Object.keys(res).map(key => ({ 53 | name: key.replace(window.CONFIG.root, ''), 54 | value: res[key].num, 55 | extendData: { 56 | ...res[key], 57 | name: key, 58 | } 59 | })) 60 | } 61 | 62 | export function useChart (ref: HTMLElement | null) { 63 | if (!ref) throw 'chart ref is not exist!' 64 | 65 | instance = Echart.init(ref) 66 | getUnknown().then(res => { 67 | const nodes = createNodes(res) 68 | const options = getChartOption(nodes) 69 | instance.setOption(options) 70 | }) 71 | return { 72 | instance, 73 | } 74 | } -------------------------------------------------------------------------------- /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}/**`, `!${outputPath}/test.json`, '!publicDir'], { 42 | force: true 43 | }) 44 | } 45 | } 46 | 47 | /** 48 | * 清理指定文件 49 | * @param {String} outputPath 输出路径 50 | * @param {Array} fileNames 要清理的文件名列表 51 | */ 52 | export async function cleanFiles(outputPath: string, fileNames: string[]) { 53 | if (!fs.existsSync(outputPath)) { 54 | logger.info(`输出路径不存在: ${outputPath}`) 55 | return 56 | } 57 | 58 | const patterns = fileNames.map(fileName => path.resolve(outputPath, fileName)) 59 | 60 | try { 61 | const deletedPaths = await del(patterns, { force: true }) 62 | logger.info(`已清理 ${deletedPaths.length} 个文件`) 63 | } catch (error) { 64 | logger.error(`清理文件失败: ${error}`) 65 | } 66 | } -------------------------------------------------------------------------------- /packages/web/src/views/unknowns/index.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 72 | -------------------------------------------------------------------------------- /packages/web/src/components/CodePreview/CodePreview.vue: -------------------------------------------------------------------------------- 1 | 65 | 81 | 92 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /packages/web/src/components/Select.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 88 | -------------------------------------------------------------------------------- /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/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/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/web/src/components/Dialog.vue: -------------------------------------------------------------------------------- 1 | 51 | 81 | 82 | 102 | -------------------------------------------------------------------------------- /packages/web/src/utils/path2tree.ts: -------------------------------------------------------------------------------- 1 | export interface ITreeListItem { 2 | label: string, 3 | value?: string, 4 | path: string, 5 | children?: ITreeListItem [] 6 | } 7 | 8 | function isDir (item: object) { 9 | return JSON.stringify(item) !== '{}' 10 | } 11 | 12 | 13 | function joinTree (originPath: string, obj: any) { 14 | const pathList = originPath.split('/').filter(path => !!path) 15 | while(pathList.length){ 16 | const currPath = pathList.shift() || '' 17 | if (obj[currPath]) { 18 | obj = obj[currPath] 19 | } else { 20 | obj = obj[currPath] = {} 21 | } 22 | } 23 | } 24 | 25 | /** 26 | * path2tree 27 | * from: ['src/', 'src/aaa'] 28 | * to: { 'src/': { 'aaa': {} } } 29 | * @param {Array} pathList 30 | * @param {String} rootPath 31 | * @param {Array} ignores 32 | * @returns 33 | */ 34 | export function path2tree (pathList: string [], rootPath: string, ignores = []) { 35 | // const ig = ignore().add(ignores) 36 | const rootReg = new RegExp(rootPath) 37 | const tree = {} 38 | // const list = ig.filter(pathList) 39 | pathList.forEach(path => { 40 | const shortPath = path.replace(rootReg, '') 41 | joinTree(shortPath, tree) 42 | }) 43 | return tree 44 | } 45 | 46 | /** 47 | * transform2List 48 | * from: { 'src': { 'aaa': {} } } 49 | * to: [{ label: 'src', children: [ { label: 'aaa' } ] }] 50 | * @param {Object} treeObj 51 | * @param {Array} list 52 | * @param {string} pPath 父级路径 53 | */ 54 | export function transform2List (treeObj: any, list: ITreeListItem [] = [], pPath: string) { 55 | const keys = Object.keys(treeObj) 56 | const fileKeys: string [] = [] 57 | const dirKeys: string [] = [] 58 | 59 | keys.forEach(key => { 60 | if (isDir(treeObj[key])) { 61 | dirKeys.push(key) 62 | } else { 63 | fileKeys.push(key) 64 | } 65 | }) 66 | fileKeys.sort() 67 | dirKeys.sort() 68 | 69 | ;[...dirKeys, ...fileKeys].forEach(key => { 70 | const dir = isDir(treeObj[key]) 71 | const path = pPath + '/' + key 72 | 73 | const item: ITreeListItem = { 74 | label: key, 75 | path: path, 76 | } 77 | 78 | if (dir) { 79 | item.children = [] 80 | } else { 81 | item.value = '' 82 | } 83 | 84 | list.push(item) 85 | if (dir) { 86 | transform2List(treeObj[key], item.children || [], path) 87 | } 88 | }) 89 | } 90 | 91 | /** 92 | * path list 2 JsonTree 93 | * @example 94 | * form: 95 | * [ 96 | * 'a/a.js', 97 | * 'a/b.js' 98 | * ] 99 | * 100 | * to: 101 | * [ 102 | * { label: 'a', path: '/a' children: [ { label: 'a.js', path: '/a/a.js' }, { label: 'b.js', path: '/a/b.js' } ] } 103 | * ] 104 | */ 105 | export default (pathList: string [], rootPath: string): ITreeListItem [] => { 106 | const treeObj = path2tree(pathList, rootPath) 107 | const list: ITreeListItem [] = [] 108 | transform2List(treeObj, list, '') 109 | return list 110 | } -------------------------------------------------------------------------------- /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/src/views/packages/echart.ts: -------------------------------------------------------------------------------- 1 | import * as Echart from 'echarts' 2 | import { getPackage } from '../../api/remote-data' 3 | import { ImportDeps, ImportDepItem } from '@js-analyzer/core/types/index'; 4 | import { $tf } from '@/language'; 5 | 6 | let instance: any 7 | 8 | interface INode { 9 | name: string 10 | value: number 11 | // symbolSize: number, 12 | // itemStyle: any 13 | extendData: ImportDepItem & { name: string } 14 | } 15 | 16 | interface NodeResult { 17 | values: INode [] 18 | names: string [] 19 | } 20 | 21 | const getChartOption = ({ names, values }: NodeResult): any => { 22 | return { 23 | legend: { 24 | show: true, 25 | type: 'scroll', 26 | orient: 'vertical', 27 | textStyle: { 28 | color: '#999', 29 | // textBorderColor: '#fff', 30 | // textBorderWidth: 2, 31 | }, 32 | left: 30, 33 | top: 20, 34 | bottom: 20, 35 | data: names 36 | }, 37 | tooltip: { 38 | trigger: 'item', 39 | formatter: (params: any) => { 40 | const data = params.data.extendData 41 | if (!data) return '' 42 | return ` 43 | ${$tf('文件名')}:${data.name}
44 | ${$tf('被引用')}:${data.num} 次
45 | ${$tf('占比')}:${params.percent}%
46 | ` 47 | } 48 | }, 49 | series: [{ 50 | name: $tf('包名'), 51 | type: 'pie', 52 | data: values, 53 | label: { 54 | show: true, 55 | position: 'right' 56 | }, 57 | emphasis: { 58 | itemStyle: { 59 | shadowBlur: 10, 60 | shadowOffsetX: 0, 61 | shadowColor: 'rgba(0, 0, 0, 0.5)' 62 | } 63 | } 64 | }] 65 | } 66 | } 67 | 68 | const createNodes = (res: ImportDeps): NodeResult => { 69 | const names: string [] = [] 70 | const values: INode [] = [] 71 | Object.keys(res).map(key => ({ 72 | name: key, 73 | ...res[key], 74 | })).sort((a,b) => a.num - b.num).forEach(item => { 75 | const label = item.name + `(${item.num})` 76 | names.push(label) 77 | values.push({ 78 | name: label, 79 | value: item.num, 80 | extendData: { 81 | name: item.name, 82 | ...res[item.name] 83 | }, 84 | }) 85 | }) 86 | return { 87 | names, 88 | values, 89 | } 90 | // const nodes: INode [] = [] 91 | // for (const name in res) { 92 | // const symbolSize = res[name].num > 100 ? 100 : res[name].num + 10 93 | // nodes.push({ 94 | // name, 95 | // value: res[name].num, 96 | // symbolSize, 97 | // extendData: res[name], 98 | // itemStyle: { 99 | // color: symbolSize > 50 ? '#ff6b81' : '#747d8c' 100 | // } 101 | // }) 102 | // } 103 | // return nodes 104 | } 105 | 106 | export function useChart (ref: HTMLElement | null) { 107 | if (!ref) throw 'chart ref is not exist!' 108 | 109 | instance = Echart.init(ref) 110 | getPackage().then(res => { 111 | const nodes = createNodes(res) 112 | const options = getChartOption(nodes) 113 | instance.setOption(options) 114 | }) 115 | return { 116 | instance, 117 | } 118 | } -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /packages/web/src/language.ts: -------------------------------------------------------------------------------- 1 | import { computed } from "vue" 2 | 3 | type Lang = 'en' | 'ja' 4 | const defaultLanguage = 'en' 5 | 6 | export const languageOptions = [ 7 | { value: 'en', text: 'English' }, 8 | { value: 'zh_CN', text: '中文(简体)' }, 9 | { value: 'ja', text: '日本語' }, 10 | ] 11 | 12 | export const languageMap = { 13 | "关系图": { "en": "Graph", "ja": "関係図" }, 14 | "包管理": { "en": "Packages", "ja": "パッケージ" }, 15 | "热词": { "en": "Hot Words", "ja": "ホットワード" }, 16 | "隐式引用": { "en": "Undeclared", "ja": "暗黙の参照" }, 17 | "默认标题": { "en": "Default Title", "ja": "デフォルトのタイトル" }, 18 | "导出信息": { "en": "Export Information", "ja": "情報をエクスポートする" }, 19 | "我的项目": { "en": "My Project", "ja": "私のプロジェクト" }, 20 | "项目管理": { "en": "Project Management", "ja": "プロジェクト管理" }, 21 | "例如:/user/app/my-project": { "en": "For example: /user/app/my-project", "ja": "例:/user/app/my-project" }, 22 | "别名,例如:@/": { "en": "Alias, for example: @/", "ja": "別名、例:@/" }, 23 | "映射名,例如:src/": { "en": "Mapping name, for example: src/", "ja": "マッピング名、例:src/" }, 24 | "逗号隔开,默认:node_modules,dist": { "en": "Separated by commas, default: node_modules,dist", "ja": "カンマ区切り、デフォルト:node_modules,dist" }, 25 | "更新中...": { "en": "Updating...", "ja": "更新中..." }, 26 | "更新": { "en": "Update", "ja": "更新" }, 27 | "代码预览": { "en": "Code Preview", "ja": "コードプレビュー" }, 28 | "全部": { "en": "All", "ja": "全て" }, 29 | "未被引用文件": { "en": "Unreferenced Files", "ja": "未参照ファイル" }, 30 | "搜索": { "en": "Search", "ja": "検索" }, 31 | "被依赖视图": { "en": "Dependency View", "ja": "依存関係ビュー" }, 32 | "依赖视图": { "en": "Dependency View", "ja": "依存関係ビュー" }, 33 | "上游依赖图": { "en": "Upstream Dependency Graph", "ja": "上流依存グラフ" }, 34 | "文件夹关系图": { "en": "Folder Relationship Map", "ja": "フォルダー関係図" }, 35 | "默认": { "en": "Default", "ja": "デフォルト" }, 36 | "依赖分析视图": { "en": "Dependency Analysis View", "ja": "依存性分析ビュー" }, 37 | "重置": { "en": "Reset", "ja": "リセット" }, 38 | "显示节点文字": { "en": "Show Node Text", "ja": "ノードテキストを表示" }, 39 | "文件详情": { "en": "File Details", "ja": "ファイルの詳細" }, 40 | "文件名": { "en": "Filename", "ja": "ファイル名" }, 41 | "被引用次数": { "en": "Number of References", "ja": "参照回数" }, 42 | "绝对路径": { "en": "Absolute Path", "ja": "絶対パス" }, 43 | "导出变量": { "en": "Exported Variables", "ja": "エクスポートされた変数" }, 44 | "引用次数": { "en": "Number of References", "ja": "参照回数" }, 45 | "引用文件": { "en": "Referenced File", "ja": "参照ファイル" }, 46 | "包名": { "en": "Package Name", "ja": "パッケージ名" }, 47 | "名称": { "en": "Name", "ja": "名前" }, 48 | "引用名": { "en": "Reference Name", "ja": "参照名" }, 49 | "引用源": { "en": "Source of Reference", "ja": "参照元" }, 50 | "引用源未注册到项目中": { "en": "Reference Source Not Registered in the Project", "ja": "参照元がプロジェクトに登録されていない" }, 51 | "引用列表": { "en": "Reference List", "ja": "参照リスト" }, 52 | "被引用": { 53 | "en": "Cited", 54 | "ja": "引用された" 55 | }, 56 | "占比": { 57 | "en": "Proportion", 58 | "ja": "割合" 59 | }, 60 | "基础信息": { 61 | "en": "Basic Information", 62 | "ja": "基本情報" 63 | }, 64 | "例如:我的项目": { 65 | "en": "For example: My Project", 66 | "ja": "例:私のプロジェクト" 67 | }, 68 | '路径': { 'en': 'path', 'ja': 'パス' }, 69 | '别名映射': { 'en': 'Alias Mapping', 'ja': 'エイリアスマッピング' }, 70 | '项目名称': { 'en': 'Project Name', 'ja': 'プロジェクト名' }, 71 | '项目路径': { 'en': 'Project Path', 'ja': 'プロジェクトパス' }, 72 | '忽略路径': { 'en': 'Ignore Path', 'ja': 'パスを無視する' }, 73 | '请尽量完善以下信息,这能分析结果更加准确!': { 'en': 'Please provide more detailed information, it will help to analyze the results more accurately!', 'ja': '詳細な情報を提供していただくと、結果をより正確に分析できます!' }, 74 | } as const 75 | 76 | export const currentLanguage = localStorage.getItem('language') || defaultLanguage 77 | 78 | export function $tf(text: keyof typeof languageMap): string { 79 | if (currentLanguage === 'zh_CN') return text 80 | 81 | return languageMap[text][currentLanguage as Lang] 82 | } 83 | 84 | export function switchLanguage (type: Lang) { 85 | localStorage.setItem('language', type) 86 | window.location.reload() 87 | } -------------------------------------------------------------------------------- /packages/web/src/views/chart/Aside.vue: -------------------------------------------------------------------------------- 1 | 5 | 102 | 103 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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` 访问。 -------------------------------------------------------------------------------- /packages/web/src/views/chart/Echart.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 162 | -------------------------------------------------------------------------------- /packages/web/src/components/ProjectManage.vue: -------------------------------------------------------------------------------- 1 | 76 |