Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,19 @@ export function scanClientChunks(
chunksByFileName.set(bundleEntry.fileName, bundleEntry)

if (bundleEntry.isEntry) {
if (entryChunk) {
throw new Error(
`multiple entries detected: ${entryChunk.fileName} ${bundleEntry.fileName}`,
)
// Skip injected entries from bundler plugins (e.g. @module-federation/vite
// emits hostInit and remoteEntry entries). These are not the app entry.
const facadeId = bundleEntry.facadeModuleId ?? ''
const isPluginInjectedEntry =
facadeId.includes('__mf__virtual') || facadeId.startsWith('virtual:mf-')
Copy link
Copy Markdown
Contributor

@schiller-manuel schiller-manuel Apr 2, 2026

Choose a reason for hiding this comment

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

we will not hardcode "arbitrary" strings here. if at all, i could imagine a generic, configurable filter logic (via a callback for example). however we are restructuring plugins right now, so this has to wait

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

No worries! Thank you for taking the time to review this! Is there a thread or PR I can follow while we wait?

if (!isPluginInjectedEntry) {
if (entryChunk) {
throw new Error(
`multiple entries detected: ${entryChunk.fileName} ${bundleEntry.fileName}`,
)
}
entryChunk = bundleEntry
}
entryChunk = bundleEntry
}

const routeFilePaths = getRouteFilePathsFromModuleIds(bundleEntry.moduleIds)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ function makeChunk(options: {
importedCss?: Array<string>
moduleIds?: Array<string>
isEntry?: boolean
facadeModuleId?: string | null
}): Rollup.OutputChunk {
return {
type: 'chunk',
Expand All @@ -29,7 +30,7 @@ function makeChunk(options: {
moduleIds: options.moduleIds ?? [],
isEntry: options.isEntry ?? false,
isDynamicEntry: false,
facadeModuleId: null,
facadeModuleId: options.facadeModuleId ?? null,
implicitlyLoadedBefore: [],
importedBindings: {},
modules: {},
Expand Down Expand Up @@ -192,6 +193,67 @@ describe('scanClientChunks', () => {
'No entry file found',
)
})

test('skips __mf__virtual plugin-injected entry chunks', () => {
const appEntry = makeChunk({
fileName: 'main.js',
isEntry: true,
facadeModuleId: '/project/src/client.tsx',
})
const mfHostInit = makeChunk({
fileName: 'hostInit.js',
isEntry: true,
facadeModuleId:
'/project/node_modules/__mf__virtual/host__H_A_I__hostAutoInit__H_A_I__.js',
})

const scanned = scanClientChunks({
'main.js': appEntry,
'hostInit.js': mfHostInit,
})

expect(scanned.entryChunk).toBe(appEntry)
})

test('skips virtual:mf- plugin-injected entry chunks', () => {
const appEntry = makeChunk({
fileName: 'main.js',
isEntry: true,
facadeModuleId: '/project/src/client.tsx',
})
const mfRemoteEntry = makeChunk({
fileName: 'remoteEntry.js',
isEntry: true,
facadeModuleId: 'virtual:mf-REMOTE_ENTRY_ID:host__remoteEntry-hash',
})

const scanned = scanClientChunks({
'main.js': appEntry,
'remoteEntry.js': mfRemoteEntry,
})

expect(scanned.entryChunk).toBe(appEntry)
})

test('still throws on multiple non-plugin app entries', () => {
const entryA = makeChunk({
fileName: 'entryA.js',
isEntry: true,
facadeModuleId: '/project/src/clientA.tsx',
})
const entryB = makeChunk({
fileName: 'entryB.js',
isEntry: true,
facadeModuleId: '/project/src/clientB.tsx',
})

expect(() =>
scanClientChunks({
'entryA.js': entryA,
'entryB.js': entryB,
}),
).toThrow('multiple entries detected')
})
})

describe('collectDynamicImportCss', () => {
Expand Down