Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/bundle-analyzer/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--polyfill: #5f707f;
--polyfill-foreground: #ffffff;
}

.dark {
Expand Down Expand Up @@ -62,6 +64,8 @@
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
--polyfill: #5f707f;
--polyfill-foreground: #ffffff;
}

@theme inline {
Expand Down Expand Up @@ -98,6 +102,8 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--color-polyfill: var(--polyfill);
--color-polyfill-foreground: var(--polyfill-foreground);
}

@layer base {
Expand Down
44 changes: 39 additions & 5 deletions apps/bundle-analyzer/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { Input } from '@/components/ui/input'
import { Skeleton, TreemapSkeleton } from '@/components/ui/skeleton'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
import { AnalyzeData, ModulesData } from '@/lib/analyze-data'
import { SpecialModule } from '@/lib/types'
import { getSpecialModuleType } from '@/lib/utils'

function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B'
Expand Down Expand Up @@ -137,6 +139,11 @@ export default function Home() {
const isAnyLoading = isAnalyzeLoading || isModulesLoading
const rootSourceIndex = getRootSourceIndex(analyzeData)

const specialModuleType = getSpecialModuleType(
analyzeData,
selectedSourceIndex
)

return (
<main
className="h-screen flex flex-col bg-background"
Expand Down Expand Up @@ -289,12 +296,39 @@ export default function Home() {
{selectedSourceIndex != null &&
analyzeData.source(selectedSourceIndex) && (
<>
<p className="text-xs text-muted-foreground mt-2">
Output Size:{' '}
{formatBytes(
analyzeData.getSourceOutputSize(selectedSourceIndex)
<dl className="space-y-2">
<div>
<dt className="text-xs text-muted-foreground inline">
Output Size:{' '}
</dt>
<dd className="text-xs text-muted-foreground inline">
{formatBytes(
analyzeData.getSourceOutputSize(
selectedSourceIndex
)
)}
</dd>
</div>
{(specialModuleType === SpecialModule.POLYFILL_MODULE ||
specialModuleType ===
SpecialModule.POLYFILL_NOMODULE) && (
<div className="flex items-center gap-2">
<dt className="inline-flex items-center rounded-md bg-polyfill/10 dark:bg-polyfill/30 px-2 py-1 text-xs font-medium text-polyfill dark:text-polyfill-foreground ring-1 ring-inset ring-polyfill/20 shrink-0">
Polyfill
</dt>
<dd className="text-xs text-muted-foreground">
Next.js built-in polyfills
{specialModuleType ===
SpecialModule.POLYFILL_NOMODULE ? (
<>
. <code>polyfill-nomodule.js</code> is only
sent to legacy browsers.
</>
) : null}
</dd>
</div>
)}
</p>
</dl>
{modulesData && (
<ImportChain
key={selectedSourceIndex}
Expand Down
28 changes: 23 additions & 5 deletions apps/bundle-analyzer/components/treemap-visualizer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { darken, lighten } from 'polished'
import { darken, lighten, readableColor } from 'polished'
import type React from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import type { AnalyzeData } from '@/lib/analyze-data'
Expand All @@ -9,6 +9,7 @@ import {
type LayoutNode,
type LayoutNodeInfo,
} from '@/lib/treemap-layout'
import { SpecialModule } from '@/lib/types'

interface TreemapVisualizerProps {
analyzeData: AnalyzeData
Expand All @@ -23,6 +24,8 @@ interface TreemapVisualizerProps {
onHoveredNodeChangeDelayed?: (nodeInfo: LayoutNodeInfo | null) => void
searchQuery?: string
filterSource?: (sourceIndex: number) => boolean
isModulePolyfillChunk?: (sourceIndex: number) => boolean
isNoModulePolyfillChunk?: (sourceIndex: number) => boolean
}

function getFileColor(node: {
Expand All @@ -33,12 +36,17 @@ function getFileColor(node: {
server?: boolean
client?: boolean
traced?: boolean
specialModuleType: SpecialModule | null
}): string {
const { js, css, json, asset, client, traced } = node
const { js, css, json, asset, client, traced, specialModuleType } = node

if (isPolyfill(specialModuleType)) {
return '#5f707f'
}

let color = '#9ca3af' // gray-400 default
if (js) color = '#0068d6'
if (css) color = '#663399'
if (js) color = '#4682b4'
if (css) color = '#8b7d9e'
if (json) color = '#297a3a'
if (asset) color = '#da2f35'

Expand All @@ -54,6 +62,13 @@ function getFileColor(node: {
return color
}

function isPolyfill(specialModuleType: SpecialModule | null): boolean {
return (
specialModuleType === SpecialModule.POLYFILL_MODULE ||
specialModuleType === SpecialModule.POLYFILL_NOMODULE
)
}

function findNodeAtPosition(
node: LayoutNode,
x: number,
Expand Down Expand Up @@ -320,7 +335,8 @@ function drawTreemap(
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height)

if (rect.width > 60 && rect.height > 30) {
ctx.fillStyle = colors.text
const textColor = readableColor(color)
ctx.fillStyle = textColor
ctx.font = '12px sans-serif'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
Expand Down Expand Up @@ -516,6 +532,7 @@ function wrapLayoutWithAncestorsUsingIndices(
titleBarHeight: titleBarHeight,
children: [currentNode],
sourceIndex: ancestorIndex,
specialModuleType: null,
}

currentNode = ancestorNode
Expand All @@ -539,6 +556,7 @@ function wrapLayoutWithAncestorsUsingIndices(
titleBarHeight: minTitleBarHeight,
children: [currentNode],
sourceIndex: rootIndex,
specialModuleType: null,
}

return rootNode
Expand Down
14 changes: 14 additions & 0 deletions apps/bundle-analyzer/lib/analyze-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,20 @@ export class AnalyzeData {
return { client, server, traced, js, css, json, asset }
}

isPolyfillModule(index: number): boolean {
const fullSourcePath = this.getFullSourcePath(index)
return fullSourcePath.endsWith(
'node_modules/next/dist/build/polyfills/polyfill-module.js'
)
}

isPolyfillNoModule(index: number): boolean {
const fullSourcePath = this.getFullSourcePath(index)
return fullSourcePath.endsWith(
'node_modules/next/dist/build/polyfills/polyfill-nomodule.js'
)
}

// Get the raw header for debugging
getRawAnalyzeHeader(): AnalyzeDataHeader {
return this.analyzeHeader
Expand Down
7 changes: 7 additions & 0 deletions apps/bundle-analyzer/lib/treemap-layout.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { AnalyzeData } from './analyze-data'
import { layoutTreemap } from './layout-treemap'
import { SpecialModule } from './types'
import { getSpecialModuleType } from './utils'

export interface LayoutRect {
x: number
Expand All @@ -18,6 +20,7 @@ export interface LayoutNode extends LayoutNodeInfo {
size: number
rect: LayoutRect
type: 'file' | 'directory' | 'collapsed-directory'
specialModuleType: SpecialModule | null
titleBarHeight?: number
children?: LayoutNode[]
itemCount?: number
Expand Down Expand Up @@ -141,6 +144,7 @@ function computeTreemapLayoutFromAnalyzeInternal(
type: 'file',
rect,
sourceIndex,
specialModuleType: getSpecialModuleType(analyzeData, sourceIndex),
...analyzeData.getSourceFlags(sourceIndex),
}
}
Expand Down Expand Up @@ -178,6 +182,7 @@ function computeTreemapLayoutFromAnalyzeInternal(
itemCount: countDescendants(sourceIndex),
children: [],
sourceIndex,
specialModuleType: null,
}
}

Expand Down Expand Up @@ -211,6 +216,7 @@ function computeTreemapLayoutFromAnalyzeInternal(
titleBarHeight,
children: [],
sourceIndex,
specialModuleType: null,
}
}

Expand Down Expand Up @@ -240,6 +246,7 @@ function computeTreemapLayoutFromAnalyzeInternal(
titleBarHeight,
children: layoutChildren,
sourceIndex,
specialModuleType: null,
}
}

Expand Down
5 changes: 5 additions & 0 deletions apps/bundle-analyzer/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,8 @@ export interface RouteManifest {
staticRoutes: Array<Route>
dynamicRoutes: Array<Route>
}

export enum SpecialModule {
POLYFILL_MODULE,
POLYFILL_NOMODULE,
}
18 changes: 18 additions & 0 deletions apps/bundle-analyzer/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
import { SpecialModule } from './types'
import { AnalyzeData } from './analyze-data'

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
Expand All @@ -8,3 +10,19 @@ export function cn(...inputs: ClassValue[]) {
export function jsonFetcher<T>(url: string): Promise<T> {
return fetch(url).then((res) => res.json())
}

export function getSpecialModuleType(
analyzeData: AnalyzeData | undefined,
sourceIndex: number | null
): SpecialModule | null {
if (!analyzeData || sourceIndex == null) return null

const path = analyzeData.source(sourceIndex)?.path || ''
if (path.endsWith('polyfill-module.js')) {
return SpecialModule.POLYFILL_MODULE
} else if (path.endsWith('polyfill-nomodule.js')) {
Comment on lines +20 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const path = analyzeData.source(sourceIndex)?.path || ''
if (path.endsWith('polyfill-module.js')) {
return SpecialModule.POLYFILL_MODULE
} else if (path.endsWith('polyfill-nomodule.js')) {
const fullPath = analyzeData.getFullSourcePath(sourceIndex)
if (fullPath.endsWith('polyfill-module.js')) {
return SpecialModule.POLYFILL_MODULE
} else if (fullPath.endsWith('polyfill-nomodule.js')) {

The getSpecialModuleType function uses analyzeData.source(sourceIndex)?.path to detect polyfill chunks, but this only returns a single path segment. It should use getFullSourcePath() to get the complete hierarchical path, just like the existing isPolyfillModule and isPolyfillNoModule methods in the AnalyzeData class.

View Details

Analysis

getSpecialModuleType() fails to detect polyfill chunks due to using partial path instead of full hierarchical path

What fails: The getSpecialModuleType() function in apps/bundle-analyzer/lib/utils.ts uses analyzeData.source(sourceIndex)?.path which returns only a single path segment instead of the complete hierarchical path. This causes it to fail detecting polyfill chunks stored at node_modules/next/dist/build/polyfills/polyfill-module.js and polyfill-nomodule.js, preventing polyfill visual distinction features (color highlighting, "Polyfill" badge) from working.

Root cause: According to the Rust structure in crates/next-api/src/analyze.rs (line 67-69), the path field contains only one segment: "When there is a parent, this is concatenated to the parent's path." Polyfill files, being nested multiple directories deep, are represented across multiple AnalyzeSource nodes in a hierarchy. Using source(sourceIndex)?.path returns only the current node's segment (e.g., a folder name or final filename part), not the complete path.

Expected behavior: The function should use getFullSourcePath() like the existing isPolyfillModule() and isPolyfillNoModule() methods in the AnalyzeData class, which correctly walk the parent chain to construct the full hierarchical path before checking the filename.

Fix: Changed getSpecialModuleType() to use analyzeData.getFullSourcePath(sourceIndex) instead of analyzeData.source(sourceIndex)?.path, matching the pattern used by isPolyfillModule() and isPolyfillNoModule() for consistency and correctness.

return SpecialModule.POLYFILL_NOMODULE
}

return null
}
6 changes: 6 additions & 0 deletions apps/bundle-analyzer/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--polyfill: oklch(0.478 0.0185 252.37);
--polyfill-foreground: oklch(0.99 0 0);
--radius: 0.625rem;
}

Expand All @@ -42,6 +44,8 @@
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--polyfill: oklch(0.478 0.0185 252.37);
--polyfill-foreground: oklch(0.99 0 0);
}

@theme inline {
Expand All @@ -64,6 +68,8 @@
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-polyfill: var(--polyfill);
--color-polyfill-foreground: var(--polyfill-foreground);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
Expand Down
1 change: 1 addition & 0 deletions crates/next-api/src/analyze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ pub async fn analyze_output_assets(output_assets: Vc<OutputAssets>) -> Result<Vc
// Skip source maps.
continue;
}

let output_file_index = builder.add_output_file(AnalyzeOutputFile { filename });
let chunk_parts = split_output_asset_into_parts(*asset).await?;
for chunk_part in chunk_parts {
Expand Down
37 changes: 26 additions & 11 deletions crates/next-api/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,16 @@ use turbopack_core::{
chunk_group_info::{ChunkGroup, ChunkGroupEntry},
},
output::{OutputAsset, OutputAssets, OutputAssetsWithReferenced},
raw_output::RawOutput,
reference::all_assets_from_entries,
reference_type::{CommonJsReferenceSubType, CssReferenceSubType, ReferenceType},
resolve::{origin::PlainResolveOrigin, parse::Request, pattern::Pattern},
source::Source,
source_map::SourceMapAsset,
virtual_output::VirtualOutputAsset,
};
use turbopack_ecmascript::resolve::cjs_resolve;
use turbopack_ecmascript::{
resolve::cjs_resolve, single_file_ecmascript_output::SingleFileEcmascriptOutput,
};

use crate::{
dynamic_imports::{NextDynamicChunkAvailability, collect_next_dynamic_chunks},
Expand Down Expand Up @@ -1300,11 +1302,11 @@ impl AppEndpoint {

let manifest_path_prefix = &app_entry.original_name;

// polyfill-nomodule.js is a pre-compiled asset distributed as part of next,
// load it as a RawModule.
// polyfill-nomodule.js is a pre-compiled asset distributed as part of next
let next_package = get_next_package(project.project_path().owned().await?).await?;
let polyfill_source =
FileSource::new(next_package.join("dist/build/polyfills/polyfill-nomodule.js")?);
let polyfill_source_path =
next_package.join("dist/build/polyfills/polyfill-nomodule.js")?;
let polyfill_source = FileSource::new(polyfill_source_path.clone());
let polyfill_output_path = client_chunking_context
.chunk_path(
Some(Vc::upcast(polyfill_source)),
Expand All @@ -1314,13 +1316,26 @@ impl AppEndpoint {
)
.owned()
.await?;
let polyfill_output_asset = ResolvedVc::upcast(
RawOutput::new(polyfill_output_path, Vc::upcast(polyfill_source))
.to_resolved()
.await?,
);

let polyfill_output = SingleFileEcmascriptOutput::new(
polyfill_output_path.clone(),
polyfill_source_path,
Vc::upcast(polyfill_source),
)
.to_resolved()
.await?;

let polyfill_output_asset = ResolvedVc::upcast(polyfill_output);
client_assets.insert(polyfill_output_asset);

let polyfill_source_map_asset = SourceMapAsset::new_fixed(
polyfill_output_path.clone(),
*ResolvedVc::upcast(polyfill_output),
)
.to_resolved()
.await?;
client_assets.insert(ResolvedVc::upcast(polyfill_source_map_asset));

let client_assets: ResolvedVc<OutputAssets> =
ResolvedVc::cell(client_assets.into_iter().collect::<Vec<_>>());

Expand Down
Loading
Loading