├── .cursor └── mcp.json ├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ ├── release-commit.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .stackblitzrc ├── .vscode ├── extensions.json ├── mcp.json └── settings.json ├── LICENSE.md ├── README.md ├── eslint.config.js ├── netlify.toml ├── node-modules-inspector.config.ts ├── package.json ├── packages ├── node-modules-inspector │ ├── bin.mjs │ ├── build.config.ts │ ├── package.json │ ├── rollup.config.mjs │ └── src │ │ ├── app │ │ ├── app.vue │ │ ├── backends │ │ │ ├── dev.ts │ │ │ ├── index.ts │ │ │ ├── static.ts │ │ │ └── websocket.ts │ │ ├── components │ │ │ ├── RenderNextTick.ts │ │ │ ├── chart │ │ │ │ ├── Framegraph.vue │ │ │ │ ├── NavBreadcrumb.vue │ │ │ │ ├── Sunburst.vue │ │ │ │ ├── SunburstSide.vue │ │ │ │ └── Treemap.vue │ │ │ ├── display │ │ │ │ ├── ClusterBadge.vue │ │ │ │ ├── DateBadge.vue │ │ │ │ ├── DeprecationMessage.vue │ │ │ │ ├── DurationBadge.vue │ │ │ │ ├── FileSizeBadge.vue │ │ │ │ ├── FundingEntry.vue │ │ │ │ ├── ModuleType.ts │ │ │ │ ├── NodeVersionRange.vue │ │ │ │ ├── NumberBadge.vue │ │ │ │ ├── PackageName.vue │ │ │ │ ├── PackageSpec.vue │ │ │ │ ├── SafeImage.vue │ │ │ │ ├── SourceTypeBadge.vue │ │ │ │ ├── Version.vue │ │ │ │ └── VersionWithUpdates.vue │ │ │ ├── graph │ │ │ │ ├── Canvas.vue │ │ │ │ ├── Dot.vue │ │ │ │ └── Node.vue │ │ │ ├── grid │ │ │ │ ├── Container.vue │ │ │ │ ├── Expand.vue │ │ │ │ └── Item.vue │ │ │ ├── integrations │ │ │ │ └── PublintPanel.vue │ │ │ ├── option │ │ │ │ ├── Checkbox.vue │ │ │ │ ├── Item.vue │ │ │ │ ├── PackageMultiSelectInput.vue │ │ │ │ ├── PackageSelect.vue │ │ │ │ └── SelectGroup.vue │ │ │ ├── panel │ │ │ │ ├── Filters.vue │ │ │ │ ├── FiltersMini.vue │ │ │ │ ├── FiltersOptionClusters.vue │ │ │ │ ├── FiltersOptionDepth.vue │ │ │ │ ├── FiltersOptionExcludes.vue │ │ │ │ ├── FiltersOptionFocus.vue │ │ │ │ ├── FiltersOptionModuleTypes.vue │ │ │ │ ├── FiltersOptionWhy.vue │ │ │ │ ├── FiltersResults.vue │ │ │ │ ├── Goto.vue │ │ │ │ ├── Nav.vue │ │ │ │ ├── NavRight.vue │ │ │ │ ├── Overview.vue │ │ │ │ ├── PackageDetails.vue │ │ │ │ ├── PackageFunding.vue │ │ │ │ ├── Settings.vue │ │ │ │ └── Terminal.vue │ │ │ ├── report │ │ │ │ ├── Deprecated.vue │ │ │ │ ├── Engines.vue │ │ │ │ ├── ExpendableContainer.vue │ │ │ │ ├── Funding.vue │ │ │ │ ├── InstallSize.vue │ │ │ │ ├── Licenses.vue │ │ │ │ ├── MultipleVersions.vue │ │ │ │ ├── PublishTime.vue │ │ │ │ ├── TransitiveDeps.vue │ │ │ │ └── UsedBy.vue │ │ │ ├── tree │ │ │ │ ├── Dependencies.vue │ │ │ │ └── Item.vue │ │ │ └── ui │ │ │ │ ├── Credits.vue │ │ │ │ ├── EmptyState.vue │ │ │ │ ├── Logo.vue │ │ │ │ ├── NoMobile.vue │ │ │ │ ├── PackageBorder.vue │ │ │ │ ├── Percentage.vue │ │ │ │ ├── PercentageFileCategories.vue │ │ │ │ ├── PercentageModuleType.vue │ │ │ │ ├── SubTitle.vue │ │ │ │ ├── TimeoutView.vue │ │ │ │ └── Title.vue │ │ ├── composables │ │ │ ├── dark.ts │ │ │ └── zoomElement.ts │ │ ├── entries │ │ │ ├── dev.vue │ │ │ ├── index.ts │ │ │ ├── main.vue │ │ │ └── webcontainer.vue │ │ ├── layouts │ │ │ └── default.vue │ │ ├── modules │ │ │ └── webcontainer.ts │ │ ├── pages │ │ │ ├── chart │ │ │ │ └── [...chart].vue │ │ │ ├── compare.vue │ │ │ ├── graph.vue │ │ │ ├── grid │ │ │ │ └── [...grid].vue │ │ │ ├── index.vue │ │ │ └── report │ │ │ │ └── [...report].vue │ │ ├── plugins │ │ │ └── floating-vue.ts │ │ ├── public │ │ │ ├── 3rd-parties │ │ │ │ ├── arethetypeswrong.png │ │ │ │ ├── bundlejs.svg │ │ │ │ ├── bundlephobia.png │ │ │ │ ├── npmgraph.png │ │ │ │ ├── packagephobia.png │ │ │ │ ├── pkg-size.svg │ │ │ │ ├── publint.svg │ │ │ │ ├── socket-dev.png │ │ │ │ └── synk.png │ │ │ ├── dot-grid-dark.png │ │ │ ├── dot-grid-light.png │ │ │ ├── favicon.svg │ │ │ └── og.png │ │ ├── server │ │ │ ├── api │ │ │ │ └── metadata.json.ts │ │ │ └── tsconfig.json │ │ ├── state │ │ │ ├── current.ts │ │ │ ├── data.ts │ │ │ ├── filters.ts │ │ │ ├── highlight.ts │ │ │ ├── payload.ts │ │ │ ├── query.ts │ │ │ ├── settings.ts │ │ │ ├── terminal.ts │ │ │ └── ui.ts │ │ ├── styles │ │ │ └── global.css │ │ ├── types │ │ │ ├── backend.ts │ │ │ └── chart.ts │ │ ├── utils │ │ │ ├── color.ts │ │ │ ├── file-category.ts │ │ │ ├── format.ts │ │ │ ├── maps.ts │ │ │ ├── module-type.ts │ │ │ ├── package-json.ts │ │ │ ├── search-parser.test.ts │ │ │ ├── search-parser.ts │ │ │ └── semver.ts │ │ └── webcontainer │ │ │ ├── Landing.vue │ │ │ ├── constants.ts │ │ │ └── container.ts │ │ ├── dirs.ts │ │ ├── node │ │ ├── cli.ts │ │ ├── config.ts │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── rpc.ts │ │ ├── server.ts │ │ ├── storage.ts │ │ ├── webcontainer │ │ │ └── server.ts │ │ └── ws.ts │ │ ├── nuxt.config.ts │ │ ├── shared │ │ ├── constants.ts │ │ ├── filters.ts │ │ ├── types.ts │ │ ├── utils.ts │ │ └── version-info.ts │ │ └── uno.config.ts └── node-modules-tools │ ├── README.md │ ├── build.config.ts │ ├── package.json │ ├── src │ ├── agent-entry │ │ ├── detect.ts │ │ ├── index.ts │ │ └── list.ts │ ├── agents │ │ ├── npm │ │ │ ├── index.ts │ │ │ └── list.ts │ │ └── pnpm │ │ │ ├── index.ts │ │ │ └── list.ts │ ├── analyze-esm.ts │ ├── constants.ts │ ├── index.ts │ ├── json-parse-stream.ts │ ├── list.ts │ ├── resolve.ts │ ├── size.ts │ ├── types │ │ ├── base.ts │ │ ├── index.ts │ │ ├── list.ts │ │ ├── node.ts │ │ └── size.ts │ ├── utils.ts │ └── utils │ │ ├── filter.test.ts │ │ ├── filter.ts │ │ ├── index.ts │ │ ├── package-json.test.ts │ │ └── package-json.ts │ └── test │ ├── module-type.test.ts │ ├── npm │ ├── fixtures │ │ └── multiple-package-jsons │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── sub │ │ │ └── package.json │ └── list.test.ts │ └── pnpm │ ├── fixtures │ └── multiple-package-jsons │ │ ├── package.json │ │ ├── pnpm-lock.yaml │ │ └── sub │ │ └── package.json │ └── list.test.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json └── vitest.config.ts /.cursor/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "nuxt": { 4 | "url": "http://localhost:3000/__mcp/sse" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v4 20 | 21 | - name: Set node 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: lts/* 25 | 26 | - name: Setup 27 | run: npm i -g @antfu/ni 28 | 29 | - name: Install 30 | run: nci 31 | 32 | - name: Lint 33 | run: nr lint 34 | 35 | - name: Typecheck 36 | run: nr typecheck 37 | 38 | test: 39 | runs-on: ${{ matrix.os }} 40 | 41 | strategy: 42 | matrix: 43 | node: [lts/*] 44 | os: [ubuntu-latest, windows-latest, macos-latest] 45 | fail-fast: false 46 | 47 | steps: 48 | - uses: actions/checkout@v4 49 | 50 | - name: Install pnpm 51 | uses: pnpm/action-setup@v4 52 | 53 | - name: Set node ${{ matrix.node }} 54 | uses: actions/setup-node@v4 55 | with: 56 | node-version: ${{ matrix.node }} 57 | 58 | - name: Setup 59 | run: npm i -g @antfu/ni 60 | 61 | - name: Install 62 | run: nci 63 | 64 | - name: Build 65 | run: nr build 66 | 67 | - name: Test 68 | run: nr test 69 | -------------------------------------------------------------------------------- /.github/workflows/release-commit.yml: -------------------------------------------------------------------------------- 1 | name: Publish Any Commit 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v4 11 | 12 | - name: Install pnpm 13 | uses: pnpm/action-setup@v4.0.0 14 | 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: lts/* 18 | cache: pnpm 19 | 20 | - name: Install dependencies 21 | run: pnpm install 22 | 23 | - name: Build 24 | run: pnpm build 25 | 26 | - name: Publish 27 | run: pnpm dlx pkg-pr-new@0.0 publish --compact --pnpm './packages/*' 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Install pnpm 20 | uses: pnpm/action-setup@v4 21 | 22 | - name: Set node 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: lts/* 26 | 27 | - run: npx changelogithub 28 | env: 29 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | dist 4 | .output 5 | .nuxt 6 | .env 7 | .idea/ 8 | .DS_Store 9 | .node-modules-inspector 10 | packages/node-modules-inspector/src/app/public/fonts 11 | packages/node-modules-inspector/runtime 12 | .vite-inspect 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | shell-emulator=true 4 | ignore-workspace-root-check=true 5 | -------------------------------------------------------------------------------- /.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": true, 3 | "startCommand": "npm run dev" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "antfu.iconify", 4 | "antfu.unocss", 5 | "antfu.goto-alias", 6 | "antfu.pnpm-catalog-lens", 7 | "dbaeumer.vscode-eslint", 8 | "vue.volar" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "servers": { 3 | "nuxt": { 4 | "type": "sse", 5 | "url": "http://localhost:3000/__mcp/sse" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Disable the default formatter, use eslint instead 3 | "prettier.enable": false, 4 | "editor.formatOnSave": false, 5 | 6 | // Auto fix 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll": "explicit", 9 | "source.organizeImports": "never" 10 | }, 11 | 12 | // Silent the stylistic rules in you IDE, but still auto fix them 13 | "eslint.rules.customizations": [ 14 | { "rule": "style/*", "severity": "off" }, 15 | { "rule": "*-indent", "severity": "off" }, 16 | { "rule": "*-spacing", "severity": "off" }, 17 | { "rule": "*-spaces", "severity": "off" }, 18 | { "rule": "*-order", "severity": "off" }, 19 | { "rule": "*-dangle", "severity": "off" }, 20 | { "rule": "*-newline", "severity": "off" }, 21 | { "rule": "*quotes", "severity": "off" }, 22 | { "rule": "*semi", "severity": "off" } 23 | ], 24 | 25 | // Enable eslint for all supported languages 26 | "eslint.validate": [ 27 | "javascript", 28 | "javascriptreact", 29 | "typescript", 30 | "typescriptreact", 31 | "vue", 32 | "html", 33 | "markdown", 34 | "json", 35 | "jsonc", 36 | "yaml" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Anthony Fu 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 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import antfu from '@antfu/eslint-config' 3 | import nuxt from './packages/node-modules-inspector/src/.nuxt/eslint.config.mjs' 4 | 5 | export default antfu({ 6 | pnpm: true, 7 | }) 8 | .append(nuxt()) 9 | .append({ 10 | files: ['packages/node-modules-inspector/src/node/**/*.ts'], 11 | rules: { 12 | 'no-console': 'off', 13 | }, 14 | }) 15 | .append({ 16 | files: ['pnpm-workspace.yaml'], 17 | name: 'antfu/yaml/pnpm-workspace', 18 | rules: { 19 | 'yaml/sort-keys': [ 20 | 'error', 21 | { 22 | order: [ 23 | 'packages', 24 | 'overrides', 25 | 'patchedDependencies', 26 | 'hoistPattern', 27 | 'catalog', 28 | 'catalogs', 29 | 30 | 'allowedDeprecatedVersions', 31 | 'allowNonAppliedPatches', 32 | 'configDependencies', 33 | 'ignoredBuiltDependencies', 34 | 'ignoredOptionalDependencies', 35 | 'neverBuiltDependencies', 36 | 'onlyBuiltDependencies', 37 | 'onlyBuiltDependenciesFile', 38 | 'packageExtensions', 39 | 'peerDependencyRules', 40 | 'supportedArchitectures', 41 | ], 42 | pathPattern: '^$', 43 | }, 44 | { 45 | order: { type: 'asc' }, 46 | pathPattern: '.*', 47 | }, 48 | ], 49 | }, 50 | }) 51 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "packages/node-modules-inspector/dist/public" 3 | command = "pnpm run wc:build" 4 | 5 | [build.environment] 6 | NODE_VERSION = "22" 7 | 8 | [[headers]] 9 | for = "/*" 10 | 11 | [headers.values] 12 | Cross-Origin-Embedder-Policy = "require-corp" 13 | Cross-Origin-Opener-Policy = "same-origin" 14 | 15 | [[redirects]] 16 | from = "/*" 17 | to = "/index.html" 18 | status = 200 19 | -------------------------------------------------------------------------------- /node-modules-inspector.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from './packages/node-modules-inspector/src/node/index' 2 | 3 | export default defineConfig({ 4 | name: 'node-modules-inspector', 5 | excludeDependenciesOf: [ 6 | 'eslint', 7 | ], 8 | excludePackages: [ 9 | '@pnpm/list', 10 | '@pnpm/types', 11 | ], 12 | defaultFilters: { 13 | excludes: [ 14 | 'webpack', 15 | ], 16 | }, 17 | publint: true, 18 | }) 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "version": "0.6.8", 4 | "private": true, 5 | "packageManager": "pnpm@10.10.0", 6 | "scripts": { 7 | "build": "pnpm -r run build", 8 | "build:debug": "NUXT_DEBUG_BUILD=true pnpm -r run build", 9 | "stub": "pnpm -r run stub", 10 | "dev": "pnpm -C packages/node-modules-inspector run dev", 11 | "start": "pnpm -C packages/node-modules-inspector run start", 12 | "wc:dev": "pnpm -C packages/node-modules-inspector run wc:dev", 13 | "wc:build": "pnpm -C packages/node-modules-inspector run wc:build", 14 | "prepare": "npx simple-git-hooks && pnpm -C packages/node-modules-inspector run dev:prepare", 15 | "lint": "pnpm -C packages/node-modules-inspector run dev:prepare && eslint .", 16 | "test": "vitest", 17 | "release": "bumpp -r && pnpm publish -r", 18 | "typecheck": "vue-tsc --noEmit" 19 | }, 20 | "devDependencies": { 21 | "@antfu/eslint-config": "catalog:lint", 22 | "@antfu/utils": "catalog:inlined", 23 | "@iconify-json/carbon": "catalog:icons", 24 | "@iconify-json/catppuccin": "catalog:icons", 25 | "@iconify-json/logos": "catalog:icons", 26 | "@iconify-json/ph": "catalog:icons", 27 | "@iconify-json/ri": "catalog:icons", 28 | "@iconify-json/simple-icons": "catalog:icons", 29 | "@nuxt/devtools": "catalog:dev", 30 | "@nuxt/eslint": "catalog:lint", 31 | "@rollup/plugin-alias": "catalog:bundling", 32 | "@rollup/plugin-commonjs": "catalog:bundling", 33 | "@rollup/plugin-node-resolve": "catalog:bundling", 34 | "@types/connect": "catalog:types", 35 | "@types/d3": "catalog:types", 36 | "@types/d3-hierarchy": "catalog:types", 37 | "@types/ws": "catalog:types", 38 | "@typescript-eslint/utils": "catalog:lint", 39 | "@unocss/eslint-config": "catalog:lint", 40 | "ansis": "catalog:deps", 41 | "bumpp": "catalog:dev", 42 | "esbuild": "catalog:bundling", 43 | "eslint": "catalog:lint", 44 | "fast-npm-meta": "catalog:deps", 45 | "lint-staged": "catalog:lint", 46 | "node-modules-inspector": "workspace:*", 47 | "node-modules-tools": "workspace:*", 48 | "nuxt": "catalog:bundling", 49 | "nuxt-eslint-auto-explicit-import": "catalog:lint", 50 | "nuxt-mcp": "catalog:bundling", 51 | "p-limit": "catalog:deps", 52 | "rollup": "catalog:bundling", 53 | "rollup-plugin-esbuild": "catalog:bundling", 54 | "simple-git-hooks": "catalog:lint", 55 | "typescript": "catalog:dev", 56 | "unbuild": "catalog:bundling", 57 | "unstorage": "catalog:deps", 58 | "vite": "catalog:bundling", 59 | "vite-plugin-inspect": "catalog:dev", 60 | "vitest": "catalog:testing", 61 | "vue-tsc": "catalog:dev" 62 | }, 63 | "resolutions": { 64 | "@nuxt/devtools": "catalog:dev", 65 | "esbuild": "catalog:bundling", 66 | "nitropack": "catalog:bundling", 67 | "nuxt": "catalog:bundling", 68 | "rollup": "catalog:bundling", 69 | "vite": "catalog:bundling" 70 | }, 71 | "simple-git-hooks": { 72 | "pre-commit": "pnpm i --frozen-lockfile --ignore-scripts --offline && npx lint-staged" 73 | }, 74 | "lint-staged": { 75 | "*": "eslint --fix" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/bin.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // eslint-disable-next-line antfu/no-top-level-await 4 | await import('./dist/cli.mjs') 5 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | entries: [ 5 | { 6 | input: 'src/node/index.ts', 7 | name: 'index', 8 | outDir: 'dist', 9 | }, 10 | { 11 | input: 'src/node/cli.ts', 12 | name: 'cli', 13 | outDir: 'dist', 14 | }, 15 | { 16 | input: 'src/dirs.ts', 17 | name: 'dirs', 18 | outDir: 'dist', 19 | }, 20 | ], 21 | clean: false, 22 | declaration: 'node16', 23 | rollup: { 24 | inlineDependencies: [ 25 | '@antfu/utils', 26 | ], 27 | }, 28 | }) 29 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-modules-inspector", 3 | "type": "module", 4 | "version": "0.6.8", 5 | "description": "A Node Modules Inspector", 6 | "author": "Anthony Fu ", 7 | "license": "MIT", 8 | "funding": "https://github.com/sponsors/antfu", 9 | "homepage": "https://github.com/antfu/node-modules-inspector#readme", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/antfu/node-modules-inspector.git" 13 | }, 14 | "bugs": "https://github.com/antfu/node-modules-inspector/issues", 15 | "keywords": [], 16 | "sideEffects": false, 17 | "exports": { 18 | ".": "./dist/index.mjs" 19 | }, 20 | "main": "./dist/index.mjs", 21 | "types": "./dist/index.d.mts", 22 | "bin": "bin.mjs", 23 | "files": [ 24 | "bin.mjs", 25 | "dist" 26 | ], 27 | "scripts": { 28 | "dev": "pnpm run -r stub && ROLLDOWN_OPTIONS_VALIDATION=loose nuxi dev src", 29 | "stub": "unbuild --stub", 30 | "build": "pnpm run wc:prepare && ROLLDOWN_OPTIONS_VALIDATION=loose nuxi build src && unbuild", 31 | "build:debug": "ROLLDOWN_OPTIONS_VALIDATION=loose NUXT_DEBUG_BUILD=true pnpm run build", 32 | "start": "node ./bin.mjs", 33 | "prepack": "pnpm build", 34 | "dev:prepare": "nuxi prepare src && pnpm wc:prepare", 35 | "wc:prepare": "rollup -c", 36 | "wc:build": "pnpm run wc:prepare && NMI_BACKEND=webcontainer ROLLDOWN_OPTIONS_VALIDATION=loose nuxi build src && unbuild", 37 | "wc:dev": "pnpm run -r stub && pnpm run wc:prepare && cd src && NMI_BACKEND=webcontainer ROLLDOWN_OPTIONS_VALIDATION=loose nuxi dev" 38 | }, 39 | "dependencies": { 40 | "ansis": "catalog:deps", 41 | "birpc": "catalog:deps", 42 | "cac": "catalog:deps", 43 | "fast-npm-meta": "catalog:deps", 44 | "get-port-please": "catalog:deps", 45 | "h3": "catalog:deps", 46 | "launch-editor": "catalog:deps", 47 | "mlly": "catalog:testing", 48 | "mrmime": "catalog:deps", 49 | "node-modules-tools": "workspace:*", 50 | "ohash": "catalog:deps", 51 | "open": "catalog:deps", 52 | "p-limit": "catalog:deps", 53 | "pathe": "catalog:deps", 54 | "publint": "catalog:deps", 55 | "structured-clone-es": "catalog:deps", 56 | "tinyglobby": "catalog:deps", 57 | "unconfig": "catalog:deps", 58 | "unstorage": "catalog:deps", 59 | "ws": "catalog:deps" 60 | }, 61 | "devDependencies": { 62 | "@types/semver": "catalog:types", 63 | "@unocss/nuxt": "catalog:bundling", 64 | "@vueuse/nuxt": "catalog:bundling", 65 | "@webcontainer/api": "catalog:frontend", 66 | "@xterm/addon-fit": "catalog:frontend", 67 | "@xterm/xterm": "catalog:frontend", 68 | "d3": "catalog:frontend", 69 | "d3-hierarchy": "catalog:frontend", 70 | "d3-shape": "catalog:frontend", 71 | "floating-vue": "catalog:frontend", 72 | "fuse.js": "catalog:frontend", 73 | "idb-keyval": "catalog:frontend", 74 | "modern-screenshot": "catalog:frontend", 75 | "nanovis": "catalog:frontend", 76 | "rollup": "catalog:bundling", 77 | "semver": "catalog:deps", 78 | "theme-vitesse": "catalog:frontend", 79 | "vite-hot-client": "catalog:frontend" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import alias from '@rollup/plugin-alias' 3 | // @ts-check 4 | import commonjs from '@rollup/plugin-commonjs' 5 | import nodeResolve from '@rollup/plugin-node-resolve' 6 | import { defineConfig } from 'rollup' 7 | import esbuild from 'rollup-plugin-esbuild' 8 | 9 | export default defineConfig({ 10 | input: './src/node/webcontainer/server.ts', 11 | output: { 12 | file: './runtime/webcontainer-server.mjs', 13 | format: 'es', 14 | inlineDynamicImports: true, 15 | }, 16 | 17 | plugins: [ 18 | alias({ 19 | entries: { 20 | 'node-modules-tools': fileURLToPath(new URL('../node-modules-tools/src/index.ts', import.meta.url)), 21 | }, 22 | }), 23 | commonjs(), 24 | nodeResolve({ 25 | preferBuiltins: true, 26 | }), 27 | esbuild({ 28 | minifyWhitespace: true, 29 | target: 'es2022', 30 | }), 31 | ], 32 | }) 33 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/app.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/backends/dev.ts: -------------------------------------------------------------------------------- 1 | import type { ConnectionMeta } from '../../shared/types' 2 | import { useRuntimeConfig } from '#app/nuxt' 3 | import { createStaticBackend } from './static' 4 | import { createWebSocketBackend } from './websocket' 5 | 6 | export async function createDevBackend() { 7 | const config = useRuntimeConfig() 8 | const baseURL = config.app.baseURL 9 | const metadata: ConnectionMeta = await fetch(`${baseURL}api/metadata.json`) 10 | .then(r => r.json()) 11 | 12 | if (metadata.backend === 'static') { 13 | return createStaticBackend() 14 | } 15 | else { 16 | const url = `${location.protocol.replace('http', 'ws')}//${location.hostname}:${metadata.websocket}` 17 | 18 | return createWebSocketBackend({ 19 | name: 'dev', 20 | websocketUrl: url, 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/backends/index.ts: -------------------------------------------------------------------------------- 1 | import type { Backend } from '../types/backend' 2 | import { shallowRef } from 'vue' 3 | 4 | export const backend = shallowRef() 5 | 6 | export function getBackend() { 7 | return backend.value! 8 | } 9 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/backends/static.ts: -------------------------------------------------------------------------------- 1 | import type { ServerFunctionsDump } from '../../shared/types' 2 | import type { Backend } from '../types/backend' 3 | import { useRuntimeConfig } from '#app/nuxt' 4 | import { parse } from 'structured-clone-es' 5 | import { ref } from 'vue' 6 | 7 | export function createStaticBackend(): Backend { 8 | const config = useRuntimeConfig() 9 | const baseURL = config.app.baseURL 10 | const status: Backend['status'] = ref('connecting') 11 | const error = ref(undefined) 12 | const getDump = fetch(`${baseURL}api/rpc-dump.json`) 13 | .then(res => res.text()) 14 | .then(text => parse(text) as ServerFunctionsDump) 15 | .then((dump) => { 16 | status.value = 'connected' 17 | return dump 18 | }) 19 | .catch((e) => { 20 | status.value = 'error' 21 | console.error('Failed to fetch RPC dump:', e) 22 | error.value = e 23 | throw e 24 | }) 25 | 26 | return { 27 | name: 'static', 28 | status, 29 | connectionError: error, 30 | connect() {}, 31 | functions: { 32 | getPayload: () => { 33 | return getDump.then(dump => dump.getPayload) 34 | }, 35 | }, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/backends/websocket.ts: -------------------------------------------------------------------------------- 1 | import type { PackageNode } from 'node-modules-tools' 2 | import type { ClientFunctions, ServerFunctions } from '../../shared/types' 3 | import type { Backend } from '../types/backend' 4 | import { createBirpc } from 'birpc' 5 | import { parse, stringify } from 'structured-clone-es' 6 | import { ref, shallowRef } from 'vue' 7 | 8 | export interface WebSocketBackendOptions { 9 | name: string 10 | websocketUrl: string 11 | } 12 | 13 | export function createWebSocketBackend(options: WebSocketBackendOptions): Backend { 14 | const status: Backend['status'] = ref('idle') 15 | const error: Backend['connectionError'] = shallowRef(undefined) 16 | 17 | let connectPromise: Promise | undefined 18 | let onMessage: any = () => {} 19 | 20 | const clientFunctions = {} as ClientFunctions 21 | 22 | const rpc = createBirpc(clientFunctions, { 23 | post: async (d) => { 24 | if (!connectPromise) 25 | connectPromise = connect() 26 | const ws = await connectPromise 27 | while (ws.readyState === ws.CONNECTING) { 28 | await new Promise(resolve => setTimeout(resolve, 100)) 29 | } 30 | if (ws.readyState !== ws.OPEN) { 31 | error.value ||= new Error('WebSocket not open, message sending dismissed') 32 | throw error.value 33 | } 34 | ws.send(d) 35 | }, 36 | on: (fn) => { 37 | onMessage = fn 38 | }, 39 | serialize: stringify, 40 | deserialize: parse, 41 | onError(err, name) { 42 | error.value = err 43 | console.error(`[node-modules-inspector] RPC error on executing "${name}":`) 44 | console.error(err) 45 | }, 46 | timeout: 120_000, 47 | }) 48 | 49 | async function connect() { 50 | try { 51 | const ws = new WebSocket(options.websocketUrl) 52 | 53 | ws.addEventListener('close', () => { 54 | status.value = 'idle' 55 | }) 56 | ws.addEventListener('open', () => { 57 | status.value = 'connected' 58 | error.value = undefined 59 | }) 60 | ws.addEventListener('error', (e) => { 61 | status.value = 'error' 62 | error.value = e 63 | }) 64 | ws.addEventListener('message', (e) => { 65 | status.value = 'connected' 66 | error.value = undefined 67 | onMessage(e.data) 68 | }) 69 | 70 | return ws 71 | } 72 | catch (e) { 73 | status.value = 'error' 74 | error.value = e 75 | throw e 76 | } 77 | } 78 | 79 | return { 80 | name: options.name, 81 | status, 82 | connectionError: error, 83 | async connect() { 84 | if (!connectPromise) 85 | connectPromise = connect() 86 | await connectPromise 87 | }, 88 | isDynamic: true, 89 | functions: { 90 | getPayload: async () => { 91 | try { 92 | return await rpc.getPayload() 93 | } 94 | catch (err) { 95 | error.value = err 96 | throw err 97 | } 98 | }, 99 | getPackagesNpmMeta: async (specs: string[]) => { 100 | return await rpc.getPackagesNpmMeta(specs) 101 | }, 102 | getPackagesNpmMetaLatest: async (pkgNames: string[]) => { 103 | return await rpc.getPackagesNpmMetaLatest(pkgNames) 104 | }, 105 | getPublint: async (pkg: Pick) => { 106 | return await rpc.getPublint(pkg) 107 | }, 108 | openInEditor: (filename: string) => rpc.openInEditor.asEvent(filename), 109 | openInFinder: (filename: string) => rpc.openInFinder.asEvent(filename), 110 | }, 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/RenderNextTick.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, ref, renderSlot } from 'vue' 2 | 3 | export default defineComponent({ 4 | name: 'RenderNextTick', 5 | setup(_, { slots }) { 6 | const render = ref(false) 7 | 8 | setTimeout(() => { 9 | render.value = true 10 | }, 0) 11 | 12 | return () => render.value 13 | ? renderSlot(slots, 'default') 14 | : renderSlot(slots, 'fallback') 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/chart/Framegraph.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/chart/NavBreadcrumb.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 41 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/chart/Sunburst.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 30 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/chart/SunburstSide.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 63 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/chart/Treemap.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 31 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/display/ClusterBadge.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 55 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/display/DateBadge.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 42 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/display/DeprecationMessage.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 46 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/display/DurationBadge.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 75 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/display/FileSizeBadge.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 61 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/display/FundingEntry.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 28 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/display/ModuleType.ts: -------------------------------------------------------------------------------- 1 | import type { PackageModuleType, PackageNode } from 'node-modules-tools' 2 | import type { PropType } from 'vue' 3 | import { Tooltip as FloatingTooltip } from 'floating-vue' 4 | import { computed, defineComponent, h } from 'vue' 5 | import { settings } from '../../state/settings' 6 | import { getModuleType, MODULE_TYPES_COLOR_BADGE, MODULE_TYPES_NAME } from '../../utils/module-type' 7 | 8 | // @unocss-include 9 | 10 | export default defineComponent({ 11 | name: 'DisplayModuleType', 12 | props: { 13 | pkg: { 14 | type: [Object, String] as PropType, 15 | required: true, 16 | }, 17 | badge: { 18 | type: Boolean, 19 | default: true, 20 | }, 21 | force: { 22 | type: Boolean, 23 | default: false, 24 | }, 25 | }, 26 | setup(props) { 27 | const type = computed(() => getModuleType(props.pkg)) 28 | const description = computed(() => { 29 | if (type.value === 'cjs') 30 | return 'CommonJS module. The legacy non-standard format.' 31 | if (type.value === 'esm') 32 | return 'Standard Ecmascript module format' 33 | if (type.value === 'dual') 34 | return 'Package that ships both ESM and CJS' 35 | if (type.value === 'faux') 36 | return 'Package that ships non-standard module format, that might work with some bundlers but not Node.js' 37 | if (type.value === 'dts') 38 | return 'Package that ships TypeScript types' 39 | }) 40 | 41 | return () => { 42 | if (settings.value.moduleTypeRender === 'none' && !props.force) 43 | return null 44 | 45 | if (settings.value.moduleTypeRender === 'circle' && !props.force) { 46 | return h(FloatingTooltip, {}, { 47 | default: () => h('div', { 48 | class: 'flex', 49 | title: type.value.toUpperCase(), 50 | }, h('div', { 51 | class: [ 52 | 'w-3 h-3 rounded-full border border-current!', 53 | MODULE_TYPES_COLOR_BADGE[type.value], 54 | ], 55 | })), 56 | popper: () => h('div', { class: 'text-sm' }, description.value), 57 | }) 58 | } 59 | 60 | return h(FloatingTooltip, {}, { 61 | default: () => h('div', { 62 | class: [ 63 | 'select-none', 64 | MODULE_TYPES_COLOR_BADGE[type.value], 65 | props.badge ? 'w-11 flex-none text-center px1 rounded text-sm' : 'bg-transparent! w-auto!', 66 | ], 67 | }, MODULE_TYPES_NAME[type.value]), 68 | popper: () => h('div', { class: 'text-sm' }, description.value), 69 | }) 70 | } 71 | }, 72 | }) 73 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/display/NodeVersionRange.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 30 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/display/NumberBadge.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 38 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/display/PackageName.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/display/PackageSpec.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 39 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/display/SafeImage.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/display/SourceTypeBadge.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 55 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/display/Version.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/display/VersionWithUpdates.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 51 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/graph/Dot.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/graph/Node.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 32 | 33 | 43 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/grid/Container.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/grid/Expand.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 27 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/grid/Item.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 65 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/integrations/PublintPanel.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 97 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/option/Checkbox.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/option/Item.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 36 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/option/PackageMultiSelectInput.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 70 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/option/PackageSelect.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 102 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/option/SelectGroup.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 40 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/panel/Filters.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 93 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/panel/FiltersOptionClusters.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 105 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/panel/FiltersOptionDepth.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 103 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/panel/FiltersOptionExcludes.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 58 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/panel/FiltersOptionFocus.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 33 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/panel/FiltersOptionModuleTypes.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 55 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/panel/FiltersOptionWhy.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 33 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/panel/FiltersResults.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 28 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/panel/Goto.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 50 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/panel/NavRight.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 53 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/panel/PackageFunding.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 41 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/panel/Settings.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 56 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/panel/Terminal.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 89 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/report/Engines.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 51 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/report/ExpendableContainer.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 94 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/report/Funding.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 93 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/report/InstallSize.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 56 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/report/MultipleVersions.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 87 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/report/TransitiveDeps.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 54 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/report/UsedBy.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 64 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/tree/Dependencies.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 67 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/tree/Item.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 33 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/ui/Credits.vue: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/ui/EmptyState.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 26 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/ui/NoMobile.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 32 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/ui/PackageBorder.vue: -------------------------------------------------------------------------------- 1 | 105 | 106 | 113 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/ui/Percentage.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 39 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/ui/PercentageFileCategories.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 38 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/ui/PercentageModuleType.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 44 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/ui/SubTitle.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/ui/TimeoutView.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 41 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/components/ui/Title.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 33 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/composables/dark.ts: -------------------------------------------------------------------------------- 1 | import { useDark } from '@vueuse/core' 2 | 3 | export const isDark = useDark({ 4 | valueLight: 'light', 5 | }) 6 | 7 | export function toggleDark() { 8 | isDark.value = !isDark.value 9 | } 10 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/composables/zoomElement.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeElementRef } from '@vueuse/core' 2 | import type { MaybeRef } from 'vue' 3 | import { useEventListener } from '@vueuse/core' 4 | import { ref, toValue } from 'vue' 5 | 6 | export function useZoomElement( 7 | target: MaybeElementRef, 8 | { 9 | wheel = true, 10 | minScale = 0.5, 11 | maxScale = 2, 12 | }: { 13 | wheel?: MaybeRef 14 | minScale?: number 15 | maxScale?: number 16 | } = {}, 17 | ) { 18 | const scale = ref(1) 19 | 20 | function zoom(factor: number, clientX?: number, clientY?: number) { 21 | const el = toValue(target) 22 | if (!el) 23 | return 24 | 25 | const { left, top, width, height } = el.getBoundingClientRect() 26 | 27 | // default to center 28 | const x = clientX ?? (left + width / 2) 29 | const y = clientY ?? (top + height / 2) 30 | 31 | const offsetX = x - left 32 | const offsetY = y - top 33 | const oldScale = scale.value 34 | 35 | scale.value = Math.max(minScale, Math.min(maxScale, oldScale + factor)) 36 | 37 | const ratio = scale.value / oldScale 38 | 39 | // Adjust scroll so that the zoom center is kept in place 40 | el.scrollLeft = (el.scrollLeft + offsetX) * ratio - offsetX 41 | el.scrollTop = (el.scrollTop + offsetY) * ratio - offsetY 42 | } 43 | 44 | function handleWheel(event: WheelEvent) { 45 | if (!toValue(wheel)) 46 | return 47 | 48 | event.preventDefault() 49 | 50 | const zoomFactor = 0.2 51 | zoom(event.deltaY > 0 ? zoomFactor : zoomFactor * -1, event.clientX, event.clientY) 52 | } 53 | 54 | function zoomIn(factor = 0.2) { 55 | zoom(factor) 56 | } 57 | 58 | function zoomOut(factor = 0.2) { 59 | zoom(factor * -1) 60 | } 61 | 62 | useEventListener(target, 'wheel', handleWheel) 63 | 64 | return { scale, zoom, zoomIn, zoomOut } 65 | } 66 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/entries/dev.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 26 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/entries/index.ts: -------------------------------------------------------------------------------- 1 | import { defineAsyncComponent } from 'vue' 2 | 3 | export default defineAsyncComponent(() => { 4 | if (import.meta.env.BACKEND === 'webcontainer') 5 | return import('./webcontainer.vue') 6 | else 7 | return import('./dev.vue') 8 | }) 9 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/entries/main.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 58 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/entries/webcontainer.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 39 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/modules/webcontainer.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises' 2 | import { fileURLToPath } from 'node:url' 3 | import { addTemplate, defineNuxtModule } from '@nuxt/kit' 4 | 5 | export default defineNuxtModule({ 6 | meta: { 7 | name: 'webcontainer-setup', 8 | }, 9 | setup() { 10 | addTemplate({ 11 | filename: 'webcontainer-server-code', 12 | getContents: async ({ nuxt }) => { 13 | try { 14 | const content = await fs.readFile(fileURLToPath(new URL('../../../runtime/webcontainer-server.mjs', import.meta.url)), 'utf-8') 15 | return `export const WEBCONTAINER_SERVER_CODE = ${JSON.stringify(content)}` 16 | } 17 | catch (e) { 18 | if (nuxt.options._prepare) { 19 | return `export const WEBCONTAINER_SERVER_CODE = ''` 20 | } 21 | throw e 22 | } 23 | }, 24 | }) 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/pages/graph.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 65 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/pages/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/pages/report/[...report].vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 61 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/plugins/floating-vue.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtPlugin } from '#app/nuxt' 2 | import FloatingVue from 'floating-vue' 3 | 4 | export default defineNuxtPlugin((nuxtApp) => { 5 | nuxtApp.vueApp.use(FloatingVue, { 6 | overflowPadding: 20, 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/public/3rd-parties/arethetypeswrong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu/node-modules-inspector/01fef40cc0cb45e414d34b6fa588bd05a9e1003c/packages/node-modules-inspector/src/app/public/3rd-parties/arethetypeswrong.png -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/public/3rd-parties/bundlephobia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu/node-modules-inspector/01fef40cc0cb45e414d34b6fa588bd05a9e1003c/packages/node-modules-inspector/src/app/public/3rd-parties/bundlephobia.png -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/public/3rd-parties/npmgraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu/node-modules-inspector/01fef40cc0cb45e414d34b6fa588bd05a9e1003c/packages/node-modules-inspector/src/app/public/3rd-parties/npmgraph.png -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/public/3rd-parties/packagephobia.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu/node-modules-inspector/01fef40cc0cb45e414d34b6fa588bd05a9e1003c/packages/node-modules-inspector/src/app/public/3rd-parties/packagephobia.png -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/public/3rd-parties/pkg-size.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/public/3rd-parties/publint.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/public/3rd-parties/socket-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu/node-modules-inspector/01fef40cc0cb45e414d34b6fa588bd05a9e1003c/packages/node-modules-inspector/src/app/public/3rd-parties/socket-dev.png -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/public/3rd-parties/synk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu/node-modules-inspector/01fef40cc0cb45e414d34b6fa588bd05a9e1003c/packages/node-modules-inspector/src/app/public/3rd-parties/synk.png -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/public/dot-grid-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu/node-modules-inspector/01fef40cc0cb45e414d34b6fa588bd05a9e1003c/packages/node-modules-inspector/src/app/public/dot-grid-dark.png -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/public/dot-grid-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu/node-modules-inspector/01fef40cc0cb45e414d34b6fa588bd05a9e1003c/packages/node-modules-inspector/src/app/public/dot-grid-light.png -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antfu/node-modules-inspector/01fef40cc0cb45e414d34b6fa588bd05a9e1003c/packages/node-modules-inspector/src/app/public/og.png -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/server/api/metadata.json.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { consola } from 'consola' 3 | import { storageNpmMeta, storageNpmMetaLatest, storagePublint } from '../../../node/storage' 4 | import { createWsServer } from '../../../node/ws' 5 | 6 | consola.restoreAll() 7 | 8 | const ws = createWsServer({ 9 | cwd: process.cwd(), 10 | storageNpmMeta, 11 | storageNpmMetaLatest, 12 | storagePublint, 13 | mode: 'dev', 14 | }).then((ws) => { 15 | // Warm up the payload 16 | setTimeout(() => { 17 | ws.serverFunctions.getPayload() 18 | }, 1) 19 | return ws 20 | }) 21 | 22 | export default eventHandler(async () => { 23 | return await (await ws).getMetadata() 24 | }) 25 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.nuxt/tsconfig.server.json", 3 | "compilerOptions": { 4 | "moduleResolution": "Bundler", 5 | "noEmit": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/state/current.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { payloads } from './payload' 3 | import { query } from './query' 4 | 5 | export const selectedNode = computed({ 6 | get() { 7 | return query.selected ? payloads.main.get(query.selected) : undefined 8 | }, 9 | set(v) { 10 | query.selected = v?.spec 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/state/highlight.ts: -------------------------------------------------------------------------------- 1 | import type { PackageNode } from 'node-modules-tools' 2 | import { payloads } from './payload' 3 | 4 | export type HighlightMode = 'focus' | 'compare' 5 | 6 | export function getCompareHighlight(pkg: PackageNode) { 7 | const a = payloads.compareA.has(pkg) 8 | const b = payloads.compareB.has(pkg) 9 | if (a && b) 10 | return 'both' 11 | if (a) 12 | return 'a' 13 | if (b) 14 | return 'b' 15 | return 'none' 16 | } 17 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/state/settings.ts: -------------------------------------------------------------------------------- 1 | import type { SettingsOptions } from '../../shared/types' 2 | import { useLocalStorage } from '@vueuse/core' 3 | 4 | export const settings = useLocalStorage( 5 | 'node-modules-inspector-settings', 6 | { 7 | graphRender: 'normal', 8 | moduleTypeSimple: false, 9 | moduleTypeRender: 'badge', 10 | deepDependenciesTree: true, 11 | packageDetailsTab: 'dependents', 12 | colorizePackageSize: true, 13 | showInstallSizeBadge: true, 14 | showPublishTimeBadge: false, 15 | showFileComposition: false, 16 | showDependencySourceBadge: 'dev', 17 | showPublintMessages: false, 18 | showThirdPartyServices: false, 19 | treatFauxAsESM: false, 20 | chartColoringMode: 'spectrum', 21 | collapseSidepanel: false, 22 | chartAnimation: true, 23 | }, 24 | { 25 | deep: true, 26 | mergeDefaults: true, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/state/terminal.ts: -------------------------------------------------------------------------------- 1 | import type { Terminal } from '@xterm/xterm' 2 | import { useLocalStorage } from '@vueuse/core' 3 | import { shallowRef } from 'vue' 4 | 5 | export const terminal = shallowRef() 6 | export const openTerminal = useLocalStorage('node-inspector-terminal', false) 7 | export const showTerminal = shallowRef(false) 8 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/state/ui.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { query } from './query' 3 | import { settings } from './settings' 4 | 5 | export const isSettingOpen = computed({ 6 | get() { 7 | return query.selected === '~settings' 8 | }, 9 | set(value) { 10 | if (!value && query.selected === '~settings') 11 | query.selected = '' 12 | else if (value) 13 | query.selected = '~settings' 14 | }, 15 | }) 16 | export const isFiltersOpen = computed({ 17 | get() { 18 | return query.selected === '~filters' 19 | }, 20 | set(value) { 21 | if (!value && query.selected === '~filters') 22 | query.selected = '' 23 | else if (value) 24 | query.selected = '~filters' 25 | }, 26 | }) 27 | 28 | export const isSidepanelCollapsed = computed(() => { 29 | return settings.value.collapseSidepanel && !isFiltersOpen.value && !isSettingOpen.value 30 | }) 31 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/styles/global.css: -------------------------------------------------------------------------------- 1 | html, body , #__nuxt{ 2 | height: 100vh; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | .bg-dots { 8 | background-image: url('/dot-grid-light.png'); 9 | background-size: 50px; 10 | background-repeat: repeat; 11 | } 12 | 13 | .dark .bg-dots, 14 | .bg-dots.dark { 15 | color-scheme: dark; 16 | background-color: black; 17 | background-image: url('/dot-grid-dark.png'); 18 | } 19 | 20 | html { 21 | --at-apply: bg-base font-sans; 22 | } 23 | body { 24 | --at-apply: color-base; 25 | } 26 | 27 | summary::-webkit-details-marker { 28 | display: none; 29 | } 30 | 31 | /* Animations */ 32 | @keyframes spin-reverse { 33 | from { 34 | transform: rotate(1080deg); 35 | } 36 | to { 37 | transform: rotate(0deg); 38 | } 39 | } 40 | .animate-spin-reverse, 41 | .hover\:animate-spin-reverse:hover { 42 | animation: spin-reverse 2.5s cubic-bezier(0.37, 0, 0.63, 1) infinite; 43 | } 44 | 45 | /* Xterm */ 46 | .terminal.xterm { 47 | height: 100%; 48 | padding: 0 1.5em; 49 | } 50 | 51 | .terminal.xterm .xterm-rows { 52 | --uno: font-mono; 53 | } 54 | 55 | /* Overrides Floating Vue */ 56 | .v-popper--theme-dropdown .v-popper__inner, 57 | .v-popper--theme-tooltip .v-popper__inner { 58 | --at-apply: bg-tooltip color-base font-sans rounded border border-base shadow dark:shadow-2xl; 59 | box-shadow: 0 6px 30px #0000001a; 60 | } 61 | 62 | .v-popper--theme-tooltip .v-popper__inner { 63 | --at-apply: text-sm; 64 | } 65 | 66 | .v-popper--theme-tooltip { 67 | max-width: 20rem; 68 | } 69 | 70 | .v-popper--theme-tooltip .v-popper__arrow-inner, 71 | .v-popper--theme-dropdown .v-popper__arrow-inner { 72 | visibility: visible; 73 | --at-apply: border-white dark:border-#111; 74 | } 75 | 76 | .v-popper--theme-tooltip .v-popper__arrow-outer, 77 | .v-popper--theme-dropdown .v-popper__arrow-outer { 78 | --at-apply: border-base; 79 | } 80 | 81 | .v-popper--theme-tooltip.v-popper--shown, 82 | .v-popper--theme-tooltip.v-popper--shown * { 83 | transition: none !important; 84 | } 85 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/types/backend.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import type { NodeModulesInspectorPayload, ServerFunctions } from '../../shared/types' 3 | 4 | export interface ReferencePayloadFunctions { 5 | getReferencePayload?: (hash?: string) => Promise 6 | getReferencePayloadList?: () => Promise<{ hash: string, timestamp: number, note: string }[]> 7 | saveReferencePayload?: (payload: NodeModulesInspectorPayload, note: string) => Promise 8 | removeReferencePayload?: (hash: string) => Promise 9 | } 10 | 11 | export type Functions = 12 | & Partial 13 | & Pick 14 | & ReferencePayloadFunctions 15 | 16 | export interface Backend { 17 | name: string 18 | status: Ref<'idle' | 'connecting' | 'connected' | 'error'> 19 | connectionError: Ref 20 | connect: () => Promise | void 21 | isDynamic?: boolean 22 | functions: Functions 23 | } 24 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/types/chart.ts: -------------------------------------------------------------------------------- 1 | import type { TreeNode } from 'nanovis' 2 | import type { PackageNode } from 'node-modules-tools' 3 | 4 | export type ChartNode = TreeNode 5 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/utils/color.ts: -------------------------------------------------------------------------------- 1 | import { isDark } from '../composables/dark' 2 | 3 | export function getHashColorFromString( 4 | name: string, 5 | opacity: number | string = 1, 6 | ) { 7 | let hash = 0 8 | for (let i = 0; i < name.length; i++) 9 | hash = name.charCodeAt(i) + ((hash << 5) - hash) 10 | const h = hash % 360 11 | return getHsla(h, opacity) 12 | } 13 | 14 | export function getHsla( 15 | hue: number, 16 | opacity: number | string = 1, 17 | ) { 18 | const saturation = isDark.value ? 50 : 65 19 | const lightness = isDark.value ? 60 : 40 20 | return `hsla(${hue}, ${saturation}%, ${lightness}%, ${opacity})` 21 | } 22 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/utils/file-category.ts: -------------------------------------------------------------------------------- 1 | // @unocss-include 2 | 3 | import type { FileCategory } from 'node-modules-tools' 4 | 5 | export const FILE_CATEGORIES_COLOR_BADGE: Record = { 6 | js: 'badge-color-yellow', 7 | ts: 'badge-color-blue', 8 | json: 'badge-color-orange', 9 | other: 'badge-color-gray', 10 | comp: 'badge-color-green', 11 | dts: 'badge-color-blue', 12 | image: 'badge-color-purple', 13 | css: 'badge-color-purple', 14 | html: 'badge-color-violet', 15 | doc: 'badge-color-gray', 16 | test: 'badge-color-orange', 17 | bin: 'badge-color-teal', 18 | map: 'badge-color-gray', 19 | wasm: 'badge-color-purple', 20 | flow: 'badge-color-yellow', 21 | font: 'badge-color-gray', 22 | } 23 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/utils/format.ts: -------------------------------------------------------------------------------- 1 | export function bytesToHumanSize(bytes: number, digits = 2) { 2 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'] 3 | const i = Math.floor(Math.log(bytes) / Math.log(1024)) 4 | if (i === 0) 5 | return ['<1', 'K'] 6 | return [(+(bytes / 1024 ** i).toFixed(digits)).toLocaleString(), sizes[i]] 7 | } 8 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/utils/maps.ts: -------------------------------------------------------------------------------- 1 | import type { PackageNode } from 'node-modules-tools' 2 | 3 | export function buildVersionToPackagesMap(packages: PackageNode[]) { 4 | const map = new Map() 5 | for (const pkg of packages) { 6 | if (!map.has(pkg.name)) 7 | map.set(pkg.name, []) 8 | map.get(pkg.name)!.push(pkg) 9 | } 10 | return map 11 | } 12 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/utils/module-type.ts: -------------------------------------------------------------------------------- 1 | import type { PackageModuleType, PackageNode } from 'node-modules-tools' 2 | import { computed } from 'vue' 3 | import { settings } from '../state/settings' 4 | 5 | export const MODULE_TYPES_FULL = ['dual', 'esm', 'faux', 'cjs', 'dts'] as PackageModuleType[] 6 | export const MODULE_TYPES_FULL_SELECT = ['dual', 'esm', 'faux', 'cjs'] as PackageModuleType[] 7 | 8 | // @unocss-include 9 | export const MODULE_TYPES_COLOR_BADGE = { 10 | esm: 'badge-color-green', 11 | dual: 'badge-color-teal', 12 | cjs: 'badge-color-yellow', 13 | faux: 'badge-color-lime', 14 | dts: 'badge-color-gray', 15 | unknown: 'badge-color-gray', 16 | } 17 | 18 | export const MODULE_TYPES_NAME = { 19 | esm: 'ESM', 20 | dual: 'DUAL', 21 | cjs: 'CJS', 22 | faux: 'FAUX', 23 | dts: 'DTS', 24 | unknown: '?', 25 | } 26 | 27 | export function getModuleType(node: PackageNode | PackageModuleType) { 28 | const type = typeof node === 'string' ? node : node.resolved.module 29 | 30 | if (settings.value.treatFauxAsESM && type === 'faux') 31 | return 'esm' 32 | if (!settings.value.moduleTypeSimple) 33 | return type 34 | 35 | if (type === 'dts') 36 | return 'dts' 37 | if (['cjs', 'faux'].includes(type)) 38 | return 'cjs' 39 | return 'esm' 40 | } 41 | 42 | export function getModuleTypeCounts(nodes: Iterable) { 43 | const counts = Object.fromEntries(MODULE_TYPES_FULL.map(type => [type, 0])) as Record 44 | for (const node of nodes) { 45 | const type = getModuleType(node) 46 | counts[type] += 1 47 | } 48 | return counts 49 | } 50 | 51 | /** 52 | * Currently available module types 53 | */ 54 | export const moduleTypesAvailable = computed(() => { 55 | const baseTypes = new Set(MODULE_TYPES_FULL) 56 | 57 | if (settings.value.treatFauxAsESM) 58 | baseTypes.delete('faux') 59 | if (settings.value.moduleTypeSimple) { 60 | baseTypes.delete('dual') 61 | baseTypes.delete('faux') 62 | } 63 | 64 | return Array.from(baseTypes) 65 | }) 66 | 67 | /** 68 | * Currently available module types, excluding DTS. 69 | */ 70 | export const moduleTypesAvailableSelect = computed(() => { 71 | const baseTypes = new Set(MODULE_TYPES_FULL_SELECT) 72 | 73 | if (settings.value.treatFauxAsESM) 74 | baseTypes.delete('faux') 75 | if (settings.value.moduleTypeSimple) { 76 | baseTypes.delete('dual') 77 | baseTypes.delete('faux') 78 | } 79 | 80 | return Array.from(baseTypes) 81 | }) 82 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/utils/package-json.ts: -------------------------------------------------------------------------------- 1 | import type { PackageNode } from 'node-modules-tools' 2 | import { normalizePkgAuthors, normalizePkgFundings, normalizePkgLicense, normalizePkgRepository } from 'node-modules-tools/utils' 3 | 4 | function weakCachedFunction(fn: (arg: T) => R): (arg: T) => R { 5 | const cache = new WeakMap() 6 | 7 | return (arg: T) => { 8 | if (cache.has(arg)) 9 | return cache.get(arg)! 10 | const result = fn(arg) 11 | cache.set(arg, result) 12 | return result 13 | } 14 | } 15 | 16 | export const getAuthors = weakCachedFunction((pkg: PackageNode) => normalizePkgAuthors(pkg.resolved.packageJson)) 17 | export const getRepository = weakCachedFunction((pkg: PackageNode) => normalizePkgRepository(pkg.resolved.packageJson)) 18 | export const getFundings = weakCachedFunction((pkg: PackageNode) => normalizePkgFundings(pkg.resolved.packageJson)) 19 | export const getLicense = weakCachedFunction((pkg: PackageNode) => normalizePkgLicense(pkg.resolved.packageJson)) 20 | 21 | export const getPackageData = weakCachedFunction((pkg: PackageNode) => { 22 | return { 23 | license: getLicense(pkg), 24 | homepage: pkg.resolved.packageJson.homepage, 25 | engines: pkg.resolved.packageJson.engines, 26 | authors: getAuthors(pkg), 27 | repository: getRepository(pkg)?.url, 28 | fundings: getFundings(pkg), 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/utils/search-parser.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { parseSearch, serializedSearch } from './search-parser' 3 | 4 | describe('parse', () => { 5 | it('basic', () => { 6 | expect(parseSearch('')).toEqual({ text: '' }) 7 | expect(parseSearch('foo bar')).toEqual({ text: 'foo bar' }) 8 | }) 9 | 10 | it('author simple', () => { 11 | expect(parseSearch('author:antfu')) 12 | .toEqual({ text: '', author: [/antfu/gi] }) 13 | expect(parseSearch('author:')) 14 | .toEqual({ text: '' }) 15 | expect(parseSearch('Hello author:antfu')) 16 | .toEqual({ text: 'Hello', author: [/antfu/gi] }) 17 | expect(parseSearch('Hello author:antfu World author:ilyaliao')) 18 | .toEqual({ text: 'Hello World', author: [/antfu/gi, /ilyaliao/gi] }) 19 | }) 20 | 21 | it('author with quote', () => { 22 | expect(parseSearch('author:"Anthony Fu"')) 23 | .toEqual({ text: '', author: [/Anthony Fu/gi] }) 24 | expect(parseSearch('author:""')) 25 | .toEqual({ text: '' }) 26 | expect(parseSearch(`Hello author:\`Anthony Fu\` World author:"Ilya Liao"`)) 27 | .toEqual({ text: 'Hello World', author: [/Anthony Fu/gi, /Ilya Liao/gi] }) 28 | }) 29 | 30 | it('mixed fields', () => { 31 | expect(parseSearch('! Some name license:MIT author:"Anthony Fu" license:Apache-2.0 not:foo')) 32 | .toEqual({ 33 | invert: true, 34 | text: 'Some name', 35 | license: [/MIT/gi, /Apache-2\.0/gi], 36 | author: [/Anthony Fu/gi], 37 | not: [/foo/gi], 38 | }) 39 | }) 40 | }) 41 | 42 | describe('serialize', () => { 43 | it('basic', () => { 44 | expect(serializedSearch(parseSearch(''))) 45 | .toEqual('') 46 | 47 | expect(serializedSearch(parseSearch('foo bar'))) 48 | .toEqual('foo bar') 49 | 50 | expect(serializedSearch(parseSearch('foo author:foo bar'))) 51 | .toEqual('foo bar author:foo') 52 | 53 | expect(serializedSearch(parseSearch('! author:foo bar license:MIT author:"Anthony Fu" license:ISC'))) 54 | .toEqual('! bar author:foo author:"Anthony Fu" license:MIT license:ISC') 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/utils/search-parser.ts: -------------------------------------------------------------------------------- 1 | export interface ParsedSearchResult { 2 | text: string 3 | invert?: boolean 4 | not?: RegExp[] 5 | license?: RegExp[] 6 | author?: RegExp[] 7 | } 8 | 9 | const RE_COLLON_FIELDS = /\b(\w+):("[^"]*"|'[^']*'|`[^`]*`|\S*)/g 10 | 11 | function createRegExp(input: string, flags = 'gi') { 12 | const escaped = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') 13 | return new RegExp(escaped, flags) 14 | } 15 | 16 | function unescapeRegExp(input: RegExp) { 17 | return input.source.replace(/\\(.)/g, '$1') 18 | } 19 | 20 | export function parseSearch(input: string) { 21 | let text = input 22 | let invert = false 23 | const exclude: RegExp[] = [] 24 | const license: RegExp[] = [] 25 | const author: RegExp[] = [] 26 | let removal: [number, number][] = [] 27 | 28 | if (text.startsWith('!')) { 29 | invert = true 30 | text = text.slice(1) 31 | } 32 | 33 | for (const match of text.matchAll(RE_COLLON_FIELDS) || []) { 34 | let [_, field, value] = match 35 | 36 | // de-quote 37 | if (value.startsWith('"') && value.endsWith('"')) 38 | value = value.slice(1, -1) 39 | else if (value.startsWith('\'') && value.endsWith('\'')) 40 | value = value.slice(1, -1) 41 | else if (value.startsWith('`') && value.endsWith('`')) 42 | value = value.slice(1, -1) 43 | 44 | switch (field) { 45 | case 'not': 46 | if (value) 47 | exclude.push(createRegExp(value, 'gi')) 48 | break 49 | case 'license': 50 | if (value) 51 | license.push(createRegExp(value, 'gi')) 52 | break 53 | case 'author': 54 | if (value) 55 | author.push(createRegExp(value, 'gi')) 56 | break 57 | default: 58 | continue 59 | } 60 | 61 | removal.push([match.index!, match.index! + match[0].length]) 62 | } 63 | 64 | if (removal.length) { 65 | removal = removal.sort((a, b) => b[0] - a[0]) 66 | for (const [start, end] of removal) { 67 | text = text.slice(0, start) + text.slice(end) 68 | } 69 | } 70 | 71 | text = text.replace(/\s+/g, ' ').trim() 72 | 73 | const result: ParsedSearchResult = { 74 | text, 75 | } 76 | if (invert) 77 | result.invert = true 78 | if (exclude.length) 79 | result.not = exclude 80 | if (license.length) 81 | result.license = license 82 | if (author.length) 83 | result.author = author 84 | 85 | return result 86 | } 87 | 88 | export function serializedSearch(search: ParsedSearchResult) { 89 | let result = search.text 90 | 91 | function quote(value: string) { 92 | if (value.includes(' ')) { 93 | return `"${value}"` 94 | } 95 | return value 96 | } 97 | 98 | if (search.not?.length) 99 | result += ` ${search.not.map(r => `not:${quote(unescapeRegExp(r))}`).join(' ')}` 100 | if (search.author?.length) 101 | result += ` ${search.author.map(r => `author:${quote(unescapeRegExp(r))}`).join(' ')}` 102 | if (search.license?.length) 103 | result += ` ${search.license.map(r => `license:${quote(unescapeRegExp(r))}`).join(' ')}` 104 | 105 | if (search.invert) 106 | result = `! ${result}` 107 | 108 | return result 109 | } 110 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/utils/semver.ts: -------------------------------------------------------------------------------- 1 | import semver from 'semver' 2 | 3 | export interface ParsedSemver { 4 | valid: boolean 5 | raw: string 6 | highest?: string 7 | lowest?: string 8 | parts?: string[] 9 | bare?: string[] 10 | } 11 | 12 | const SemverParseCache = new Map() 13 | 14 | export function parseSemverRange(range: string) { 15 | if (SemverParseCache.has(range)) 16 | return SemverParseCache.get(range)! 17 | 18 | const result: ParsedSemver = { 19 | valid: false, 20 | raw: range, 21 | } 22 | SemverParseCache.set(range, result) 23 | 24 | if (!semver.validRange(range)) { 25 | return result 26 | } 27 | 28 | const parts = range 29 | .split(/\|\|/g) 30 | .map(i => i.replace(/\s+/g, '').replace(/(\.[0x*])+$/g, '')) 31 | 32 | const partsBare = parts 33 | .map(i => i.replace(/^(?:\^|>=|>)/g, '').replace(/\.[*x]$/, '').trim()) 34 | .map((i) => { 35 | const parts = i.split(/\./) 36 | if (parts.length === 1) 37 | return `${i}.0.0` 38 | if (parts.length === 2) 39 | return `${i}.0` 40 | return i 41 | }) 42 | .sort(compareSemver) 43 | 44 | const highest = partsBare.at(-1)! 45 | const lowest = partsBare.at(0)! 46 | if (!semver.valid(highest) || !semver.valid(lowest)) { 47 | return result 48 | } 49 | 50 | result.valid = true 51 | result.highest = highest 52 | result.lowest = lowest 53 | result.parts = parts 54 | result.bare = partsBare 55 | return result 56 | } 57 | 58 | export function compareSemver(a: string, b: string) { 59 | if (a === b) 60 | return 0 61 | try { 62 | return semver.compare(a, b) 63 | } 64 | catch (e) { 65 | console.error('Failed to compare semver ', e) 66 | return 0 67 | } 68 | } 69 | 70 | export function compareSemverRange(a = '*', b = '*') { 71 | if (a === b) 72 | return 0 73 | const parsedA = parseSemverRange(a) 74 | const parsedB = parseSemverRange(b) 75 | const compare = compareSemver(parsedB.lowest || '*', parsedA.lowest || '*') 76 | if (compare !== 0) 77 | return compare 78 | return ((parsedB.parts?.length || 0) - (parsedA.parts?.length || 0)) 79 | } 80 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/app/webcontainer/constants.ts: -------------------------------------------------------------------------------- 1 | import { WEBCONTAINER_SERVER_CODE as _WEBCONTAINER_SERVER_CODE } from '#build/webcontainer-server-code' 2 | 3 | export const CODE_SERVER = _WEBCONTAINER_SERVER_CODE as string 4 | 5 | export const CODE_PACKAGE_JSON = JSON.stringify({ 6 | name: 'node-modules-inspector-workspace', 7 | private: true, 8 | type: 'module', 9 | }) 10 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/dirs.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | 3 | export const distDir = fileURLToPath(new URL('../dist/public', import.meta.url)) 4 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/node/config.ts: -------------------------------------------------------------------------------- 1 | import type { NodeModulesInspectorConfig } from '../shared/types' 2 | 3 | export function defineConfig(config: NodeModulesInspectorConfig): NodeModulesInspectorConfig { 4 | return config 5 | } 6 | 7 | export type { NodeModulesInspectorConfig } 8 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/node/constants.ts: -------------------------------------------------------------------------------- 1 | import c from 'ansis' 2 | 3 | export const MARK_CHECK = c.green('✔') 4 | export const MARK_INFO = c.blue('ℹ') 5 | export const MARK_ERROR = c.red('✖') 6 | export const MARK_NODE = '⬢' 7 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/node/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config' 2 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/node/server.ts: -------------------------------------------------------------------------------- 1 | import type { Buffer } from 'node:buffer' 2 | import type { CreateWsServerOptions } from './ws' 3 | import { readFile, stat } from 'node:fs/promises' 4 | import { createServer } from 'node:http' 5 | import { createApp, eventHandler, serveStatic, toNodeListener } from 'h3' 6 | import { lookup } from 'mrmime' 7 | import { join } from 'pathe' 8 | import { distDir } from '../dirs' 9 | import { createWsServer } from './ws' 10 | 11 | export async function createHostServer(options: CreateWsServerOptions) { 12 | const app = createApp() 13 | 14 | const ws = await createWsServer(options) 15 | 16 | const fileMap = new Map | undefined>>() 17 | const readCachedFile = (id: string) => { 18 | if (!fileMap.has(id)) 19 | fileMap.set(id, readFile(id).catch(() => undefined)) 20 | return fileMap.get(id) 21 | } 22 | 23 | app.use('/api/metadata.json', eventHandler(async (event) => { 24 | event.node.res.setHeader('Content-Type', 'application/json') 25 | return event.node.res.end(JSON.stringify(await ws.getMetadata())) 26 | })) 27 | 28 | app.use('/', eventHandler(async (event) => { 29 | const result = await serveStatic(event, { 30 | fallthrough: true, 31 | getContents: id => readCachedFile(join(distDir, id)), 32 | getMeta: async (id) => { 33 | const stats = await stat(join(distDir, id)).catch(() => {}) 34 | if (!stats || !stats.isFile()) 35 | return 36 | return { 37 | type: lookup(id), 38 | size: stats.size, 39 | mtime: stats.mtimeMs, 40 | } 41 | }, 42 | }) 43 | 44 | if (result === false) 45 | return readCachedFile(join(distDir, 'index.html')) 46 | })) 47 | 48 | return { 49 | server: createServer(toNodeListener(app)), 50 | ws, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/node/storage.ts: -------------------------------------------------------------------------------- 1 | import type { NpmMeta, NpmMetaLatest, PublintMessage } from 'node-modules-tools' 2 | import process from 'node:process' 3 | import { join } from 'pathe' 4 | import { createStorage } from 'unstorage' 5 | import driverFs from 'unstorage/drivers/fs-lite' 6 | 7 | export const storageNpmMeta = createStorage({ 8 | driver: driverFs({ 9 | base: join(process.cwd(), 'node_modules/.cache/node-modules-inspector/npm-meta'), 10 | }), 11 | }) 12 | 13 | export const storageNpmMetaLatest = createStorage({ 14 | driver: driverFs({ 15 | base: join(process.cwd(), 'node_modules/.cache/node-modules-inspector/npm-meta-latest'), 16 | }), 17 | }) 18 | 19 | export const storagePublint = createStorage({ 20 | driver: driverFs({ 21 | base: join(process.cwd(), 'node_modules/.cache/node-modules-inspector/publint'), 22 | }), 23 | }) 24 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/node/webcontainer/server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the entry point that we bundles with Rollup into a single file. 3 | * 4 | * The dist is located at `runtime/webcontainer-server.mjs` by `pnpm run wc:prepare`. 5 | * 6 | * The dist will be send to WebConainter to create the server to communicate with the main app. 7 | */ 8 | 9 | import type { NpmMeta, NpmMetaLatest } from 'node-modules-tools' 10 | import process from 'node:process' 11 | import { stringify } from 'structured-clone-es' 12 | import { createStorage } from 'unstorage' 13 | import driverMemory from 'unstorage/drivers/memory' 14 | import { WEBCONTAINER_STDOUT_PREFIX } from '../../shared/constants' 15 | import { createServerFunctions } from '../rpc' 16 | 17 | const rpc = createServerFunctions({ 18 | cwd: process.cwd(), 19 | storageNpmMeta: createStorage({ driver: driverMemory() }), 20 | storageNpmMetaLatest: createStorage({ driver: driverMemory() }), 21 | mode: 'dev', 22 | }) 23 | 24 | async function run() { 25 | // eslint-disable-next-line unimport/auto-insert 26 | const heartbeat = setInterval(() => { 27 | console.log(WEBCONTAINER_STDOUT_PREFIX + stringify({ status: 'heartbeat', heartbeat: Date.now() })) 28 | }, 100) 29 | 30 | try { 31 | console.log(WEBCONTAINER_STDOUT_PREFIX + stringify(await rpc.getPayload())) 32 | } 33 | catch (err) { 34 | console.log(WEBCONTAINER_STDOUT_PREFIX + stringify({ status: 'error', error: err })) 35 | } 36 | finally { 37 | clearInterval(heartbeat) 38 | } 39 | } 40 | 41 | run() 42 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/node/ws.ts: -------------------------------------------------------------------------------- 1 | import type { ChannelOptions } from 'birpc' 2 | import type { WebSocket } from 'ws' 3 | import type { ConnectionMeta } from '../shared/types' 4 | import type { CreateServerFunctionsOptions } from './rpc' 5 | import c from 'ansis' 6 | import { createBirpcGroup } from 'birpc' 7 | import { getPort } from 'get-port-please' 8 | import { parse, stringify } from 'structured-clone-es' 9 | import { WebSocketServer } from 'ws' 10 | import { MARK_CHECK } from './constants' 11 | import { createServerFunctions } from './rpc' 12 | 13 | export interface CreateWsServerOptions extends CreateServerFunctionsOptions { 14 | cwd: string 15 | port?: number 16 | } 17 | 18 | export async function createWsServer(options: CreateWsServerOptions) { 19 | const port = options.port ?? await getPort({ port: 7812, random: true }) 20 | const wss = new WebSocketServer({ 21 | port, 22 | }) 23 | const wsClients = new Set() 24 | 25 | const serverFunctions = createServerFunctions(options) 26 | const rpc = createBirpcGroup( 27 | serverFunctions, 28 | [], 29 | { 30 | onError(error, name) { 31 | console.error(c.red`⬢ RPC error on executing "${c.bold(name)}":`) 32 | console.error(error) 33 | throw error 34 | }, 35 | timeout: 120_000, 36 | }, 37 | ) 38 | 39 | wss.on('connection', (ws) => { 40 | wsClients.add(ws) 41 | const channel: ChannelOptions = { 42 | post: d => ws.send(d), 43 | on: (fn) => { 44 | ws.on('message', (data) => { 45 | fn(data) 46 | }) 47 | }, 48 | serialize: stringify, 49 | deserialize: parse, 50 | } 51 | rpc.updateChannels((c) => { 52 | c.push(channel) 53 | }) 54 | ws.on('close', () => { 55 | wsClients.delete(ws) 56 | rpc.updateChannels((c) => { 57 | const index = c.indexOf(channel) 58 | if (index >= 0) 59 | c.splice(index, 1) 60 | }) 61 | }) 62 | 63 | console.log(c.green`${MARK_CHECK} Websocket client connected`) 64 | }) 65 | 66 | const getMetadata = async (): Promise => { 67 | return { 68 | backend: 'websocket', 69 | websocket: port, 70 | } 71 | } 72 | 73 | return { 74 | port, 75 | wss, 76 | rpc, 77 | serverFunctions, 78 | getMetadata, 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/shared/constants.ts: -------------------------------------------------------------------------------- 1 | export const WS_EVENT_NAME = 'node-modules-inspector' 2 | 3 | export const WEBCONTAINER_STDOUT_PREFIX = '::node-modules-inspector::' 4 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/shared/filters.ts: -------------------------------------------------------------------------------- 1 | import type { PackageModuleType } from 'node-modules-tools' 2 | 3 | export interface FilterOptions { 4 | search: string 5 | modules: null | PackageModuleType[] 6 | focus: null | string[] 7 | why: null | string[] 8 | sourceType: null | 'prod' | 'dev' 9 | depths: null | (number | string)[] 10 | clusters: null | string[] 11 | clustersMode: 'and' | 'or' 12 | 13 | excludes: null | string[] 14 | excludeDts: boolean 15 | excludeOptional: boolean 16 | excludePrivate: boolean 17 | excludeWorkspace: boolean 18 | 19 | compareA: null | string[] 20 | compareB: null | string[] 21 | } 22 | 23 | export interface FilterSchema { 24 | type: StringConstructor | ArrayConstructor | BooleanConstructor 25 | default: Type 26 | category: 'select' | 'exclude' | 'compare' | 'option' 27 | } 28 | 29 | export const FILTERS_SCHEMA: { 30 | [x in keyof FilterOptions]: FilterSchema 31 | } = { 32 | search: { type: String, default: '', category: 'select' }, 33 | modules: { type: Array, default: null, category: 'select' }, 34 | focus: { type: Array, default: null, category: 'select' }, 35 | why: { type: Array, default: null, category: 'select' }, 36 | sourceType: { type: String, default: null, category: 'select' }, 37 | depths: { type: Array, default: null, category: 'select' }, 38 | clusters: { type: Array, default: null, category: 'select' }, 39 | 40 | clustersMode: { type: String, default: 'or', category: 'option' }, 41 | 42 | // Compare 43 | compareA: { type: Array, default: [], category: 'compare' }, 44 | compareB: { type: Array, default: [], category: 'compare' }, 45 | 46 | // Excludes 47 | excludes: { type: Array, default: null, category: 'exclude' }, 48 | excludeDts: { type: Boolean, default: true, category: 'exclude' }, 49 | excludeOptional: { type: Boolean, default: true, category: 'exclude' }, 50 | excludePrivate: { type: Boolean, default: false, category: 'exclude' }, 51 | excludeWorkspace: { type: Boolean, default: import.meta.env.BACKEND === 'webcontainer', category: 'exclude' }, 52 | } 53 | -------------------------------------------------------------------------------- /packages/node-modules-inspector/src/shared/utils.ts: -------------------------------------------------------------------------------- 1 | import type { NpmMetaLatest } from 'node-modules-tools' 2 | 3 | export function isNpmMetaLatestValid(meta?: NpmMetaLatest): boolean { 4 | if (!meta) 5 | return false 6 | if (meta.vaildUntil < Date.now()) 7 | return false 8 | return !!meta.publishedAt 9 | } 10 | -------------------------------------------------------------------------------- /packages/node-modules-tools/README.md: -------------------------------------------------------------------------------- 1 | # node-modules-tools 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![bundle][bundle-src]][bundle-href] 6 | [![JSDocs][jsdocs-src]][jsdocs-href] 7 | [![License][license-src]][license-href] 8 | 9 | ## Sponsors 10 | 11 |

12 | 13 | 14 | 15 |

16 | 17 | ## License 18 | 19 | [MIT](./LICENSE) License © [Anthony Fu](https://github.com/antfu) 20 | 21 | 22 | 23 | [npm-version-src]: https://img.shields.io/npm/v/node-modules-tools?style=flat&colorA=080f12&colorB=1fa669 24 | [npm-version-href]: https://npmjs.com/package/node-modules-tools 25 | [npm-downloads-src]: https://img.shields.io/npm/dm/node-modules-tools?style=flat&colorA=080f12&colorB=1fa669 26 | [npm-downloads-href]: https://npmjs.com/package/node-modules-tools 27 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/node-modules-tools?style=flat&colorA=080f12&colorB=1fa669&label=minzip 28 | [bundle-href]: https://bundlephobia.com/result?p=node-modules-tools 29 | [license-src]: https://img.shields.io/github/license/antfu/node-modules-tools.svg?style=flat&colorA=080f12&colorB=1fa669 30 | [license-href]: https://github.com/antfu/node-modules-tools/blob/main/LICENSE 31 | [jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669 32 | [jsdocs-href]: https://www.jsdocs.io/package/node-modules-tools 33 | -------------------------------------------------------------------------------- /packages/node-modules-tools/build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | entries: [ 5 | 'src/index', 6 | 'src/utils', 7 | 'src/constants', 8 | ], 9 | declaration: true, 10 | clean: true, 11 | rollup: { 12 | inlineDependencies: true, 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /packages/node-modules-tools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-modules-tools", 3 | "type": "module", 4 | "version": "0.6.8", 5 | "description": "Tools for inspecting node_modules", 6 | "author": "Anthony Fu ", 7 | "license": "MIT", 8 | "funding": "https://github.com/sponsors/antfu", 9 | "homepage": "https://github.com/antfu/node-modules-inspector#readme", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/antfu/node-modules-inspector.git", 13 | "directory": "packages/node-modules-tools" 14 | }, 15 | "bugs": "https://github.com/antfu/node-modules-inspector/issues", 16 | "keywords": [], 17 | "sideEffects": false, 18 | "exports": { 19 | ".": "./dist/index.mjs", 20 | "./utils": "./dist/utils.mjs", 21 | "./constants": "./dist/constants.mjs" 22 | }, 23 | "main": "./dist/index.mjs", 24 | "module": "./dist/index.mjs", 25 | "types": "./dist/index.d.mts", 26 | "files": [ 27 | "dist" 28 | ], 29 | "scripts": { 30 | "build": "unbuild", 31 | "stub": "unbuild --stub", 32 | "prepublishOnly": "nr build", 33 | "start": "tsx src/index.ts", 34 | "test": "vitest" 35 | }, 36 | "dependencies": { 37 | "js-yaml": "catalog:deps", 38 | "p-limit": "catalog:deps", 39 | "package-manager-detector": "catalog:deps", 40 | "pathe": "catalog:deps", 41 | "pkg-types": "catalog:types", 42 | "publint": "catalog:deps", 43 | "semver": "catalog:deps", 44 | "tinyexec": "catalog:deps" 45 | }, 46 | "devDependencies": { 47 | "@pnpm/list": "catalog:types", 48 | "@pnpm/types": "catalog:types", 49 | "@types/js-yaml": "catalog:types", 50 | "@types/stream-json": "catalog:types", 51 | "stream-json": "catalog:deps", 52 | "unbuild": "catalog:bundling" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/node-modules-tools/src/agent-entry/detect.ts: -------------------------------------------------------------------------------- 1 | import type { AgentName } from 'package-manager-detector' 2 | import type { BaseOptions } from '../types' 3 | import { detect } from 'package-manager-detector' 4 | 5 | export async function getPackageManager(options: BaseOptions): Promise { 6 | const manager = await detect({ 7 | cwd: options.cwd, 8 | }) 9 | if (!manager) 10 | throw new Error('Cannot detect package manager in the current path') 11 | 12 | return manager.name 13 | } 14 | -------------------------------------------------------------------------------- /packages/node-modules-tools/src/agent-entry/index.ts: -------------------------------------------------------------------------------- 1 | export * from './detect' 2 | export * from './list' 3 | -------------------------------------------------------------------------------- /packages/node-modules-tools/src/agents/npm/index.ts: -------------------------------------------------------------------------------- 1 | export { listPackageDependencies } from './list' 2 | -------------------------------------------------------------------------------- /packages/node-modules-tools/src/agents/pnpm/index.ts: -------------------------------------------------------------------------------- 1 | export { listPackageDependencies } from './list' 2 | -------------------------------------------------------------------------------- /packages/node-modules-tools/src/constants.ts: -------------------------------------------------------------------------------- 1 | import type { PackageModuleType } from './types' 2 | 3 | export const PackageModuleTypes: readonly PackageModuleType[] = Object.freeze(['cjs', 'esm', 'dual', 'faux', 'dts']) 4 | 5 | export const CLUSTER_DEP_PROD = 'dep:prod' 6 | export const CLUSTER_DEP_DEV = 'dep:dev' 7 | export const CLUSTER_DEP_OPTIONAL = 'dep:optional' 8 | -------------------------------------------------------------------------------- /packages/node-modules-tools/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './agent-entry' 2 | export * from './constants' 3 | 4 | export { listPackageDependencies } from './list' 5 | export { resolvePackage } from './resolve' 6 | 7 | export * from './types' 8 | export * from './utils' 9 | -------------------------------------------------------------------------------- /packages/node-modules-tools/src/json-parse-stream.ts: -------------------------------------------------------------------------------- 1 | import StreamJSON from 'stream-json' 2 | import Assembler from 'stream-json/Assembler' 3 | 4 | export class JsonParseStreamError extends Error { 5 | constructor( 6 | message: string, 7 | public data: any, 8 | ) { 9 | super(message) 10 | } 11 | } 12 | 13 | export function parseJsonStream( 14 | stream: NodeJS.ReadableStream, 15 | ): Promise { 16 | const assembler = new Assembler() 17 | const parser = StreamJSON.parser() 18 | 19 | return new Promise((resolve) => { 20 | parser.on('data', (chunk) => { 21 | // @ts-expect-error casting 22 | assembler[chunk.name]?.(chunk.value) 23 | }) 24 | stream.pipe(parser) 25 | parser.on('end', () => { 26 | resolve(assembler.current) 27 | }) 28 | }) 29 | } 30 | 31 | export function parseJsonStreamWithConcatArrays( 32 | stream: NodeJS.ReadableStream, 33 | sourceName: string, 34 | ): Promise { 35 | const assembler = new Assembler() 36 | const parser = StreamJSON.parser({ 37 | jsonStreaming: true, 38 | }) 39 | 40 | const values: T[] = [] 41 | 42 | return new Promise((resolve) => { 43 | parser.on('data', (chunk) => { 44 | // @ts-expect-error casting 45 | assembler[chunk.name]?.(chunk.value) 46 | if (assembler.done) { 47 | if (!Array.isArray(assembler.current)) { 48 | console.error(`Failed to parse JSON output from ${sourceName}`) 49 | // eslint-disable-next-line no-console 50 | console.dir(assembler.current, { depth: 3 }) 51 | throw new JsonParseStreamError(`Failed to parse JSON output from ${sourceName}`, assembler.current) 52 | } 53 | values.push(...assembler.current) 54 | } 55 | }) 56 | stream.pipe(parser) 57 | parser.on('end', () => { 58 | resolve(values) 59 | }) 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /packages/node-modules-tools/src/list.ts: -------------------------------------------------------------------------------- 1 | import type { ListPackageDependenciesOptions, ListPackageDependenciesResult } from './types' 2 | import pLimit from 'p-limit' 3 | import { getPackageManager } from './agent-entry/detect' 4 | import { listPackageDependenciesRaw } from './agent-entry/list' 5 | import { resolvePackage } from './resolve' 6 | 7 | export async function listPackageDependencies( 8 | options: ListPackageDependenciesOptions, 9 | ): Promise { 10 | const packageManager = await getPackageManager(options) 11 | const result = await listPackageDependenciesRaw(packageManager, options) as ListPackageDependenciesResult 12 | const limit = pLimit(10) 13 | await Promise.all(Array.from(result.packages.values()).map(pkg => limit(() => resolvePackage(packageManager, pkg, options)))) 14 | return result 15 | } 16 | -------------------------------------------------------------------------------- /packages/node-modules-tools/src/resolve.ts: -------------------------------------------------------------------------------- 1 | import type { AgentName } from 'package-manager-detector' 2 | import type { PackageJson } from 'pkg-types' 3 | import type { BaseOptions, PackageNode, PackageNodeBase } from './types' 4 | import { existsSync } from 'node:fs' 5 | import { readFile } from 'node:fs/promises' 6 | import { objectPick } from '@antfu/utils' 7 | import { join } from 'pathe' 8 | import { analyzePackageModuleType } from './analyze-esm' 9 | import { getPackageInstallSize } from './size' 10 | 11 | // @keep-unique 12 | // @keep-sorted 13 | export const PACKAGE_JSON_KEYS = [ 14 | 'author', 15 | 'authors', 16 | 'bin', 17 | 'bugs', 18 | 'dependencies', 19 | 'description', 20 | 'devDependencies', 21 | 'engines', 22 | 'exports', 23 | 'funding', 24 | 'fundings', 25 | 'homepage', 26 | 'imports', 27 | 'keywords', 28 | 'license', 29 | 'licenses', 30 | 'main', 31 | 'module', 32 | 'name', 33 | 'optionalDependencies', 34 | 'repository', 35 | 'types', 36 | 'version', 37 | ] satisfies (keyof PackageJson)[] 38 | 39 | /** 40 | * Analyze a package node, and return a resolved package node. 41 | * This function mutates the input package node. 42 | * 43 | * - Set `module` to the resolved module type (cjs, esm, dual, faux, none). 44 | */ 45 | export async function resolvePackage( 46 | _packageManager: AgentName, 47 | pkg: PackageNodeBase, 48 | _options: BaseOptions, 49 | ): Promise { 50 | const _pkg = pkg as unknown as PackageNode 51 | if (_pkg.resolved) 52 | return _pkg 53 | 54 | const path = join(pkg.filepath, 'package.json') 55 | if (existsSync(path)) { 56 | // In cases like optional dependencies, the package might not be installed. 57 | const content = await readFile(path, 'utf-8') 58 | const json = JSON.parse(stripBomTag(content)) as PackageJson 59 | 60 | _pkg.resolved = { 61 | module: analyzePackageModuleType(json), 62 | packageJson: objectPick(json, PACKAGE_JSON_KEYS), 63 | installSize: await getPackageInstallSize(_pkg), 64 | } 65 | } 66 | else { 67 | _pkg.filepath = '' 68 | _pkg.resolved = { 69 | module: 'unknown', 70 | packageJson: {}, 71 | } 72 | } 73 | return _pkg 74 | } 75 | 76 | // strip UTF-8 BOM 77 | // copied from https://github.com/vitejs/vite/blob/90f1420430d7eff45c1e00a300fb0edd972ee0df/packages/vite/src/node/utils.ts#L1322 78 | function stripBomTag(content: string): string { 79 | // eslint-disable-next-line unicorn/number-literal-case 80 | if (content.charCodeAt(0) === 0xfeff) { 81 | return content.slice(1) 82 | } 83 | 84 | return content 85 | } 86 | -------------------------------------------------------------------------------- /packages/node-modules-tools/src/size.ts: -------------------------------------------------------------------------------- 1 | import type { FileCategory, PackageInstallSizeInfo, PackageNodeRaw } from './types' 2 | import fs from 'node:fs/promises' 3 | import { join, relative } from 'node:path' 4 | 5 | export async function getPackageInstallSize( 6 | pkg: PackageNodeRaw, 7 | ): Promise { 8 | if (pkg.workspace || !pkg.name || !pkg.version) 9 | return 10 | if (pkg.name.startsWith('#')) 11 | return 12 | if (pkg.version.match(/^(?:file|link|workspace):/)) 13 | return 14 | if (!pkg.filepath) 15 | return 16 | 17 | const root = pkg.filepath 18 | const files: string[] = [] 19 | 20 | async function traverse(dir: string) { 21 | for (const n of await fs.readdir(dir, { withFileTypes: true })) { 22 | if (n.isFile()) { 23 | files.push(join(dir, n.name)) 24 | } 25 | else if (n.isDirectory()) { 26 | if (n.name.match(/^\.|^node_modules$/)) 27 | continue 28 | await traverse(join(dir, n.name)) 29 | } 30 | } 31 | } 32 | 33 | await traverse(root) 34 | 35 | const types = files.map(f => guessFileCategory(relative(root, f))) 36 | const categories: PackageInstallSizeInfo['categories'] = {} 37 | const sizes = await Promise.all(files.map(getSingleFileSize)) 38 | 39 | let bytes = 0 40 | for (let i = 0; i < files.length; i++) { 41 | bytes += sizes[i] 42 | const type = types[i] 43 | if (!categories[type]) 44 | categories[type] = { bytes: 0, count: 0 } 45 | categories[type].bytes += sizes[i] 46 | categories[type].count += 1 47 | } 48 | 49 | return { 50 | bytes, 51 | categories, 52 | } 53 | } 54 | 55 | async function getSingleFileSize(file: string) { 56 | try { 57 | const stats = await fs.stat(file) 58 | return stats.size 59 | } 60 | catch { 61 | return 0 62 | } 63 | } 64 | 65 | export function guessFileCategory(file: string): FileCategory { 66 | const parts = file.split(/\/|\\/g) 67 | const dirs = parts.slice(0, -1) 68 | const base = parts.at(-1)! 69 | 70 | if (dirs.some(d => d.match(/^(?:test|tests|__tests__)$/))) 71 | return 'test' 72 | if (dirs.some(d => d.match(/^\.\w/)) || base.startsWith('.')) 73 | return 'other' 74 | if (dirs.some(d => d.match(/^(?:bin|binary)$/))) 75 | return 'bin' 76 | 77 | if (base.match(/\.(?:test|tests|spec|specs)\.\w+$/i)) 78 | return 'test' 79 | if (base.match(/\.map$/i)) 80 | return 'map' 81 | if (base.match(/\.d(\.\w+)?\.[cm]?tsx?$/i)) 82 | return 'dts' 83 | if (base.match(/\.exe$/i)) 84 | return 'bin' 85 | if (base.match(/\.(?:css|scss|sass|less)$/i)) 86 | return 'css' 87 | if (base.match(/\.(?:json[c5]?|ya?ml)$/i)) 88 | return 'json' 89 | if (base.match(/\.html?$/i)) 90 | return 'html' 91 | if (base.match(/\.[cm]?jsx?$/i)) 92 | return 'js' 93 | if (base.match(/\.[cm]?tsx?$/i)) 94 | return 'ts' 95 | if (base.match(/\.(?:vue|svelte|astro)$/i)) 96 | return 'comp' 97 | if (base.match(/\.(?:png|jpe?g|gif|svg)$/i)) 98 | return 'image' 99 | if (base.match(/\.(?:md|txt|mdx|markdown|rst)$/i)) 100 | return 'doc' 101 | if (base.match(/\.(?:wasm|wat)$/i)) 102 | return 'wasm' 103 | if (base.match(/\.flow$/i)) 104 | return 'flow' 105 | if (base.match(/\.(?:ttf|otf|woff2?)$/i)) 106 | return 'font' 107 | return 'other' 108 | } 109 | -------------------------------------------------------------------------------- /packages/node-modules-tools/src/types/base.ts: -------------------------------------------------------------------------------- 1 | export interface BaseOptions { 2 | /** 3 | * Current working directory 4 | */ 5 | cwd: string 6 | } 7 | -------------------------------------------------------------------------------- /packages/node-modules-tools/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base' 2 | export * from './list' 3 | export * from './node' 4 | export * from './size' 5 | -------------------------------------------------------------------------------- /packages/node-modules-tools/src/types/list.ts: -------------------------------------------------------------------------------- 1 | import type { BaseOptions } from './base' 2 | import type { PackageNode, PackageNodeBase, PackageNodeRaw } from './node' 3 | 4 | export interface ListPackageDependenciesOptions extends BaseOptions { 5 | /** 6 | * Depeth of the dependency tree 7 | */ 8 | depth: number 9 | /** 10 | * Should it list dependencies of all packages in the monorepo 11 | */ 12 | monorepo: boolean 13 | /** 14 | * Filter if a package should be included and continue traversing 15 | */ 16 | traverseFilter?: (node: PackageNodeRaw) => boolean 17 | /** 18 | * Filter if workspace should be recognized 19 | */ 20 | workspace?: boolean 21 | /** 22 | * Filter whether a package's dependencies should be included 23 | */ 24 | dependenciesFilter?: (node: PackageNodeRaw) => boolean 25 | } 26 | 27 | export interface ListPackageDependenciesRawResult { 28 | root: string 29 | packageManager: string 30 | packageManagerVersion?: string 31 | packages: Map 32 | } 33 | 34 | export interface ListPackageDependenciesBaseResult extends ListPackageDependenciesRawResult { 35 | packages: Map 36 | } 37 | 38 | export interface ListPackageDependenciesResult extends ListPackageDependenciesBaseResult { 39 | packages: Map 40 | } 41 | -------------------------------------------------------------------------------- /packages/node-modules-tools/src/types/node.ts: -------------------------------------------------------------------------------- 1 | import type { PackageJson } from 'pkg-types' 2 | import type { Message as PublintMessage } from 'publint' 3 | import type { PackageInstallSizeInfo } from './size' 4 | 5 | export type { PackageJson, PublintMessage } 6 | 7 | export type PackageModuleTypeSimple = 'cjs' | 'esm' 8 | export type PackageModuleType = 'cjs' | 'esm' | 'dual' | 'faux' | 'dts' | 'unknown' 9 | 10 | export interface PackageNodeRaw { 11 | /** Package Name */ 12 | name: string 13 | /** Version */ 14 | version: string 15 | /** Combined name and version using `@` */ 16 | spec: string 17 | /** Absolute file path of the package */ 18 | filepath: string 19 | /** Direct dependencies of this package */ 20 | dependencies: Set 21 | 22 | /** Is this package from local workspace */ 23 | workspace?: boolean 24 | /** Is this package private */ 25 | private?: boolean 26 | 27 | /** 28 | * Cluster of this package. 29 | * Cluster is a set of labels that will inherits to all nested dependencies. 30 | * 31 | * Typically it would includes things like `dep:dev` or `dep:prod`, 32 | * or catalogs like `catalog:default` or `catalog:custom-named`. 33 | */ 34 | clusters: Set 35 | } 36 | 37 | export interface PackageNodeBase extends PackageNodeRaw { 38 | /** Direct dependents of this package */ 39 | dependents: Set 40 | /** The lowest depth of this package */ 41 | depth: number 42 | /** The shallowest dependent of this package */ 43 | shallowestDependent: Set | undefined 44 | /** All nested dependencies of this package */ 45 | flatDependencies: Set 46 | /** All nested dependents of this package */ 47 | flatDependents: Set 48 | /** All clusters of this package */ 49 | flatClusters: Set 50 | } 51 | 52 | export interface PackageNode extends PackageNodeBase { 53 | resolved: { 54 | module: PackageModuleType 55 | packageJson: PackageJson 56 | installSize?: PackageInstallSizeInfo 57 | npmMeta?: NpmMeta 58 | npmMetaLatest?: NpmMetaLatest 59 | /** 60 | * Result for publint, null for invalid, undefined for not checked yet, empty array for all good 61 | */ 62 | publint?: PublintMessage[] | null 63 | } 64 | } 65 | 66 | export interface NpmMeta { 67 | publishedAt: number 68 | deprecated?: string 69 | } 70 | 71 | /** 72 | * Npm meta of the latest version of a certain package 73 | * Unlike NpmMeta with is immutable, NpmMetaLatest is coupled with time, 74 | * so the `vaildUntil` is used to determine if the meta would need to be updated. 75 | */ 76 | export interface NpmMetaLatest extends NpmMeta { 77 | version: string 78 | /** 79 | * Date when the meta was fetched 80 | */ 81 | fetechedAt: number 82 | /** 83 | * We calculate a smart "TTL" based on how open the package updates. 84 | * If this timestemp is greater than the current time, the meta should be discarded. 85 | */ 86 | vaildUntil: number 87 | } 88 | -------------------------------------------------------------------------------- /packages/node-modules-tools/src/types/size.ts: -------------------------------------------------------------------------------- 1 | export interface PackageInstallSizeInfo { 2 | bytes: number 3 | categories: Partial> 4 | } 5 | 6 | export type FileCategory = 7 | | 'comp' 8 | | 'css' 9 | | 'doc' 10 | | 'dts' 11 | | 'html' 12 | | 'image' 13 | | 'js' 14 | | 'json' 15 | | 'other' 16 | | 'test' 17 | | 'ts' 18 | | 'bin' 19 | | 'map' 20 | | 'wasm' 21 | | 'flow' 22 | | 'font' 23 | -------------------------------------------------------------------------------- /packages/node-modules-tools/src/utils.ts: -------------------------------------------------------------------------------- 1 | export * from './utils/index' 2 | -------------------------------------------------------------------------------- /packages/node-modules-tools/src/utils/filter.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { constructPackageFilter } from './filter' 3 | 4 | describe('constructPackageFilter', () => { 5 | it('exact', () => { 6 | const filter = constructPackageFilter('foo@1.0.0') 7 | expect(filter({ name: 'foo', version: '1.0.0' })).toBe(true) 8 | expect(filter({ name: 'foo', version: '2.0.0' })).toBe(false) 9 | }) 10 | 11 | it('exact with scope', () => { 12 | const filter = constructPackageFilter('@foo/bar@1.0.0') 13 | expect(filter({ name: '@foo/bar', version: '1.0.0' })).toBe(true) 14 | expect(filter({ name: '@foo/bar', version: '2.0.0' })).toBe(false) 15 | }) 16 | 17 | it('any version', () => { 18 | const filter = constructPackageFilter('foo') 19 | expect(filter({ name: 'foo', version: '1.0.0' })).toBe(true) 20 | expect(filter({ name: 'foo', version: '2.0.0' })).toBe(true) 21 | }) 22 | 23 | it('semver range', () => { 24 | const filter = constructPackageFilter('foo@^1.0.0') 25 | expect(filter({ name: 'foo', version: '1.0.0' })).toBe(true) 26 | expect(filter({ name: 'foo', version: '1.1.0' })).toBe(true) 27 | expect(filter({ name: 'foo', version: '2.0.0' })).toBe(false) 28 | }) 29 | 30 | it('prefix', () => { 31 | const filter = constructPackageFilter('foo-*') 32 | expect(filter({ name: 'foo-bar', version: '1.0.0' })).toBe(true) 33 | expect(filter({ name: 'foo-bar', version: '2.0.0' })).toBe(true) 34 | expect(filter({ name: 'bar-foo', version: '1.0.0' })).toBe(false) 35 | }) 36 | 37 | it('wildcard', () => { 38 | const filter = constructPackageFilter('*foo*') 39 | expect(filter({ name: 'foo-bar', version: '1.0.0' })).toBe(true) 40 | expect(filter({ name: 'bar-foo', version: '2.0.0' })).toBe(true) 41 | expect(filter({ name: 'bar', version: '1.0.0' })).toBe(false) 42 | }) 43 | 44 | it('wildcard with dot', () => { 45 | const filter = constructPackageFilter('foo.*') 46 | expect(filter({ name: 'foo.bar', version: '1.0.0' })).toBe(true) 47 | expect(filter({ name: 'foo-bar', version: '2.0.0' })).toBe(false) 48 | }) 49 | 50 | it('wildcard with dot and dash', () => { 51 | const filter = constructPackageFilter('foo.*-bar') 52 | expect(filter({ name: 'foo.bar-bar', version: '1.0.0' })).toBe(true) 53 | expect(filter({ name: 'foo-bar', version: '2.0.0' })).toBe(false) 54 | }) 55 | 56 | it('wildcard with dot and dash and wildcard', () => { 57 | const filter = constructPackageFilter('foo.*-bar*') 58 | expect(filter({ name: 'foo.bar-bar', version: '1.0.0' })).toBe(true) 59 | expect(filter({ name: 'foo-bar', version: '2.0.0' })).toBe(false) 60 | expect(filter({ name: 'foo.bar', version: '1.0.0' })).toBe(false) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /packages/node-modules-tools/src/utils/filter.ts: -------------------------------------------------------------------------------- 1 | import { satisfies } from 'semver' 2 | 3 | export interface PackageNodeLike { 4 | name: string 5 | version: string 6 | } 7 | 8 | /** 9 | * Construct a filter to match a package name 10 | * 11 | * - @foo/bar@1.0.0 or foobar@1.0.0 -> Exact match 12 | * - @foo/bar@* or @foo/bar -> Any version of the package 13 | * - @foo/bar@^1.0.0 -> Any version that matches the semver range 14 | * - bar-* -> Any version that matches the prefix 15 | * - *eslint* -> Any version that matches the wildcard 16 | */ 17 | export function constructPackageFilter(range: string): (pkg: PackageNodeLike) => boolean { 18 | const [name, version = '*'] = range.split(/\b@/) 19 | const hasWildcard = name?.includes('*') 20 | const nameMatch = hasWildcard 21 | ? new RegExp(`^${Array.from(name).map(char => char === '*' ? '.*' : char === '.' ? '\\.' : char).join('')}$`) 22 | : name 23 | 24 | return (pkg) => { 25 | const isNameMatch = nameMatch instanceof RegExp ? nameMatch.test(pkg.name) : pkg.name === name 26 | const isVersionMatch = version === '*' || pkg.version === version || satisfies(pkg.version, version) 27 | return isNameMatch && isVersionMatch 28 | } 29 | } 30 | 31 | export function constructPackageFilters( 32 | ranges: (string | ((pkg: Node) => boolean)) [], 33 | mode: 'some' | 'every', 34 | ): (pkg: Node) => boolean { 35 | const filters = ranges.map(x => typeof x === 'string' ? constructPackageFilter(x) : x) 36 | return pkg => mode === 'some' 37 | ? filters.some(filter => filter(pkg)) 38 | : filters.every(filter => filter(pkg)) 39 | } 40 | -------------------------------------------------------------------------------- /packages/node-modules-tools/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './filter' 2 | export * from './package-json' 3 | -------------------------------------------------------------------------------- /packages/node-modules-tools/src/utils/package-json.test.ts: -------------------------------------------------------------------------------- 1 | import type { PackageJson } from 'pkg-types' 2 | import { describe, expect, it } from 'vitest' 3 | import { normalizePkgLicense } from './package-json' 4 | 5 | describe('normalize', () => { 6 | describe('normalizePkgLicense', () => { 7 | it('should parse legacy object format', () => { 8 | expect(normalizePkgLicense({ license: { type: 'MIT', url: 'dontcare' } } as unknown as PackageJson)).toMatchInlineSnapshot(`"MIT"`) 9 | }) 10 | it('should parse legacy array format', () => { 11 | expect(normalizePkgLicense({ licenses: [] } as unknown as PackageJson)).toMatchInlineSnapshot(`undefined`) 12 | expect(normalizePkgLicense({ licenses: [{ type: 'MIT', url: 'dontcare' }] } as unknown as PackageJson)).toMatchInlineSnapshot(`"MIT"`) 13 | expect(normalizePkgLicense({ licenses: [{ type: 'MIT', url: 'dontcare' }, { type: 'ISC', url: 'dontcare' }] } as unknown as PackageJson)).toMatchInlineSnapshot(`"(MIT OR ISC)"`) 14 | }) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /packages/node-modules-tools/test/module-type.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises' 2 | import { resolvePath } from 'mlly' 3 | import { resolvePackageJSON } from 'pkg-types' 4 | import { expect, it } from 'vitest' 5 | import { analyzePackageModuleType } from '../src/analyze-esm' 6 | 7 | async function getPackageJsonPath(pkg: string) { 8 | return JSON.parse(await fs.readFile( 9 | await resolvePath(`${pkg}/package.json`) 10 | .catch(async () => { 11 | return await resolvePackageJSON(await resolvePath(pkg)) 12 | }), 13 | 'utf-8', 14 | )) 15 | } 16 | 17 | it('types only', async () => { 18 | expect(analyzePackageModuleType(await getPackageJsonPath('type-fest'))) 19 | .toEqual('dts') 20 | 21 | expect(analyzePackageModuleType(await getPackageJsonPath('@types/node'))) 22 | .toEqual('dts') 23 | }) 24 | 25 | it('dual', async () => { 26 | expect(analyzePackageModuleType(await getPackageJsonPath('h3'))) 27 | .toEqual('dual') 28 | }) 29 | 30 | it('cjs', async () => { 31 | expect(analyzePackageModuleType(await getPackageJsonPath('debug'))) 32 | .toEqual('cjs') 33 | }) 34 | 35 | it('esm', async () => { 36 | expect(analyzePackageModuleType(await getPackageJsonPath('p-limit'))) 37 | .toEqual('esm') 38 | }) 39 | -------------------------------------------------------------------------------- /packages/node-modules-tools/test/npm/fixtures/multiple-package-jsons/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parent-package-json", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "parent-package-json", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "workspaces": [ 12 | "./sub/" 13 | ] 14 | }, 15 | "node_modules/child-package-json": { 16 | "resolved": "sub", 17 | "link": true 18 | }, 19 | "sub": { 20 | "version": "1.0.0", 21 | "license": "ISC" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/node-modules-tools/test/npm/fixtures/multiple-package-jsons/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parent-package-json", 3 | "version": "1.0.0", 4 | "packageManager": "npm@0.0.0", 5 | "description": "", 6 | "author": "", 7 | "license": "ISC", 8 | "keywords": [], 9 | "main": "index.js", 10 | "workspaces": ["./sub/"], 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/node-modules-tools/test/npm/fixtures/multiple-package-jsons/sub/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "child-package-json", 3 | "version": "1.0.0", 4 | "description": "", 5 | "author": "", 6 | "license": "ISC", 7 | "keywords": [], 8 | "main": "index.js", 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/node-modules-tools/test/npm/list.test.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { describe, expect, it } from 'vitest' 3 | import { listPackageDependencies } from '../../src' 4 | 5 | describe('listNpmPackageDependencies', () => { 6 | it('runs with multiple package.json files', async () => { 7 | const list = await listPackageDependencies({ 8 | cwd: fileURLToPath(new URL('./fixtures/multiple-package-jsons', import.meta.url)), 9 | depth: 25, 10 | monorepo: true, 11 | workspace: false, 12 | }) 13 | 14 | expect(list.packageManager).toBe('npm') 15 | expect(list.packages.size).toBe(2) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/node-modules-tools/test/pnpm/fixtures/multiple-package-jsons/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parent-package-json", 3 | "private": true, 4 | "dependencies": { 5 | "child-package-json": "workspace:*", 6 | "vitest": "catalog:testing" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/node-modules-tools/test/pnpm/fixtures/multiple-package-jsons/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: {} 10 | -------------------------------------------------------------------------------- /packages/node-modules-tools/test/pnpm/fixtures/multiple-package-jsons/sub/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "child-package-json", 3 | "private": true 4 | } 5 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | 4 | catalogs: 5 | bundling: 6 | '@rollup/plugin-alias': ^5.1.1 7 | '@rollup/plugin-commonjs': ^28.0.3 8 | '@rollup/plugin-node-resolve': ^16.0.1 9 | '@unocss/nuxt': ^66.1.0 10 | '@vueuse/nuxt': ^13.1.0 11 | esbuild: ^0.25.4 12 | nitropack: ^2.11.11 13 | nuxt: ^3.17.2 14 | nuxt-mcp: ^0.1.2 15 | rollup: ^4.40.2 16 | rollup-plugin-esbuild: ^6.2.1 17 | unbuild: ^3.5.0 18 | vite: npm:rolldown-vite@latest 19 | deps: 20 | ansis: ^3.17.0 21 | birpc: ^2.3.0 22 | cac: ^6.7.14 23 | fast-npm-meta: ^0.4.2 24 | get-port-please: ^3.1.2 25 | h3: ^1.15.3 26 | js-yaml: ^4.1.0 27 | launch-editor: ^2.10.0 28 | mrmime: ^2.0.1 29 | ohash: ^2.0.11 30 | open: ^10.1.2 31 | p-limit: ^6.2.0 32 | package-manager-detector: ^1.3.0 33 | pathe: ^2.0.3 34 | publint: ^0.3.12 35 | semver: ^7.7.1 36 | stream-json: ^1.9.1 37 | structured-clone-es: ^1.0.0 38 | tinyexec: ^1.0.1 39 | tinyglobby: ^0.2.13 40 | unconfig: ^7.3.2 41 | unstorage: ^1.16.0 42 | ws: ^8.18.2 43 | dev: 44 | '@nuxt/devtools': ^2.4.0 45 | bumpp: ^10.1.0 46 | typescript: ^5.8.3 47 | vite-plugin-inspect: ^11.0.1 48 | vue-tsc: ^2.2.10 49 | frontend: 50 | '@webcontainer/api': ^1.6.1 51 | '@xterm/addon-fit': ^0.10.0 52 | '@xterm/xterm': ^5.5.0 53 | d3: ^7.9.0 54 | d3-hierarchy: ^3.1.2 55 | d3-shape: ^3.2.0 56 | floating-vue: ^5.2.2 57 | fuse.js: ^7.1.0 58 | idb-keyval: ^6.2.1 59 | modern-screenshot: ^4.6.0 60 | nanovis: ^0.1.0 61 | theme-vitesse: ^0.8.3 62 | vite-hot-client: ^2.0.4 63 | icons: 64 | '@iconify-json/carbon': ^1.2.8 65 | '@iconify-json/catppuccin': ^1.2.11 66 | '@iconify-json/logos': ^1.2.4 67 | '@iconify-json/ph': ^1.2.2 68 | '@iconify-json/ri': ^1.2.5 69 | '@iconify-json/simple-icons': ^1.2.33 70 | inlined: 71 | '@antfu/utils': ^9.2.0 72 | lint: 73 | '@antfu/eslint-config': ^4.12.1 74 | '@nuxt/eslint': ^1.3.0 75 | '@typescript-eslint/utils': ^8.32.0 76 | '@unocss/eslint-config': ^66.1.0 77 | eslint: ^9.26.0 78 | lint-staged: ^15.5.1 79 | nuxt-eslint-auto-explicit-import: ^0.1.1 80 | simple-git-hooks: ^2.13.0 81 | testing: 82 | mlly: ^1.7.4 83 | vitest: ^3.1.3 84 | types: 85 | '@pnpm/list': ^1000.0.16 86 | '@pnpm/types': ^1000.5.0 87 | '@types/connect': ^3.4.38 88 | '@types/d3': ^7.4.3 89 | '@types/d3-hierarchy': ^3.1.7 90 | '@types/js-yaml': ^4.0.9 91 | '@types/semver': ^7.7.0 92 | '@types/stream-json': ^1.7.8 93 | '@types/ws': ^8.18.1 94 | pkg-types: ^2.1.0 95 | 96 | onlyBuiltDependencies: 97 | - '@parcel/watcher' 98 | - esbuild 99 | - rolldown 100 | - simple-git-hooks 101 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./packages/node-modules-inspector/src/.nuxt/tsconfig.json", 3 | "compilerOptions": { 4 | "moduleResolution": "Bundler", 5 | "resolveJsonModule": true, 6 | "noEmit": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | workspace: ['packages/*'], 6 | }, 7 | }) 8 | --------------------------------------------------------------------------------