Skip to content

Commit fc2e52c

Browse files
committed
fix(pack): scope dev stats to entrypoints
1 parent eb486d6 commit fc2e52c

4 files changed

Lines changed: 209 additions & 20 deletions

File tree

crates/pack-api/src/entrypoint.rs

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use crate::{
1414
operation::EntrypointsOperation,
1515
project::ProjectContainer,
1616
utils::get_issues,
17-
webpack_stats::generate_webpack_stats,
17+
webpack_stats::{OutputAssetGroups, generate_webpack_stats},
1818
};
1919

2020
#[turbo_tasks::value(shared)]
@@ -59,15 +59,19 @@ pub async fn all_output_assets_operation(
5959
) -> Result<Vc<OutputAssets>> {
6060
let project = container.project();
6161

62-
let endpoint_assets = project
62+
let endpoint_asset_groups = project
6363
.get_all_endpoints()
6464
.await?
6565
.iter()
66-
.map(|endpoint| async move { endpoint.output().await?.output_assets.await })
66+
.map(|endpoint| async move { Ok(endpoint.output().await?.output_assets) })
6767
.try_join()
6868
.await?;
6969

70-
let output_assets: FxIndexSet<ResolvedVc<Box<dyn OutputAsset>>> = endpoint_assets
70+
let output_assets: FxIndexSet<ResolvedVc<Box<dyn OutputAsset>>> = endpoint_asset_groups
71+
.iter()
72+
.map(|assets| async move { assets.await })
73+
.try_join()
74+
.await?
7175
.iter()
7276
.flat_map(|assets| assets.iter().copied())
7377
.collect();
@@ -89,22 +93,56 @@ pub async fn all_output_assets_operation(
8993
let mut stats_outputs: Vec<ResolvedVc<Box<dyn OutputAsset>>> = Vec::new();
9094

9195
if !has_server {
92-
stats_outputs.push(make_stats_output(output_assets, dist_root).await?);
96+
stats_outputs.push(
97+
make_stats_output(
98+
output_assets,
99+
Vc::<OutputAssetGroups>::cell(endpoint_asset_groups),
100+
dist_root,
101+
)
102+
.await?,
103+
);
93104
} else {
94105
let server_dist_root_vc = container.project().server_dist_root();
95106
let server_dist_root_read = server_dist_root_vc.await?;
96107
let mut client: Vec<ResolvedVc<Box<dyn OutputAsset>>> = Vec::new();
97108
let mut server: Vec<ResolvedVc<Box<dyn OutputAsset>>> = Vec::new();
109+
let mut client_groups = Vec::with_capacity(endpoint_asset_groups.len());
110+
for assets in endpoint_asset_groups {
111+
let mut group = Vec::new();
112+
for asset in assets.await?.iter().copied() {
113+
if !asset.path().await?.is_inside_ref(&server_dist_root_read) {
114+
group.push(asset);
115+
}
116+
}
117+
if !group.is_empty() {
118+
client_groups.push(ResolvedVc::cell(group));
119+
}
120+
}
98121
for asset in output_assets.await?.iter().copied() {
99122
if asset.path().await?.is_inside_ref(&server_dist_root_read) {
100123
server.push(asset);
101124
} else {
102125
client.push(asset);
103126
}
104127
}
105-
stats_outputs.push(make_stats_output(Vc::cell(client), dist_root).await?);
128+
stats_outputs.push(
129+
make_stats_output(
130+
Vc::cell(client),
131+
Vc::<OutputAssetGroups>::cell(client_groups),
132+
dist_root,
133+
)
134+
.await?,
135+
);
106136
if !server.is_empty() {
107-
stats_outputs.push(make_stats_output(Vc::cell(server), server_dist_root_vc).await?);
137+
let server_assets = ResolvedVc::cell(server);
138+
stats_outputs.push(
139+
make_stats_output(
140+
*server_assets,
141+
Vc::<OutputAssetGroups>::cell(vec![server_assets]),
142+
server_dist_root_vc,
143+
)
144+
.await?,
145+
);
108146
}
109147
}
110148

@@ -113,9 +151,10 @@ pub async fn all_output_assets_operation(
113151

114152
async fn make_stats_output(
115153
assets: Vc<OutputAssets>,
154+
asset_groups: Vc<OutputAssetGroups>,
116155
dist_root: Vc<FileSystemPath>,
117156
) -> Result<ResolvedVc<Box<dyn OutputAsset>>> {
118-
let webpack_stats = generate_webpack_stats(assets, dist_root).await?;
157+
let webpack_stats = generate_webpack_stats(assets, asset_groups, dist_root).await?;
119158
let stats_json = serde_json::to_string_pretty(&*webpack_stats)?;
120159
let dist_root_owned = dist_root.owned().await?;
121160
let stats_output = VirtualOutputAsset::new(

crates/pack-api/src/webpack_stats.rs

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ pub struct AssetIntermediateInfo {
3131
pub dev_chunk_list: Option<RcStr>,
3232
}
3333

34+
#[turbo_tasks::value(transparent)]
35+
pub struct OutputAssetGroups(pub Vec<ResolvedVc<OutputAssets>>);
36+
3437
fn normalize_stats_path(path: RcStr) -> RcStr {
3538
path.strip_prefix("./").map(Into::into).unwrap_or(path)
3639
}
@@ -381,6 +384,7 @@ pub async fn get_asset_intermediate_info(
381384
#[turbo_tasks::function]
382385
pub async fn generate_webpack_stats(
383386
entry_assets: Vc<OutputAssets>,
387+
entry_asset_groups: Vc<OutputAssetGroups>,
384388
dist_root: Vc<FileSystemPath>,
385389
) -> Result<Vc<WebpackStats>> {
386390
let mut assets = vec![];
@@ -402,10 +406,13 @@ pub async fn generate_webpack_stats(
402406
})
403407
.try_join()
404408
.await?;
409+
let asset_info_by_asset: FxHashMap<_, _> = all_assets
410+
.iter()
411+
.copied()
412+
.zip(asset_results.iter())
413+
.collect();
405414

406-
let mut dev_chunk_lists: Vec<RcStr> = vec![];
407-
for info in asset_results {
408-
let info = info;
415+
for info in &asset_results {
409416
if seen_asset_paths.insert(info.asset.name.clone()) {
410417
assets.push(info.asset.clone());
411418
}
@@ -424,17 +431,39 @@ pub async fn generate_webpack_stats(
424431
modules.insert(module.id.clone(), module.clone());
425432
}
426433
}
427-
if let Some(dev_chunk_list) = &info.dev_chunk_list {
428-
dev_chunk_lists.push(dev_chunk_list.clone());
429-
}
430434
}
431435

432-
for dev_chunk_list in dev_chunk_lists {
433-
for entrypoint in entrypoints.values_mut() {
434-
entrypoint.chunks.push(dev_chunk_list.clone());
435-
entrypoint.assets.push(WebpackStatsEntrypointAssets {
436-
name: dev_chunk_list.clone(),
437-
});
436+
// Endpoint output groups preserve which evaluate entry owns each development chunk list.
437+
// Associating these lists after flattening all output assets made every entrypoint include
438+
// every other page's HMR bootstrap in multi-page builds.
439+
for group in entry_asset_groups.await?.iter().copied() {
440+
let group = group.await?;
441+
let group_entrypoints: FxIndexMap<_, _> = group
442+
.iter()
443+
.filter_map(|asset| asset_info_by_asset.get(asset))
444+
.flat_map(|info| info.entrypoints.iter())
445+
.map(|(name, _)| (name.clone(), ()))
446+
.collect();
447+
let group_chunk_lists: FxIndexMap<_, _> = group
448+
.iter()
449+
.filter_map(|asset| asset_info_by_asset.get(asset))
450+
.filter_map(|info| info.dev_chunk_list.as_ref())
451+
.map(|name| (name.clone(), ()))
452+
.collect();
453+
454+
for entrypoint_name in group_entrypoints.keys() {
455+
let Some(entrypoint) = entrypoints.get_mut(entrypoint_name) else {
456+
continue;
457+
};
458+
for dev_chunk_list in group_chunk_lists.keys() {
459+
if entrypoint.chunks.contains(dev_chunk_list) {
460+
continue;
461+
}
462+
entrypoint.chunks.push(dev_chunk_list.clone());
463+
entrypoint.assets.push(WebpackStatsEntrypointAssets {
464+
name: dev_chunk_list.clone(),
465+
});
466+
}
438467
}
439468
}
440469

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import fs from "fs";
2+
import path from "path";
3+
import { serve } from "../commands/dev";
4+
5+
const [, , projectPath, portArg] = process.argv;
6+
7+
if (!projectPath || !portArg) {
8+
throw new Error("Usage: serveMultiClientStatsChild <projectPath> <port>");
9+
}
10+
11+
const port = Number(portArg);
12+
const srcDir = path.join(projectPath, "src");
13+
const statsPath = path.join(projectPath, "dist", "stats.json");
14+
15+
function entrypointAssets(stats: any, name: string): string[] {
16+
return (stats.entrypoints?.[name]?.assets ?? []).map((asset: any) =>
17+
typeof asset === "string" ? asset : (asset?.name ?? ""),
18+
);
19+
}
20+
21+
function chunkLists(assets: string[]): string[] {
22+
return assets.filter(
23+
(asset) => asset.includes("src_alpha_") || asset.includes("src_beta_"),
24+
);
25+
}
26+
27+
async function waitForStats() {
28+
const deadline = Date.now() + 20_000;
29+
30+
while (true) {
31+
if (fs.existsSync(statsPath)) {
32+
const stats = JSON.parse(fs.readFileSync(statsPath, "utf8"));
33+
if (stats.entrypoints?.alpha && stats.entrypoints?.beta) {
34+
return stats;
35+
}
36+
}
37+
if (Date.now() > deadline) {
38+
throw new Error(`Timed out waiting for ${statsPath}`);
39+
}
40+
await new Promise((resolve) => setTimeout(resolve, 100));
41+
}
42+
}
43+
44+
async function main() {
45+
fs.rmSync(projectPath, { recursive: true, force: true });
46+
fs.mkdirSync(srcDir, { recursive: true });
47+
fs.writeFileSync(
48+
path.join(srcDir, "alpha.js"),
49+
'import("./alpha-lazy.js").then(({ default: value }) => console.log(value));\n',
50+
);
51+
fs.writeFileSync(
52+
path.join(srcDir, "alpha-lazy.js"),
53+
'export default "alpha";\n',
54+
);
55+
fs.writeFileSync(
56+
path.join(srcDir, "beta.js"),
57+
'import("./beta-lazy.js").then(({ default: value }) => console.log(value));\n',
58+
);
59+
fs.writeFileSync(
60+
path.join(srcDir, "beta-lazy.js"),
61+
'export default "beta";\n',
62+
);
63+
64+
await serve(
65+
{
66+
config: {
67+
entry: [
68+
{ import: "./src/alpha.js", name: "alpha" },
69+
{ import: "./src/beta.js", name: "beta" },
70+
],
71+
output: { path: "./dist", clean: true },
72+
stats: true,
73+
},
74+
},
75+
projectPath,
76+
projectPath,
77+
{
78+
hostname: "127.0.0.1",
79+
logServerInfo: false,
80+
port,
81+
},
82+
);
83+
84+
const stats = await waitForStats();
85+
const alphaChunkLists = chunkLists(entrypointAssets(stats, "alpha"));
86+
const betaChunkLists = chunkLists(entrypointAssets(stats, "beta"));
87+
88+
console.log(
89+
`__STATS_SNAPSHOT__${JSON.stringify({
90+
alphaHasOwnChunkLists: alphaChunkLists.some((asset) =>
91+
asset.includes("src_alpha_"),
92+
),
93+
alphaHasOnlyOwnChunkLists: alphaChunkLists.every((asset) =>
94+
asset.includes("src_alpha_"),
95+
),
96+
betaHasOwnChunkLists: betaChunkLists.some((asset) =>
97+
asset.includes("src_beta_"),
98+
),
99+
betaHasOnlyOwnChunkLists: betaChunkLists.every((asset) =>
100+
asset.includes("src_beta_"),
101+
),
102+
})}`,
103+
);
104+
process.kill(process.pid, "SIGTERM");
105+
}
106+
107+
main().catch((error) => {
108+
console.error(error);
109+
process.exit(1);
110+
});

packages/pack/src/__test__/serveStats.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,17 @@ describe("serve stats", () => {
135135
`);
136136
}, 30_000);
137137

138+
it("keeps dev chunk lists scoped to their owning entrypoint", async () => {
139+
await expect(
140+
runServeStatsFixture("serveMultiClientStatsChild.ts"),
141+
).resolves.toEqual({
142+
alphaHasOwnChunkLists: true,
143+
alphaHasOnlyOwnChunkLists: true,
144+
betaHasOwnChunkLists: true,
145+
betaHasOnlyOwnChunkLists: true,
146+
});
147+
}, 30_000);
148+
138149
it("keeps all named server entries after rebuilding one entry", async () => {
139150
await expect(
140151
runServeStatsFixture("serveMultiServerStatsChild.ts"),

0 commit comments

Comments
 (0)