Skip to content

Commit 98db6d7

Browse files
committed
Turbopack: make stats.json useable
1 parent 2d19dc6 commit 98db6d7

File tree

6 files changed

+254
-45
lines changed

6 files changed

+254
-45
lines changed

crates/next-api/src/app.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,8 +1368,12 @@ impl AppEndpoint {
13681368
.should_create_webpack_stats()
13691369
.await?
13701370
{
1371-
let webpack_stats =
1372-
generate_webpack_stats(app_entry.original_name.clone(), &client_assets).await?;
1371+
let webpack_stats = generate_webpack_stats(
1372+
*module_graphs.base,
1373+
app_entry.original_name.clone(),
1374+
client_assets.iter().copied(),
1375+
)
1376+
.await?;
13731377
let stats_output = VirtualOutputAsset::new(
13741378
node_root.join(&format!(
13751379
"server/app{manifest_path_prefix}/webpack-stats.json",

crates/next-api/src/pages.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1392,8 +1392,12 @@ impl PageEndpoint {
13921392
.should_create_webpack_stats()
13931393
.await?
13941394
{
1395-
let webpack_stats =
1396-
generate_webpack_stats(original_name.to_owned(), &client_assets.await?).await?;
1395+
let webpack_stats = generate_webpack_stats(
1396+
self.client_module_graph(),
1397+
original_name.to_owned(),
1398+
client_assets.await?.iter().copied(),
1399+
)
1400+
.await?;
13971401
let stats_output = VirtualOutputAsset::new(
13981402
node_root.join(&format!(
13991403
"server/pages{manifest_path_prefix}/webpack-stats.json",

crates/next-api/src/webpack_stats.rs

Lines changed: 183 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,178 @@
11
use anyhow::Result;
2+
use rustc_hash::FxHashSet;
23
use serde::Serialize;
4+
use tracing::{Level, instrument};
35
use turbo_rcstr::RcStr;
4-
use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc, Vc};
6+
use turbo_tasks::{
7+
FxIndexMap, FxIndexSet, ResolvedVc, TryJoinIterExt, ValueToString, Vc, fxindexmap,
8+
};
59
use turbopack_browser::ecmascript::EcmascriptBrowserChunk;
610
use turbopack_core::{
7-
chunk::{Chunk, ChunkItem},
11+
chunk::{Chunk, ChunkItem, ChunkItemExt, ModuleId},
12+
module::Module,
13+
module_graph::ModuleGraph,
814
output::OutputAsset,
915
};
1016

11-
pub async fn generate_webpack_stats<'a, I>(
17+
#[instrument(level = Level::INFO, skip_all)]
18+
pub async fn generate_webpack_stats<I>(
19+
module_graph: Vc<ModuleGraph>,
1220
entry_name: RcStr,
1321
entry_assets: I,
1422
) -> Result<WebpackStats>
1523
where
16-
I: IntoIterator<Item = &'a ResolvedVc<Box<dyn OutputAsset>>>,
24+
I: IntoIterator<Item = ResolvedVc<Box<dyn OutputAsset>>>,
1725
{
1826
let mut assets = vec![];
1927
let mut chunks = vec![];
2028
let mut chunk_items: FxIndexMap<Vc<Box<dyn ChunkItem>>, FxIndexSet<RcStr>> =
2129
FxIndexMap::default();
22-
let mut modules = vec![];
30+
31+
let entry_assets = entry_assets.into_iter().collect::<Vec<_>>();
32+
33+
let (asset_parents, asset_children) = {
34+
let mut asset_children =
35+
FxIndexMap::with_capacity_and_hasher(entry_assets.len(), Default::default());
36+
let mut visited =
37+
FxHashSet::with_capacity_and_hasher(entry_assets.len(), Default::default());
38+
let mut queue = entry_assets.clone();
39+
while let Some(asset) = queue.pop() {
40+
if visited.insert(asset) {
41+
let references = asset.references().await?;
42+
asset_children.insert(asset, references.clone());
43+
queue.extend(references);
44+
}
45+
}
46+
47+
let mut asset_parents: FxIndexMap<_, Vec<_>> =
48+
FxIndexMap::with_capacity_and_hasher(entry_assets.len(), Default::default());
49+
for (&parent, children) in &asset_children {
50+
for child in children {
51+
asset_parents.entry(*child).or_default().push(parent);
52+
}
53+
}
54+
55+
(asset_parents, asset_children)
56+
};
57+
58+
let asset_reasons = {
59+
let module_graph = module_graph.await?;
60+
let mut edges = vec![];
61+
module_graph
62+
.traverse_all_edges_unordered(|(parent_node, r), current| {
63+
edges.push((
64+
parent_node.module,
65+
RcStr::from(format!("{}: {}", r.chunking_type, r.export)),
66+
current.module,
67+
));
68+
Ok(())
69+
})
70+
.await?;
71+
72+
let edges = edges
73+
.into_iter()
74+
.map(async |(parent, ty, child)| {
75+
let parent_path = parent.ident().path().await?.path.clone();
76+
Ok((
77+
child,
78+
WebpackStatsReason {
79+
module: parent_path.clone(),
80+
module_identifier: parent.ident().to_string().owned().await?,
81+
module_name: parent_path,
82+
ty,
83+
},
84+
))
85+
})
86+
.try_join()
87+
.await?;
88+
89+
let mut asset_reasons: FxIndexMap<_, Vec<_>> = FxIndexMap::default();
90+
for (child, reason) in edges {
91+
asset_reasons.entry(child).or_default().push(reason);
92+
}
93+
asset_reasons
94+
};
95+
2396
for asset in entry_assets {
24-
let path = normalize_client_path(&asset.path().await?.path);
97+
let path = RcStr::from(normalize_client_path(&asset.path().await?.path));
2598

2699
let Some(asset_len) = *asset.size_bytes().await? else {
27100
continue;
28101
};
29102

30-
if let Some(chunk) = ResolvedVc::try_downcast_type::<EcmascriptBrowserChunk>(*asset) {
31-
let chunk_ident = normalize_client_path(&chunk.path().await?.path);
103+
if let Some(chunk) = ResolvedVc::try_downcast_type::<EcmascriptBrowserChunk>(asset) {
32104
chunks.push(WebpackStatsChunk {
33105
size: asset_len,
34-
files: vec![chunk_ident.clone().into()],
35-
id: chunk_ident.clone().into(),
106+
files: vec![path.clone()],
107+
id: path.clone(),
108+
parents: if let Some(parents) = asset_parents.get(&asset) {
109+
parents
110+
.iter()
111+
.map(async |c| Ok(normalize_client_path(&c.path().await?.path).into()))
112+
.try_join()
113+
.await?
114+
} else {
115+
vec![]
116+
},
117+
children: if let Some(children) = asset_children.get(&asset) {
118+
children
119+
.iter()
120+
.map(async |c| Ok(normalize_client_path(&c.path().await?.path).into()))
121+
.try_join()
122+
.await?
123+
} else {
124+
vec![]
125+
},
36126
..Default::default()
37127
});
38128

39129
for item in chunk.chunk().chunk_items().await? {
40-
// let name =
41-
chunk_items
42-
.entry(**item)
43-
.or_default()
44-
.insert(chunk_ident.clone().into());
130+
chunk_items.entry(**item).or_default().insert(path.clone());
45131
}
46132
}
47133

48134
assets.push(WebpackStatsAsset {
49135
ty: "asset".into(),
50-
name: path.clone().into(),
51-
chunks: vec![path.into()],
136+
name: path.clone(),
137+
chunk_names: vec![path],
52138
size: asset_len,
53139
..Default::default()
54140
});
55141
}
56142

57-
for (chunk_item, chunks) in chunk_items {
58-
let size = *chunk_item
59-
.content_ident()
60-
.path()
61-
.await?
62-
.read()
63-
.len()
64-
.await?;
65-
let path = chunk_item.asset_ident().path().await?.path.clone();
66-
modules.push(WebpackStatsModule {
67-
name: path.clone(),
68-
id: path.clone(),
69-
chunks: chunks.into_iter().collect(),
70-
size,
71-
});
72-
}
143+
// TODO map the chunk items back to the module
144+
145+
// TODO try downcast modules to `EcmascriptMergedModule` to get the socpe hoisted modules as
146+
// well
147+
148+
let modules = chunk_items
149+
.into_iter()
150+
.map(async |(chunk_item, chunks)| {
151+
let size = *chunk_item
152+
.content_ident()
153+
.path()
154+
.await?
155+
.read()
156+
.len()
157+
.await?;
158+
Ok(WebpackStatsModule {
159+
name: chunk_item.asset_ident().path().await?.path.clone(),
160+
id: chunk_item.id().owned().await?,
161+
identifier: chunk_item.asset_ident().to_string().owned().await?,
162+
chunks: chunks.into_iter().collect(),
163+
size,
164+
// TODO Find all incoming edges to this module
165+
reasons: asset_reasons
166+
.get(&chunk_item.module().to_resolved().await?)
167+
.cloned()
168+
.unwrap_or_default(),
169+
})
170+
})
171+
.try_join()
172+
.await?;
73173

74-
let mut entrypoints = FxIndexMap::default();
75-
entrypoints.insert(
76-
entry_name.clone(),
174+
let entrypoints: FxIndexMap<_, _> = fxindexmap!(
175+
entry_name.clone() =>
77176
WebpackStatsEntrypoint {
78177
name: entry_name.clone(),
79178
chunks: chunks.iter().map(|c| c.id.clone()).collect(),
@@ -83,7 +182,7 @@ where
83182
name: a.name.clone(),
84183
})
85184
.collect(),
86-
},
185+
}
87186
);
88187

89188
Ok(WebpackStats {
@@ -108,35 +207,81 @@ pub struct WebpackStatsAssetInfo {}
108207
pub struct WebpackStatsAsset {
109208
#[serde(rename = "type")]
110209
pub ty: RcStr,
210+
/// The `output` filename
111211
pub name: RcStr,
112212
pub info: WebpackStatsAssetInfo,
213+
/// The size of the file in bytes
113214
pub size: u64,
215+
/// Indicates whether or not the asset made it to the `output` directory
114216
pub emitted: bool,
217+
/// Indicates whether or not the asset was compared with the same file on the output file
218+
/// system
115219
pub compared_for_emit: bool,
116220
pub cached: bool,
221+
/// The chunks this asset contains
222+
pub chunk_names: Vec<RcStr>,
223+
/// The chunk IDs this asset contains
117224
pub chunks: Vec<RcStr>,
118225
}
119226

120227
#[derive(Serialize, Debug, Default)]
121228
#[serde(rename_all = "camelCase")]
122229
pub struct WebpackStatsChunk {
230+
/// Indicates whether or not the chunk went through Code Generation
123231
pub rendered: bool,
232+
/// Indicates whether this chunk is loaded on initial page load or [on
233+
/// demand](/guides/lazy-loading)
124234
pub initial: bool,
235+
/// Indicates whether or not the chunk contains the webpack runtime
125236
pub entry: bool,
126237
pub recorded: bool,
238+
/// The ID of this chunk
127239
pub id: RcStr,
240+
/// Chunk size in bytes
128241
pub size: u64,
129242
pub hash: RcStr,
243+
/// An array of filename strings that contain this chunk
130244
pub files: Vec<RcStr>,
245+
/// An list of chunk names contained within this chunk
246+
pub names: Vec<RcStr>,
247+
/// Parent chunk IDs
248+
pub parents: Vec<RcStr>,
249+
/// Child chunk IDs
250+
pub children: Vec<RcStr>,
131251
}
132252

133253
#[derive(Serialize, Debug)]
134254
#[serde(rename_all = "camelCase")]
135255
pub struct WebpackStatsModule {
256+
/// Path to the actual file
136257
pub name: RcStr,
137-
pub id: RcStr,
258+
/// The ID of the module
259+
pub id: ModuleId,
260+
/// A unique ID used internally
261+
pub identifier: RcStr,
138262
pub chunks: Vec<RcStr>,
139263
pub size: Option<u64>,
264+
pub reasons: Vec<WebpackStatsReason>,
265+
}
266+
267+
#[derive(Clone, Serialize, Debug)]
268+
#[serde(rename_all = "camelCase")]
269+
pub struct WebpackStatsReason {
270+
/// The [WebpackStatsModule::name]
271+
pub module: RcStr,
272+
// /// The [WebpackStatsModule::id]
273+
// pub module_id: ModuleId,
274+
/// The [WebpackStatsModule::identifier]
275+
pub module_identifier: RcStr,
276+
/// A more readable name for the module (used for "pretty-printing")
277+
pub module_name: RcStr,
278+
/// The [type of request](/api/module-methods) used
279+
#[serde(rename = "type")]
280+
pub ty: RcStr,
281+
// /// Raw string used for the `import` or `require` request
282+
// pub user_request: RcStr,
283+
// /// Lines of code that caused the module to be included
284+
// pub loc: RcStr
140285
}
141286

142287
#[derive(Serialize, Debug)]

packages/next/src/shared/lib/turbopack/manifest-loader.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ export class TurbopackManifestLoader {
339339
private mergeWebpackStats(statsFiles: Iterable<WebpackStats>): WebpackStats {
340340
const entrypoints: Record<string, StatsChunkGroup> = {}
341341
const assets: Map<string, StatsAsset> = new Map()
342-
const chunks: Map<string, StatsChunk> = new Map()
342+
const chunks: Map<string | number, StatsChunk> = new Map()
343343
const modules: Map<string | number, StatsModule> = new Map()
344344

345345
for (const statsFile of statsFiles) {
@@ -361,8 +361,8 @@ export class TurbopackManifestLoader {
361361

362362
if (statsFile.chunks) {
363363
for (const chunk of statsFile.chunks) {
364-
if (!chunks.has(chunk.name)) {
365-
chunks.set(chunk.name, chunk)
364+
if (!chunks.has(chunk.id!)) {
365+
chunks.set(chunk.id!, chunk)
366366
}
367367
}
368368
}
@@ -388,6 +388,7 @@ export class TurbopackManifestLoader {
388388
}
389389

390390
return {
391+
version: 'Turbopack',
391392
entrypoints,
392393
assets: [...assets.values()],
393394
chunks: [...chunks.values()],

0 commit comments

Comments
 (0)