Skip to content

Commit 3dcb7db

Browse files
amir-zahediclaude
andauthored
fix(gdu): collect CSS from static imports in build manifest (#413)
* fix(gdu): collect CSS from static imports in build manifest The build manifest was only collecting CSS from dynamic chunk imports, missing CSS files from statically imported modules. This adds a recursive walk of static imports to ensure all CSS dependencies are included in the manifest entry for each MFE entry point. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: Applies formatting * chore: add changeset for static import CSS collection fix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3235892 commit 3dcb7db

File tree

4 files changed

+263
-7
lines changed

4 files changed

+263
-7
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"gdu": patch
3+
---
4+
5+
Collect CSS from statically imported chunks in build manifest
6+
7+
The `collectEntryCssFiles` function now traverses the entry chunk's static import tree (via `chunk.imports`) to gather CSS from all transitively-imported chunks. Previously, only CSS from the entry chunk itself was included in `assets.css`, causing missing styles for MFEs that render statically-imported Overdrive components before any lazy route loads.

packages/gdu/config/vite/plugins/GuruBuildManifest.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,25 +61,41 @@ function ensureDirectoryExists(dir: string): void {
6161
fs.mkdirSync(dir, { recursive: true });
6262
}
6363

64-
function collectEntryCssFiles(
64+
export function collectEntryCssFiles(
6565
bundle: Record<
6666
string,
6767
{
6868
type: string;
6969
isEntry?: boolean;
70+
imports?: string[];
7071
viteMetadata?: { importedCss?: Set<string> };
7172
}
7273
>,
7374
): Set<string> {
7475
const entryCssFiles = new Set<string>();
75-
for (const chunk of Object.values(bundle)) {
76-
if (chunk.type !== 'chunk' || !chunk.isEntry) continue;
77-
const referencedCss = chunk.viteMetadata?.importedCss;
78-
if (!referencedCss) continue;
79-
for (const cssFile of referencedCss) {
80-
entryCssFiles.add(cssFile);
76+
const visited = new Set<string>();
77+
78+
function walk(fileName: string) {
79+
if (visited.has(fileName)) return;
80+
visited.add(fileName);
81+
82+
const chunk = bundle[fileName];
83+
if (!chunk || chunk.type !== 'chunk') return;
84+
85+
const css = chunk.viteMetadata?.importedCss;
86+
if (css) {
87+
for (const file of css) entryCssFiles.add(file);
88+
}
89+
90+
if (chunk.imports) {
91+
for (const dep of chunk.imports) walk(dep);
8192
}
8293
}
94+
95+
for (const [fileName, chunk] of Object.entries(bundle)) {
96+
if (chunk.type === 'chunk' && chunk.isEntry) walk(fileName);
97+
}
98+
8399
return entryCssFiles;
84100
}
85101

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { collectEntryCssFiles } from '../GuruBuildManifest';
2+
3+
type BundleChunk = {
4+
type: string;
5+
isEntry?: boolean;
6+
imports?: string[];
7+
dynamicImports?: string[];
8+
viteMetadata?: { importedCss?: Set<string> };
9+
};
10+
11+
type MockBundle = Record<string, BundleChunk>;
12+
13+
describe('collectEntryCssFiles', () => {
14+
it('collects CSS only from the entry chunk itself', () => {
15+
const bundle: MockBundle = {
16+
'assets/main-abc123.js': {
17+
type: 'chunk',
18+
isEntry: true,
19+
viteMetadata: {
20+
importedCss: new Set(['assets/main-abc123.css']),
21+
},
22+
},
23+
};
24+
25+
const result = collectEntryCssFiles(bundle);
26+
27+
expect(result).toEqual(new Set(['assets/main-abc123.css']));
28+
});
29+
30+
it('collects CSS from statically imported sibling chunks', () => {
31+
const bundle: MockBundle = {
32+
'assets/main-abc123.js': {
33+
type: 'chunk',
34+
isEntry: true,
35+
imports: ['assets/vendor-def456.js'],
36+
viteMetadata: {
37+
importedCss: new Set(['assets/main-abc123.css']),
38+
},
39+
},
40+
'assets/vendor-def456.js': {
41+
type: 'chunk',
42+
viteMetadata: {
43+
importedCss: new Set(['assets/vendor-def456.css']),
44+
},
45+
},
46+
};
47+
48+
const result = collectEntryCssFiles(bundle);
49+
50+
expect(result).toEqual(
51+
new Set(['assets/main-abc123.css', 'assets/vendor-def456.css']),
52+
);
53+
});
54+
55+
it('collects CSS from deep transitive static imports', () => {
56+
const bundle: MockBundle = {
57+
'assets/entry-aaa.js': {
58+
type: 'chunk',
59+
isEntry: true,
60+
imports: ['assets/a-bbb.js'],
61+
viteMetadata: {
62+
importedCss: new Set(['assets/entry.css']),
63+
},
64+
},
65+
'assets/a-bbb.js': {
66+
type: 'chunk',
67+
imports: ['assets/b-ccc.js'],
68+
viteMetadata: {
69+
importedCss: new Set(['assets/a.css']),
70+
},
71+
},
72+
'assets/b-ccc.js': {
73+
type: 'chunk',
74+
imports: ['assets/c-ddd.js'],
75+
viteMetadata: {
76+
importedCss: new Set(['assets/b.css']),
77+
},
78+
},
79+
'assets/c-ddd.js': {
80+
type: 'chunk',
81+
viteMetadata: {
82+
importedCss: new Set(['assets/c.css']),
83+
},
84+
},
85+
};
86+
87+
const result = collectEntryCssFiles(bundle);
88+
89+
expect(result).toEqual(
90+
new Set([
91+
'assets/entry.css',
92+
'assets/a.css',
93+
'assets/b.css',
94+
'assets/c.css',
95+
]),
96+
);
97+
});
98+
99+
it('handles circular imports without infinite looping', () => {
100+
const bundle: MockBundle = {
101+
'assets/entry-aaa.js': {
102+
type: 'chunk',
103+
isEntry: true,
104+
imports: ['assets/a-bbb.js'],
105+
viteMetadata: {
106+
importedCss: new Set(['assets/entry.css']),
107+
},
108+
},
109+
'assets/a-bbb.js': {
110+
type: 'chunk',
111+
imports: ['assets/b-ccc.js'],
112+
viteMetadata: {
113+
importedCss: new Set(['assets/a.css']),
114+
},
115+
},
116+
'assets/b-ccc.js': {
117+
type: 'chunk',
118+
imports: ['assets/a-bbb.js'],
119+
viteMetadata: {
120+
importedCss: new Set(['assets/b.css']),
121+
},
122+
},
123+
};
124+
125+
const result = collectEntryCssFiles(bundle);
126+
127+
expect(result).toEqual(
128+
new Set(['assets/entry.css', 'assets/a.css', 'assets/b.css']),
129+
);
130+
});
131+
132+
it('does not collect CSS from dynamically imported chunks', () => {
133+
const bundle: MockBundle = {
134+
'assets/main-abc123.js': {
135+
type: 'chunk',
136+
isEntry: true,
137+
imports: ['assets/static-dep.js'],
138+
dynamicImports: ['assets/lazy-page.js'],
139+
viteMetadata: {
140+
importedCss: new Set(['assets/main.css']),
141+
},
142+
},
143+
'assets/static-dep.js': {
144+
type: 'chunk',
145+
viteMetadata: {
146+
importedCss: new Set(['assets/static.css']),
147+
},
148+
},
149+
'assets/lazy-page.js': {
150+
type: 'chunk',
151+
viteMetadata: {
152+
importedCss: new Set(['assets/lazy.css']),
153+
},
154+
},
155+
};
156+
157+
const result = collectEntryCssFiles(bundle);
158+
159+
expect(result).toEqual(
160+
new Set(['assets/main.css', 'assets/static.css']),
161+
);
162+
expect(result.has('assets/lazy.css')).toBe(false);
163+
});
164+
165+
it('handles chunks without viteMetadata gracefully', () => {
166+
const bundle: MockBundle = {
167+
'assets/main-abc123.js': {
168+
type: 'chunk',
169+
isEntry: true,
170+
imports: ['assets/no-css.js', 'assets/no-metadata.js'],
171+
viteMetadata: {
172+
importedCss: new Set(['assets/main.css']),
173+
},
174+
},
175+
'assets/no-css.js': {
176+
type: 'chunk',
177+
viteMetadata: {},
178+
},
179+
'assets/no-metadata.js': {
180+
type: 'chunk',
181+
},
182+
};
183+
184+
const result = collectEntryCssFiles(bundle);
185+
186+
expect(result).toEqual(new Set(['assets/main.css']));
187+
});
188+
189+
it('collects CSS independently from multiple entry chunks', () => {
190+
const bundle: MockBundle = {
191+
'assets/app-aaa.js': {
192+
type: 'chunk',
193+
isEntry: true,
194+
imports: ['assets/shared-lib.js'],
195+
viteMetadata: {
196+
importedCss: new Set(['assets/app.css']),
197+
},
198+
},
199+
'assets/worker-bbb.js': {
200+
type: 'chunk',
201+
isEntry: true,
202+
imports: ['assets/worker-dep.js'],
203+
viteMetadata: {
204+
importedCss: new Set(['assets/worker.css']),
205+
},
206+
},
207+
'assets/shared-lib.js': {
208+
type: 'chunk',
209+
viteMetadata: {
210+
importedCss: new Set(['assets/shared.css']),
211+
},
212+
},
213+
'assets/worker-dep.js': {
214+
type: 'chunk',
215+
viteMetadata: {
216+
importedCss: new Set(['assets/worker-dep.css']),
217+
},
218+
},
219+
};
220+
221+
const result = collectEntryCssFiles(bundle);
222+
223+
expect(result).toEqual(
224+
new Set([
225+
'assets/app.css',
226+
'assets/shared.css',
227+
'assets/worker.css',
228+
'assets/worker-dep.css',
229+
]),
230+
);
231+
});
232+
});

packages/gdu/config/vite/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface RolldownOutputChunk {
2424
fileName: string;
2525
isEntry: boolean;
2626
isDynamicEntry: boolean;
27+
imports?: string[];
2728
viteMetadata?: { importedCss?: Set<string> };
2829
}
2930

0 commit comments

Comments
 (0)