diff --git a/apps/bundle-analyzer/app/globals.css b/apps/bundle-analyzer/app/globals.css index cc4f7fd3b600d..7c945ac8d8677 100644 --- a/apps/bundle-analyzer/app/globals.css +++ b/apps/bundle-analyzer/app/globals.css @@ -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 { @@ -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 { @@ -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 { diff --git a/apps/bundle-analyzer/app/page.tsx b/apps/bundle-analyzer/app/page.tsx index cd4a5390d2418..514cf0dd00b72 100644 --- a/apps/bundle-analyzer/app/page.tsx +++ b/apps/bundle-analyzer/app/page.tsx @@ -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' @@ -137,6 +139,11 @@ export default function Home() { const isAnyLoading = isAnalyzeLoading || isModulesLoading const rootSourceIndex = getRootSourceIndex(analyzeData) + const specialModuleType = getSpecialModuleType( + analyzeData, + selectedSourceIndex + ) + return (
-

- Output Size:{' '} - {formatBytes( - analyzeData.getSourceOutputSize(selectedSourceIndex) +

+
+
+ Output Size:{' '} +
+
+ {formatBytes( + analyzeData.getSourceOutputSize( + selectedSourceIndex + ) + )} +
+
+ {(specialModuleType === SpecialModule.POLYFILL_MODULE || + specialModuleType === + SpecialModule.POLYFILL_NOMODULE) && ( +
+
+ Polyfill +
+
+ Next.js built-in polyfills + {specialModuleType === + SpecialModule.POLYFILL_NOMODULE ? ( + <> + . polyfill-nomodule.js is only + sent to legacy browsers. + + ) : null} +
+
)} -

+
{modulesData && ( void searchQuery?: string filterSource?: (sourceIndex: number) => boolean + isModulePolyfillChunk?: (sourceIndex: number) => boolean + isNoModulePolyfillChunk?: (sourceIndex: number) => boolean } function getFileColor(node: { @@ -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' @@ -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, @@ -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' @@ -516,6 +532,7 @@ function wrapLayoutWithAncestorsUsingIndices( titleBarHeight: titleBarHeight, children: [currentNode], sourceIndex: ancestorIndex, + specialModuleType: null, } currentNode = ancestorNode @@ -539,6 +556,7 @@ function wrapLayoutWithAncestorsUsingIndices( titleBarHeight: minTitleBarHeight, children: [currentNode], sourceIndex: rootIndex, + specialModuleType: null, } return rootNode diff --git a/apps/bundle-analyzer/lib/analyze-data.ts b/apps/bundle-analyzer/lib/analyze-data.ts index 17202049f73c6..1ae82a5aa1706 100644 --- a/apps/bundle-analyzer/lib/analyze-data.ts +++ b/apps/bundle-analyzer/lib/analyze-data.ts @@ -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 diff --git a/apps/bundle-analyzer/lib/treemap-layout.ts b/apps/bundle-analyzer/lib/treemap-layout.ts index 29a3a0e8b37a6..67b023928a97a 100644 --- a/apps/bundle-analyzer/lib/treemap-layout.ts +++ b/apps/bundle-analyzer/lib/treemap-layout.ts @@ -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 @@ -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 @@ -141,6 +144,7 @@ function computeTreemapLayoutFromAnalyzeInternal( type: 'file', rect, sourceIndex, + specialModuleType: getSpecialModuleType(analyzeData, sourceIndex), ...analyzeData.getSourceFlags(sourceIndex), } } @@ -178,6 +182,7 @@ function computeTreemapLayoutFromAnalyzeInternal( itemCount: countDescendants(sourceIndex), children: [], sourceIndex, + specialModuleType: null, } } @@ -211,6 +216,7 @@ function computeTreemapLayoutFromAnalyzeInternal( titleBarHeight, children: [], sourceIndex, + specialModuleType: null, } } @@ -240,6 +246,7 @@ function computeTreemapLayoutFromAnalyzeInternal( titleBarHeight, children: layoutChildren, sourceIndex, + specialModuleType: null, } } diff --git a/apps/bundle-analyzer/lib/types.ts b/apps/bundle-analyzer/lib/types.ts index cb1efb0df7f25..f0320edf363b3 100644 --- a/apps/bundle-analyzer/lib/types.ts +++ b/apps/bundle-analyzer/lib/types.ts @@ -33,3 +33,8 @@ export interface RouteManifest { staticRoutes: Array dynamicRoutes: Array } + +export enum SpecialModule { + POLYFILL_MODULE, + POLYFILL_NOMODULE, +} diff --git a/apps/bundle-analyzer/lib/utils.ts b/apps/bundle-analyzer/lib/utils.ts index 879fcef2e26ea..857a409da3175 100644 --- a/apps/bundle-analyzer/lib/utils.ts +++ b/apps/bundle-analyzer/lib/utils.ts @@ -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)) @@ -8,3 +10,19 @@ export function cn(...inputs: ClassValue[]) { export function jsonFetcher(url: string): Promise { 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')) { + return SpecialModule.POLYFILL_NOMODULE + } + + return null +} diff --git a/apps/bundle-analyzer/styles/globals.css b/apps/bundle-analyzer/styles/globals.css index c3272f8b358be..812455c123c1a 100644 --- a/apps/bundle-analyzer/styles/globals.css +++ b/apps/bundle-analyzer/styles/globals.css @@ -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; } @@ -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 { @@ -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); diff --git a/crates/next-api/src/analyze.rs b/crates/next-api/src/analyze.rs index 97d042e944a51..aaf34df05e1b4 100644 --- a/crates/next-api/src/analyze.rs +++ b/crates/next-api/src/analyze.rs @@ -367,6 +367,7 @@ pub async fn analyze_output_assets(output_assets: Vc) -> Result = ResolvedVc::cell(client_assets.into_iter().collect::>()); diff --git a/crates/next-api/src/route.rs b/crates/next-api/src/route.rs index 2dd77f52dd19e..1d3c5825392f0 100644 --- a/crates/next-api/src/route.rs +++ b/crates/next-api/src/route.rs @@ -9,7 +9,7 @@ use turbo_tasks::{ }; use turbopack_core::{ module_graph::{GraphEntries, ModuleGraph}, - output::OutputAssets, + output::{OptionOutputAsset, OutputAssets}, }; use crate::{operation::OptionEndpoint, paths::ServerPath, project::Project}; @@ -72,6 +72,10 @@ pub trait Endpoint { } #[turbo_tasks::function] fn module_graphs(self: Vc) -> Vc; + #[turbo_tasks::function] + fn polyfill_asset(self: Vc) -> Vc { + Vc::cell(None) + } } #[derive( diff --git a/turbopack/crates/turbopack-ecmascript/src/lib.rs b/turbopack/crates/turbopack-ecmascript/src/lib.rs index 7f329e43e9bfb..9dd250face48e 100644 --- a/turbopack/crates/turbopack-ecmascript/src/lib.rs +++ b/turbopack/crates/turbopack-ecmascript/src/lib.rs @@ -24,6 +24,7 @@ mod path_visitor; pub mod references; pub mod runtime_functions; pub mod side_effect_optimization; +pub mod single_file_ecmascript_output; pub mod source_map; pub(crate) mod static_code; mod swc_comments; diff --git a/turbopack/crates/turbopack-ecmascript/src/single_file_ecmascript_output.rs b/turbopack/crates/turbopack-ecmascript/src/single_file_ecmascript_output.rs new file mode 100644 index 0000000000000..1592cad139c74 --- /dev/null +++ b/turbopack/crates/turbopack-ecmascript/src/single_file_ecmascript_output.rs @@ -0,0 +1,99 @@ +use std::sync::Arc; + +use anyhow::Result; +use swc_core::common::{BytePos, FileName, LineCol, SourceMap}; +use tokio::io::AsyncReadExt; +use turbo_tasks::{ResolvedVc, Vc}; +use turbo_tasks_fs::{FileContent, FileSystemPath, rope::Rope}; +use turbopack_core::{ + asset::{Asset, AssetContent}, + output::{OutputAsset, OutputAssetsReference}, + source::Source, + source_map::{GenerateSourceMap, OptionStringifiedSourceMap}, +}; + +use crate::parse::generate_js_source_map; + +/// An EcmaScript OutputAsset composed of one file, no parsing and no references. Includes a source +/// map to the original file. +#[turbo_tasks::value] +pub struct SingleFileEcmascriptOutput { + output_path: FileSystemPath, + source_path: FileSystemPath, + source: ResolvedVc>, +} + +#[turbo_tasks::value_impl] +impl OutputAsset for SingleFileEcmascriptOutput { + #[turbo_tasks::function] + fn path(&self) -> Vc { + self.output_path.clone().cell() + } +} + +#[turbo_tasks::value_impl] +impl OutputAssetsReference for SingleFileEcmascriptOutput {} + +#[turbo_tasks::value_impl] +impl Asset for SingleFileEcmascriptOutput { + #[turbo_tasks::function] + fn content(&self) -> Vc { + self.source.content() + } +} + +#[turbo_tasks::value_impl] +impl SingleFileEcmascriptOutput { + #[turbo_tasks::function] + pub fn new( + output_path: FileSystemPath, + source_path: FileSystemPath, + source: ResolvedVc>, + ) -> Vc { + SingleFileEcmascriptOutput { + output_path, + source_path, + source, + } + .cell() + } +} + +#[turbo_tasks::value_impl] +impl GenerateSourceMap for SingleFileEcmascriptOutput { + #[turbo_tasks::function] + pub async fn generate_source_map(&self) -> Result> { + let FileContent::Content(file) = &*self.source.content().file_content().await? else { + return Ok(Vc::cell(None)); + }; + + let file_source = { + let mut s = String::new(); + file.read().read_to_string(&mut s).await?; + s + }; + + let mut mappings = vec![]; + // Start from 1 because 0 is reserved for dummy spans in SWC. + let mut pos: u32 = 1; + for (index, line) in file_source.split_inclusive('\n').enumerate() { + mappings.push(( + BytePos(pos), + LineCol { + line: index as u32, + col: 0, + }, + )); + pos += line.len() as u32; + } + + let sm: Arc = Default::default(); + sm.new_source_file( + FileName::Custom(self.source_path.to_string()).into(), + file_source, + ); + + let map = generate_js_source_map(&*sm, mappings, None::<&Rope>, true, true)?; + Ok(Vc::cell(Some(map))) + } +}