From be846300af3bfa83bc110dda185dcefdd831d1d4 Mon Sep 17 00:00:00 2001
From: Alexander Karan
Date: Mon, 9 Mar 2026 19:29:21 +0800
Subject: [PATCH 1/4] Init CLI UI
---
.../src/components/DependencyCharts.astro | 3 +
.../src/components/DependencyStatsTable.astro | 1 +
.../docs/src/components/DupeDepsChart.astro | 10 ++
packages/docs/src/content/config.ts | 1 +
packages/docs/src/lib/collections.ts | 9 ++
.../docs/src/pages/framework/[slug].astro | 131 +++++++++++++++++-
6 files changed, 154 insertions(+), 1 deletion(-)
create mode 100644 packages/docs/src/components/DupeDepsChart.astro
diff --git a/packages/docs/src/components/DependencyCharts.astro b/packages/docs/src/components/DependencyCharts.astro
index e0e63b1..2140e30 100644
--- a/packages/docs/src/components/DependencyCharts.astro
+++ b/packages/docs/src/components/DependencyCharts.astro
@@ -1,6 +1,7 @@
---
import ChartTabs from './ChartTabs.astro'
import DepsChart from './DepsChart.astro'
+import DupeDepsChart from './DupeDepsChart.astro'
import ProdDepsChart from './ProdDepsChart.astro'
---
@@ -9,7 +10,9 @@ import ProdDepsChart from './ProdDepsChart.astro'
label="Dependency graphs"
tab1Label="Deps"
tab2Label="Prod Deps"
+ tab3Label="Dupe Deps"
>
+
diff --git a/packages/docs/src/components/DependencyStatsTable.astro b/packages/docs/src/components/DependencyStatsTable.astro
index d021036..58dd21b 100644
--- a/packages/docs/src/components/DependencyStatsTable.astro
+++ b/packages/docs/src/components/DependencyStatsTable.astro
@@ -13,6 +13,7 @@ const depsColumns = [
},
{ key: 'prodDependencies', header: 'Prod Deps' },
{ key: 'devDependencies', header: 'Dev Deps' },
+ { key: 'duplicateDependencies', header: 'Dupe Deps' },
{ key: 'nodeModulesSize', header: 'Size' },
{ key: 'nodeModulesSizeProdOnly', header: 'Size (Prod Only)' },
{
diff --git a/packages/docs/src/components/DupeDepsChart.astro b/packages/docs/src/components/DupeDepsChart.astro
new file mode 100644
index 0000000..dac14c1
--- /dev/null
+++ b/packages/docs/src/components/DupeDepsChart.astro
@@ -0,0 +1,10 @@
+---
+import { chartDupeDepsData } from '../lib/collections'
+import ComparisonBarChart from './ComparisonBarChart.astro'
+---
+
+
diff --git a/packages/docs/src/content/config.ts b/packages/docs/src/content/config.ts
index 1ed41eb..84c259b 100644
--- a/packages/docs/src/content/config.ts
+++ b/packages/docs/src/content/config.ts
@@ -22,6 +22,7 @@ const devtimeCollection = defineCollection({
buildOutputSize: z.number(),
nodeModulesSize: z.number(),
nodeModulesSizeProdOnly: z.number(),
+ duplicateDependencies: z.number().optional(),
timingMeasuredAt: z.string(),
runner: z.string(),
frameworkVersion: z.string().optional(),
diff --git a/packages/docs/src/lib/collections.ts b/packages/docs/src/lib/collections.ts
index f604032..5c6eeb3 100644
--- a/packages/docs/src/lib/collections.ts
+++ b/packages/docs/src/lib/collections.ts
@@ -18,6 +18,7 @@ const depsStats = starterStats.map((f) => ({
isFocused: f.isFocused,
prodDependencies: f.prodDependencies,
devDependencies: f.devDependencies,
+ duplicateDependencies: f.duplicateDependencies,
nodeModulesSize: formatBytesToMB(f.nodeModulesSize),
nodeModulesSizeProdOnly: formatBytesToMB(f.nodeModulesSizeProdOnly),
graph: 'View',
@@ -39,4 +40,12 @@ const buildInstallData = starterStats.map((f) => ({
buildOutput: formatBytesToMB(f.buildOutputSize),
}))
+export const chartDupeDepsData = starterStats
+ .filter((f) => f?.name != null && Number.isFinite(f.duplicateDependencies))
+ .map((f) => ({
+ name: f.name,
+ value: f.duplicateDependencies!,
+ focused: f.isFocused,
+ }))
+
export { ssrStats, depsStats, buildInstallData }
diff --git a/packages/docs/src/pages/framework/[slug].astro b/packages/docs/src/pages/framework/[slug].astro
index 7535f66..aa24df7 100644
--- a/packages/docs/src/pages/framework/[slug].astro
+++ b/packages/docs/src/pages/framework/[slug].astro
@@ -1,4 +1,7 @@
---
+import { readFileSync } from 'node:fs'
+import { join } from 'node:path'
+import { fileURLToPath } from 'node:url'
import { getCollection } from 'astro:content'
import BackLink from '../../components/BackLink.astro'
import DevTimeChart from '../../components/DevTimeChart.astro'
@@ -11,6 +14,26 @@ import { buildInstallData, depsStats } from '../../lib/collections'
import { getFrameworkSlug } from '../../lib/utils'
export async function getStaticPaths() {
+ interface E18eMessage {
+ severity: string
+ message: string
+ fixableBy?: string
+ }
+
+ function readE18eMessages(packageName: string): E18eMessage[] {
+ try {
+ const packagesDir = fileURLToPath(
+ new URL('../../../../', import.meta.url),
+ )
+ const filePath = join(packagesDir, packageName, 'e18e-stats.json')
+ const content = readFileSync(filePath, 'utf-8')
+ const data = JSON.parse(content) as { messages?: E18eMessage[] }
+ return data.messages ?? []
+ } catch {
+ return []
+ }
+ }
+
const devtimeEntries = await getCollection('devtime')
const runtimeEntries = await getCollection('runtime')
const baseline = runtimeEntries.find(
@@ -22,18 +45,29 @@ export async function getStaticPaths() {
const runtimeEntry = runtimeEntries.find(
(e) => e.data.package === runtimePackage,
)
+ const e18eMessages = readE18eMessages(entry.data.package)
return {
params: { slug },
props: {
devtime: entry.data,
runtime: runtimeEntry?.data,
baseline,
+ e18eMessages,
},
}
})
}
-const { devtime, runtime, baseline } = Astro.props
+const { devtime, runtime, baseline, e18eMessages } = Astro.props
+
+const dupeDepsMessages = e18eMessages.filter((m) =>
+ m.message.startsWith('[duplicate dependency]'),
+)
+
+function parseDupeName(message: string): string {
+ const match = message.match(/\[duplicate dependency\] (\S+) has/)
+ return match?.[1] ?? 'unknown'
+}
const measuredDateDisplay = (() => {
const d = new Date(devtime.timingMeasuredAt)
@@ -48,6 +82,9 @@ const buildEntry = buildInstallData.find((e) => e.package === devtime.package)!
const depsColumns = [
{ key: 'prodDependencies', header: 'Prod Deps' },
{ key: 'devDependencies', header: 'Dev Deps' },
+ ...(devtime.duplicateDependencies != null
+ ? [{ key: 'duplicateDependencies', header: 'Dupe Deps' }]
+ : []),
{ key: 'nodeModulesSize', header: 'Size' },
{ key: 'nodeModulesSizeProdOnly', header: 'Size (Prod Only)' },
{
@@ -61,6 +98,9 @@ const depsData = [
{
prodDependencies: String(depsEntry.prodDependencies),
devDependencies: String(depsEntry.devDependencies),
+ ...(devtime.duplicateDependencies != null
+ ? { duplicateDependencies: String(devtime.duplicateDependencies) }
+ : {}),
nodeModulesSize: depsEntry.nodeModulesSize,
nodeModulesSizeProdOnly: depsEntry.nodeModulesSizeProdOnly,
graph: depsEntry.graph,
@@ -170,6 +210,33 @@ const ssrData = [
{buildEntry.buildOutput}
+ {
+ dupeDepsMessages.length > 0 && (
+
+ Duplicate Dependencies
+
+ {dupeDepsMessages.length} duplicate{' '}
+ {dupeDepsMessages.length === 1 ? 'dependency' : 'dependencies'}{' '}
+ detected across this starter's node_modules.
+
+
+
+ View {dupeDepsMessages.length} duplicate{' '}
+ {dupeDepsMessages.length === 1 ? 'dependency' : 'dependencies'}
+
+
+ {dupeDepsMessages.map((msg) => (
+ -
+ {parseDupeName(msg.message)}
+
{msg.message}
+
+ ))}
+
+
+
+ )
+ }
+
{
runtime && (
<>
@@ -200,4 +267,66 @@ const ssrData = [
.build-output strong {
color: var(--ft-text);
}
+
+ .dupe-deps-intro {
+ color: var(--ft-muted);
+ font-size: 14px;
+ margin: 0 0 0.75em;
+ }
+
+ .dupe-deps-details {
+ border: 1px solid var(--ft-border);
+ border-radius: 8px;
+ overflow: hidden;
+ }
+
+ .dupe-deps-details summary {
+ padding: 10px 14px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ user-select: none;
+ color: var(--ft-text);
+ background: var(--ft-bg-muted);
+ }
+
+ .dupe-deps-details summary:hover {
+ background: var(--ft-border);
+ }
+
+ .dupe-deps-details[open] summary {
+ border-bottom: 1px solid var(--ft-border);
+ }
+
+ .dupe-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ }
+
+ .dupe-item {
+ padding: 12px 14px;
+ border-bottom: 1px solid var(--ft-border);
+ }
+
+ .dupe-item:last-child {
+ border-bottom: none;
+ }
+
+ .dupe-name {
+ display: block;
+ font-size: 14px;
+ color: var(--ft-text);
+ margin-bottom: 6px;
+ }
+
+ .dupe-message {
+ margin: 0;
+ font-size: 12px;
+ line-height: 1.5;
+ color: var(--ft-muted);
+ white-space: pre-wrap;
+ word-break: break-word;
+ font-family: ui-monospace, 'SFMono-Regular', Menlo, monospace;
+ }
From 054f9c49f834dae7f5ba6dee315e837ee90bf4cb Mon Sep 17 00:00:00 2001
From: Alexander Karan
Date: Sun, 15 Mar 2026 19:17:15 +0800
Subject: [PATCH 2/4] Fixed type
---
packages/docs/src/pages/framework/[slug].astro | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/packages/docs/src/pages/framework/[slug].astro b/packages/docs/src/pages/framework/[slug].astro
index aa24df7..d2de05c 100644
--- a/packages/docs/src/pages/framework/[slug].astro
+++ b/packages/docs/src/pages/framework/[slug].astro
@@ -13,13 +13,13 @@ import Layout from '../../layouts/Layout.astro'
import { buildInstallData, depsStats } from '../../lib/collections'
import { getFrameworkSlug } from '../../lib/utils'
-export async function getStaticPaths() {
- interface E18eMessage {
- severity: string
- message: string
- fixableBy?: string
- }
+interface E18eMessage {
+ severity: string
+ message: string
+ fixableBy?: string
+}
+export async function getStaticPaths() {
function readE18eMessages(packageName: string): E18eMessage[] {
try {
const packagesDir = fileURLToPath(
From 1f511ed555f00feff0db0608f679c199b8a9e94e Mon Sep 17 00:00:00 2001
From: Alexander Karan
Date: Mon, 16 Mar 2026 19:20:34 +0800
Subject: [PATCH 3/4] Updated name
---
packages/docs/src/components/DependencyCharts.astro | 2 +-
packages/docs/src/components/DependencyStatsTable.astro | 2 +-
packages/docs/src/pages/framework/[slug].astro | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/packages/docs/src/components/DependencyCharts.astro b/packages/docs/src/components/DependencyCharts.astro
index 2140e30..5ba4749 100644
--- a/packages/docs/src/components/DependencyCharts.astro
+++ b/packages/docs/src/components/DependencyCharts.astro
@@ -10,7 +10,7 @@ import ProdDepsChart from './ProdDepsChart.astro'
label="Dependency graphs"
tab1Label="Deps"
tab2Label="Prod Deps"
- tab3Label="Dupe Deps"
+ tab3Label="Dup. Deps"
>
diff --git a/packages/docs/src/components/DependencyStatsTable.astro b/packages/docs/src/components/DependencyStatsTable.astro
index 58dd21b..cbf4517 100644
--- a/packages/docs/src/components/DependencyStatsTable.astro
+++ b/packages/docs/src/components/DependencyStatsTable.astro
@@ -13,7 +13,7 @@ const depsColumns = [
},
{ key: 'prodDependencies', header: 'Prod Deps' },
{ key: 'devDependencies', header: 'Dev Deps' },
- { key: 'duplicateDependencies', header: 'Dupe Deps' },
+ { key: 'duplicateDependencies', header: 'Dup. Deps' },
{ key: 'nodeModulesSize', header: 'Size' },
{ key: 'nodeModulesSizeProdOnly', header: 'Size (Prod Only)' },
{
diff --git a/packages/docs/src/pages/framework/[slug].astro b/packages/docs/src/pages/framework/[slug].astro
index d2de05c..e450665 100644
--- a/packages/docs/src/pages/framework/[slug].astro
+++ b/packages/docs/src/pages/framework/[slug].astro
@@ -83,7 +83,7 @@ const depsColumns = [
{ key: 'prodDependencies', header: 'Prod Deps' },
{ key: 'devDependencies', header: 'Dev Deps' },
...(devtime.duplicateDependencies != null
- ? [{ key: 'duplicateDependencies', header: 'Dupe Deps' }]
+ ? [{ key: 'duplicateDependencies', header: 'Dup. Deps' }]
: []),
{ key: 'nodeModulesSize', header: 'Size' },
{ key: 'nodeModulesSizeProdOnly', header: 'Size (Prod Only)' },
From 82060fe6f9833c19771da661c9acbc527d9bad5d Mon Sep 17 00:00:00 2001
From: Alexander Karan
Date: Mon, 16 Mar 2026 19:30:13 +0800
Subject: [PATCH 4/4] Updated names
---
.../docs/src/components/DependencyCharts.astro | 4 ++--
...rt.astro => DuplicateDependencyChart.astro} | 4 ++--
packages/docs/src/lib/collections.ts | 2 +-
packages/docs/src/pages/framework/[slug].astro | 18 +++++++++++-------
4 files changed, 16 insertions(+), 12 deletions(-)
rename packages/docs/src/components/{DupeDepsChart.astro => DuplicateDependencyChart.astro} (58%)
diff --git a/packages/docs/src/components/DependencyCharts.astro b/packages/docs/src/components/DependencyCharts.astro
index 5ba4749..c0d6ffe 100644
--- a/packages/docs/src/components/DependencyCharts.astro
+++ b/packages/docs/src/components/DependencyCharts.astro
@@ -1,7 +1,7 @@
---
import ChartTabs from './ChartTabs.astro'
import DepsChart from './DepsChart.astro'
-import DupeDepsChart from './DupeDepsChart.astro'
+import DuplicateDependencyChart from './DuplicateDependencyChart.astro'
import ProdDepsChart from './ProdDepsChart.astro'
---
@@ -14,5 +14,5 @@ import ProdDepsChart from './ProdDepsChart.astro'
>
-
+
diff --git a/packages/docs/src/components/DupeDepsChart.astro b/packages/docs/src/components/DuplicateDependencyChart.astro
similarity index 58%
rename from packages/docs/src/components/DupeDepsChart.astro
rename to packages/docs/src/components/DuplicateDependencyChart.astro
index dac14c1..016eee7 100644
--- a/packages/docs/src/components/DupeDepsChart.astro
+++ b/packages/docs/src/components/DuplicateDependencyChart.astro
@@ -1,10 +1,10 @@
---
-import { chartDupeDepsData } from '../lib/collections'
+import { chartDuplicateDependencyData } from '../lib/collections'
import ComparisonBarChart from './ComparisonBarChart.astro'
---
diff --git a/packages/docs/src/lib/collections.ts b/packages/docs/src/lib/collections.ts
index 5c6eeb3..e869fce 100644
--- a/packages/docs/src/lib/collections.ts
+++ b/packages/docs/src/lib/collections.ts
@@ -40,7 +40,7 @@ const buildInstallData = starterStats.map((f) => ({
buildOutput: formatBytesToMB(f.buildOutputSize),
}))
-export const chartDupeDepsData = starterStats
+export const chartDuplicateDependencyData = starterStats
.filter((f) => f?.name != null && Number.isFinite(f.duplicateDependencies))
.map((f) => ({
name: f.name,
diff --git a/packages/docs/src/pages/framework/[slug].astro b/packages/docs/src/pages/framework/[slug].astro
index e450665..52ba9c4 100644
--- a/packages/docs/src/pages/framework/[slug].astro
+++ b/packages/docs/src/pages/framework/[slug].astro
@@ -60,7 +60,7 @@ export async function getStaticPaths() {
const { devtime, runtime, baseline, e18eMessages } = Astro.props
-const dupeDepsMessages = e18eMessages.filter((m) =>
+const duplicateDependencyMessages = e18eMessages.filter((m) =>
m.message.startsWith('[duplicate dependency]'),
)
@@ -211,21 +211,25 @@ const ssrData = [
{
- dupeDepsMessages.length > 0 && (
+ duplicateDependencyMessages.length > 0 && (
Duplicate Dependencies
- {dupeDepsMessages.length} duplicate{' '}
- {dupeDepsMessages.length === 1 ? 'dependency' : 'dependencies'}{' '}
+ {duplicateDependencyMessages.length} duplicate{' '}
+ {duplicateDependencyMessages.length === 1
+ ? 'dependency'
+ : 'dependencies'}{' '}
detected across this starter's node_modules.
- View {dupeDepsMessages.length} duplicate{' '}
- {dupeDepsMessages.length === 1 ? 'dependency' : 'dependencies'}
+ View {duplicateDependencyMessages.length} duplicate{' '}
+ {duplicateDependencyMessages.length === 1
+ ? 'dependency'
+ : 'dependencies'}
- {dupeDepsMessages.map((msg) => (
+ {duplicateDependencyMessages.map((msg) => (
-
{parseDupeName(msg.message)}
{msg.message}