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}