Skip to content

Commit 8f2fc0e

Browse files
authored
feat: package detail view (#116)
1 parent 56c957f commit 8f2fc0e

File tree

8 files changed

+219
-47
lines changed

8 files changed

+219
-47
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<script setup lang="ts">
2+
import type { SessionContext } from '~~/shared/types/data'
3+
import { useRpc } from '#imports'
4+
import { useAsyncState } from '@vueuse/core'
5+
import { computed } from 'vue'
6+
7+
const props = defineProps<{
8+
session: SessionContext
9+
package: string
10+
}>()
11+
const emit = defineEmits<{
12+
(e: 'close'): void
13+
}>()
14+
const parsedPackage = computed(() => {
15+
const match = props.package.match(/^(@?[^@]+)@(.+)$/)
16+
const [, name, version] = match!
17+
return { name, version }
18+
})
19+
const rpc = useRpc()
20+
const { state, isLoading } = useAsyncState(
21+
async () => {
22+
return await rpc.value!['vite:rolldown:get-package-details']?.({
23+
session: props.session.id,
24+
id: props.package,
25+
})
26+
},
27+
null,
28+
)
29+
30+
const importers = computed(() => {
31+
const pathMap = new Map()
32+
state.value?.files.filter(f => !!f.importers).flatMap(f => f.importers).filter(i => !i.path.startsWith(state.value?.dir ?? '')).forEach((importer) => {
33+
pathMap.set(importer.path, importer)
34+
})
35+
return Array.from(pathMap.values())
36+
})
37+
38+
function openInNpm() {
39+
const url = `https://www.npmjs.com/package/${parsedPackage.value.name}`
40+
window.open(url, '_blank')
41+
}
42+
</script>
43+
44+
<template>
45+
<VisualLoading v-if="isLoading" />
46+
47+
<div v-if="state" p4 relative h-full w-full of-auto z-panel-content>
48+
<div flex="~ col gap-3">
49+
<div flex="~ gap-3 items-center" :title="package">
50+
<div flex="~ items-center gap-1">
51+
<div>
52+
<DisplayHighlightedPackageName :name="parsedPackage.name!" />
53+
</div>
54+
<DisplayFileSizeBadge :bytes="state.transformedCodeSize" />
55+
</div>
56+
<div flex-auto />
57+
<button btn-action flex="~ items-center" @click="openInNpm">
58+
<div i-ph-arrow-square-out-duotone />
59+
Open in npm
60+
</button>
61+
<DisplayCloseButton
62+
@click="emit('close')"
63+
/>
64+
</div>
65+
66+
<details open="true">
67+
<summary op50>
68+
<span>Bundled Files ({{ state.files.length }})</span>
69+
</summary>
70+
<div flex="~ col gap-1" mt2 ws-nowrap>
71+
<div v-for="file of state.files.filter(f => !!f.transformedCodeSize)" :key="file.path" flex="~ row gap-1 items-center nowrap" hover="bg-active" border="~ base rounded" px2 py1 w-full>
72+
<DisplayModuleId :id="file.path" :session="session" ws-nowrap flex-1 disable-tooltip link :cwd="state.dir" />
73+
<span inline-flex>
74+
<DisplayFileSizeBadge :bytes="file.transformedCodeSize" text-xs />
75+
</span>
76+
</div>
77+
</div>
78+
</details>
79+
80+
<details open="true">
81+
<summary op50>
82+
<span>Importers ({{ importers.length }})</span>
83+
</summary>
84+
<div flex="~ col gap-1" mt2 ws-nowrap>
85+
<div v-for="importer of importers" :key="importer.path" flex="~ row gap-1 items-center nowrap" hover="bg-active" border="~ base rounded" px2 py1 w-full>
86+
<DisplayModuleId :id="importer.path" :session="session" ws-nowrap flex-1 disable-tooltip link />
87+
<DisplayBadge v-if="importer.version" :text="`v${importer.version}`" as="span" />
88+
</div>
89+
</div>
90+
</details>
91+
</div>
92+
</div>
93+
</template>

packages/vite/src/app/components/packages/Importers.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ const props = defineProps<{
88
showVersion: boolean
99
}>()
1010
11-
const importers = computed(() => [...new Set(props.package.files.filter(f => !!f.importers).flatMap(f => f.importers))])
11+
const importers = computed(() => {
12+
const pathMap = new Map()
13+
props.package.files.filter(f => !!f.importers).flatMap(f => f.importers).filter(i => !i.path.startsWith(props.package?.dir ?? '')).forEach((importer) => {
14+
pathMap.set(importer.path, importer)
15+
})
16+
return Array.from(pathMap.values())
17+
})
1218
</script>
1319

1420
<template>

packages/vite/src/app/components/packages/Table.vue

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import type { PackageInfo, SessionContext } from '~~/shared/types'
3+
import { useRoute } from '#app/composables/router'
34
import { useCycleList } from '@vueuse/core'
45
import { Menu as VMenu } from 'floating-vue'
56
import { settings } from '~~/app/state/settings'
@@ -14,6 +15,7 @@ withDefaults(defineProps<{
1415
groupView: false,
1516
})
1617
18+
const route = useRoute()
1719
const { state: sizeSortType, next } = useCycleList(['', 'desc', 'asc'], {
1820
initialValue: settings.value.packageSizeSortType,
1921
})
@@ -60,11 +62,12 @@ function toggleSizeSortType() {
6062
key-prop="dir"
6163
>
6264
<template #default="{ item, index }">
63-
<div
65+
<NuxtLink
6466
role="row"
6567
flex="~ row"
6668
class="border-base border-b-1 border-dashed"
6769
:class="[index === packages.length - 1 ? 'border-b-0' : '']"
70+
:to="{ path: route.path, query: { package: `${item.name}@${item.version}` } }"
6871
>
6972
<div v-if="!groupView" role="cell" font-mono flex-none min-w80 py1.5 px2 ws-nowrap text-sm>
7073
<DisplayHighlightedPackageName :name="item.name" />
@@ -81,7 +84,7 @@ function toggleSizeSortType() {
8184
<span w24 inline-flex>
8285
<DisplayFileSizeBadge :bytes="file.transformedCodeSize" />
8386
</span>
84-
<DisplayModuleId :id="file.path" :session="session" ws-nowrap flex-1 disable-tooltip link />
87+
<DisplayModuleId :id="file.path" :session="session" ws-nowrap flex-1 disable-tooltip link :cwd="item.dir" />
8588
</div>
8689
</div>
8790
</template>
@@ -90,7 +93,7 @@ function toggleSizeSortType() {
9093
<div role="cell" flex="~ items-center" flex-1 font-mono py1.5 pl20 pr2 text-sm op80>
9194
<PackagesImporters :package="item" :session="session" :show-version="groupView" />
9295
</div>
93-
</div>
96+
</NuxtLink>
9497
</template>
9598
</DataVirtualList>
9699
<div v-else>

packages/vite/src/app/pages/session/[session].vue

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const rpc = useRpc()
2323
const router = useRouter()
2424
const route = useRoute()
2525
26-
const currentPanelType = computed(() => ['module', 'asset', 'plugin', 'chunk'].find(key => typeof route.query[key] !== 'undefined'))
26+
const currentPanelType = computed(() => ['module', 'asset', 'plugin', 'chunk', 'package'].find(key => typeof route.query[key] !== 'undefined'))
2727
function closeCurrentPanel() {
2828
if (currentPanelType.value) {
2929
router.replace({ query: { ...route.query, [currentPanelType.value]: undefined } })
@@ -161,6 +161,19 @@ onMounted(async () => {
161161
@close="closeCurrentPanel"
162162
/>
163163
</div>
164+
<div
165+
v-if="currentPanelType === 'package'"
166+
:key="(route.query.package as string)"
167+
fixed right-0 bottom-0 top-20 z-panel-content
168+
bg-glass border="l t base rounded-tl-xl"
169+
class="left-20 xl:left-100 2xl:left-150"
170+
>
171+
<DataPackageDetailsLoader
172+
:package="(route.query.package as string)"
173+
:session="session"
174+
@close="closeCurrentPanel"
175+
/>
176+
</div>
164177
</div>
165178
</div>
166179
</template>

packages/vite/src/app/pages/session/[session]/packages.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import type { PackageInfo, SessionContext } from '~~/shared/types/data'
33
import type { ClientSettings } from '~/state/settings'
44
import type { PackageChartInfo, PackageChartNode } from '~/types/chart'
5-
import { useRoute } from '#app/composables/router'
5+
import { useRoute, useRouter } from '#app/composables/router'
66
import { useRpc } from '#imports'
77
import { computedWithControl, useAsyncState, useMouse } from '@vueuse/core'
88
import Fuse from 'fuse.js'
@@ -18,6 +18,7 @@ const props = defineProps<{
1818
1919
const mouse = reactive(useMouse())
2020
const route = useRoute()
21+
const router = useRouter()
2122
2223
const packageTypeRules = [
2324
{
@@ -119,6 +120,11 @@ const { tree, chartOptions, graph, nodeHover, nodeSelected, selectedNode, select
119120
if (node === null)
120121
nodeHover.value = undefined
121122
},
123+
onClick(node) {
124+
if (node.meta?.type === 'package') {
125+
router.replace({ query: { ...route.query, package: `${node.meta?.name}@${node.meta?.version}` } })
126+
}
127+
},
122128
onLeave() {
123129
nodeHover.value = undefined
124130
},
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { defineRpcFunction } from '@vitejs/devtools-kit'
2+
import { getLogsManager } from '../utils'
3+
import { getPackagesManifest } from './rolldown-get-packages'
4+
5+
export const rolldownGetPackageDetails = defineRpcFunction({
6+
name: 'vite:rolldown:get-package-details',
7+
type: 'query',
8+
setup: (context) => {
9+
const manager = getLogsManager(context)
10+
return {
11+
handler: async ({ session, id }: { session: string, id: string }) => {
12+
const reader = await manager.loadSession(session)
13+
const packagesManifest = await getPackagesManifest(reader)
14+
return packagesManifest.get(id)
15+
},
16+
}
17+
},
18+
})

packages/vite/src/node/rpc/functions/rolldown-get-packages.ts

Lines changed: 72 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,79 @@
11
import type { PackageInfo } from '../../../shared/types'
2+
import type { RolldownEventsReader } from '../../rolldown/events-reader'
23
import { readProjectManifestOnly } from '@pnpm/read-project-manifest'
34
import { defineRpcFunction } from '@vitejs/devtools-kit'
45
import { getPackageDirPath, isNodeModulePath } from '../../../shared/utils/filepath'
56
import { getLogsManager } from '../utils'
67

8+
export async function getPackagesManifest(reader: RolldownEventsReader) {
9+
const modulesMap = reader.manager.modules
10+
const chunks = Array.from(reader.manager.chunks.values())
11+
const packagesManifest = new Map<string, PackageInfo>()
12+
13+
const getImporters = (path: string, packageDir: string, visited = new Set<string>()): string[] => {
14+
const importers = modulesMap.get(path)?.importers || []
15+
const result: string[] = []
16+
17+
for (const importer of importers) {
18+
if (visited.has(importer))
19+
continue
20+
21+
visited.add(importer)
22+
const module = modulesMap.get(importer)
23+
const imports = module?.imports?.map(i => i.module_id) || []
24+
25+
if (imports.some(i => getPackageDirPath(i) === packageDir)) {
26+
result.push(importer)
27+
}
28+
29+
result.push(...getImporters(importer, packageDir, visited))
30+
}
31+
32+
return result
33+
}
34+
35+
const packages = chunks.map(chunk => chunk.modules.map(module => module)).flat().filter(isNodeModulePath).map((p) => {
36+
const module = modulesMap.get(p)
37+
const moduleBuildMetrics = module?.build_metrics
38+
return {
39+
path: p,
40+
dir: getPackageDirPath(p),
41+
transformedCodeSize: moduleBuildMetrics?.transforms[moduleBuildMetrics?.transforms.length - 1]?.transformed_code_size ?? 0,
42+
}
43+
})
44+
await Promise.all(packages.map(async (p) => {
45+
const manifest = await readProjectManifestOnly(p.dir)
46+
const packageKey = `${manifest.name!}@${manifest.version!}`
47+
const packageInfo = packagesManifest.get(packageKey)
48+
const importers = getImporters(p.path, p.dir).map(i => ({ path: i, version: '' }))
49+
if (packageInfo) {
50+
packagesManifest.set(packageKey, {
51+
...packageInfo,
52+
files: [...packageInfo.files, {
53+
path: p.path,
54+
transformedCodeSize: p.transformedCodeSize,
55+
importers,
56+
}],
57+
transformedCodeSize: packageInfo.transformedCodeSize + p.transformedCodeSize,
58+
})
59+
}
60+
else {
61+
packagesManifest.set(packageKey, {
62+
name: manifest.name!,
63+
version: manifest.version!,
64+
dir: p.dir,
65+
files: [{
66+
path: p.path,
67+
transformedCodeSize: p.transformedCodeSize,
68+
importers,
69+
}],
70+
transformedCodeSize: p.transformedCodeSize,
71+
})
72+
}
73+
}))
74+
return packagesManifest
75+
}
76+
777
export const rolldownGetPackages = defineRpcFunction({
878
name: 'vite:rolldown:get-packages',
979
type: 'query',
@@ -12,49 +82,10 @@ export const rolldownGetPackages = defineRpcFunction({
1282
return {
1383
handler: async ({ session }: { session: string }) => {
1484
const reader = await manager.loadSession(session)
15-
const chunks = Array.from(reader.manager.chunks.values())
1685
const modulesMap = reader.manager.modules
1786
const duplicatePackagesMap = new Map<string, number>()
18-
const packagesManifest = new Map<string, PackageInfo>()
19-
const packages = chunks.map(chunk => chunk.modules.map(module => module)).flat().filter(isNodeModulePath).map((p) => {
20-
const module = modulesMap.get(p)
21-
const moduleBuildMetrics = module?.build_metrics
22-
return {
23-
path: p,
24-
dir: getPackageDirPath(p),
25-
transformedCodeSize: moduleBuildMetrics?.transforms[moduleBuildMetrics?.transforms.length - 1]?.transformed_code_size ?? 0,
26-
}
27-
})
28-
await Promise.all(packages.map(async (p) => {
29-
const manifest = await readProjectManifestOnly(p.dir)
30-
const packageKey = `${manifest.name!}@${manifest.version!}`
31-
const packageInfo = packagesManifest.get(packageKey)
32-
const module = modulesMap.get(p.path)
33-
if (packageInfo) {
34-
packagesManifest.set(packageKey, {
35-
...packageInfo,
36-
files: [...packageInfo.files, {
37-
path: p.path,
38-
transformedCodeSize: p.transformedCodeSize,
39-
importers: module?.importers?.map(i => ({ path: i, version: '' })) ?? [],
40-
}],
41-
transformedCodeSize: packageInfo.transformedCodeSize + p.transformedCodeSize,
42-
})
43-
}
44-
else {
45-
packagesManifest.set(packageKey, {
46-
name: manifest.name!,
47-
version: manifest.version!,
48-
dir: p.dir,
49-
files: [{
50-
path: p.path,
51-
transformedCodeSize: p.transformedCodeSize,
52-
importers: module?.importers?.map(i => ({ path: i, version: '' })) ?? [],
53-
}],
54-
transformedCodeSize: p.transformedCodeSize,
55-
})
56-
}
57-
}))
87+
const packagesManifest = await getPackagesManifest(reader)
88+
5889
const normalizedPackages = await Promise.all(
5990
Array.from<PackageInfo>(packagesManifest.values())
6091
.map((p) => {

packages/vite/src/node/rpc/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { rolldownGetChunksGraph } from './functions/rolldown-get-chunks-graph'
77
import { rolldownGetModuleInfo } from './functions/rolldown-get-module-info'
88
import { rolldownGetModuleRawEvents } from './functions/rolldown-get-module-raw-events'
99
import { rolldownGetModuleTransforms } from './functions/rolldown-get-module-transforms'
10+
import { rolldownGetPackageDetails } from './functions/rolldown-get-package-details'
1011
import { rolldownGetPackages } from './functions/rolldown-get-packages'
1112
import { rolldownGetPluginDetails } from './functions/rolldown-get-plugin-details'
1213
import { rolldownGetRawEvents } from './functions/rolldown-get-raw-events'
@@ -30,6 +31,7 @@ export const rpcFunctions = [
3031
rolldownGetSessionCompareSummary,
3132
rolldownGetChunkInfo,
3233
rolldownGetPackages,
34+
rolldownGetPackageDetails,
3335
] as const
3436

3537
export type ServerFunctions = RpcDefinitionsToFunctions<typeof rpcFunctions>

0 commit comments

Comments
 (0)