diff --git a/packages/docs/src/components/DependencyCharts.astro b/packages/docs/src/components/DependencyCharts.astro index e0e63b1..c0d6ffe 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 DuplicateDependencyChart from './DuplicateDependencyChart.astro' import ProdDepsChart from './ProdDepsChart.astro' --- @@ -9,7 +10,9 @@ import ProdDepsChart from './ProdDepsChart.astro' label="Dependency graphs" tab1Label="Deps" tab2Label="Prod Deps" + tab3Label="Dup. Deps" > + diff --git a/packages/docs/src/components/DependencyStatsTable.astro b/packages/docs/src/components/DependencyStatsTable.astro index d021036..cbf4517 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: 'Dup. Deps' }, { key: 'nodeModulesSize', header: 'Size' }, { key: 'nodeModulesSizeProdOnly', header: 'Size (Prod Only)' }, { diff --git a/packages/docs/src/components/DuplicateDependencyChart.astro b/packages/docs/src/components/DuplicateDependencyChart.astro new file mode 100644 index 0000000..016eee7 --- /dev/null +++ b/packages/docs/src/components/DuplicateDependencyChart.astro @@ -0,0 +1,10 @@ +--- +import { chartDuplicateDependencyData } 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 e1dc852..51d8aec 100644 --- a/packages/docs/src/content.config.ts +++ b/packages/docs/src/content.config.ts @@ -24,6 +24,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..e869fce 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 chartDuplicateDependencyData = 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..52ba9c4 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' @@ -10,7 +13,27 @@ import Layout from '../../layouts/Layout.astro' import { buildInstallData, depsStats } from '../../lib/collections' import { getFrameworkSlug } from '../../lib/utils' +interface E18eMessage { + severity: string + message: string + fixableBy?: string +} + export async function getStaticPaths() { + 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 duplicateDependencyMessages = 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: 'Dup. 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,37 @@ const ssrData = [ {buildEntry.buildOutput}

+ { + duplicateDependencyMessages.length > 0 && ( +
+

Duplicate Dependencies

+

+ {duplicateDependencyMessages.length} duplicate{' '} + {duplicateDependencyMessages.length === 1 + ? 'dependency' + : 'dependencies'}{' '} + detected across this starter's node_modules. +

+
+ + View {duplicateDependencyMessages.length} duplicate{' '} + {duplicateDependencyMessages.length === 1 + ? 'dependency' + : 'dependencies'} + +
    + {duplicateDependencyMessages.map((msg) => ( +
  • + {parseDupeName(msg.message)} +
    {msg.message}
    +
  • + ))} +
+
+
+ ) + } + { runtime && ( <> @@ -200,4 +271,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; + }