From 3c2de71aefc52c521dd5b0bdf041c2b0ff5ecbf5 Mon Sep 17 00:00:00 2001 From: Jimmy Multani Date: Wed, 1 Apr 2026 12:12:23 -0400 Subject: [PATCH 1/3] fix(start): skip plugin-injected entries in manifest scanner --- .../start-manifest-plugin/manifestBuilder.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/start-plugin-core/src/start-manifest-plugin/manifestBuilder.ts b/packages/start-plugin-core/src/start-manifest-plugin/manifestBuilder.ts index c08ae4ce3f2..d27272174b9 100644 --- a/packages/start-plugin-core/src/start-manifest-plugin/manifestBuilder.ts +++ b/packages/start-plugin-core/src/start-manifest-plugin/manifestBuilder.ts @@ -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-') + if (!isPluginInjectedEntry) { + if (entryChunk) { + throw new Error( + `multiple entries detected: ${entryChunk.fileName} ${bundleEntry.fileName}`, + ) + } + entryChunk = bundleEntry } - entryChunk = bundleEntry } const routeFilePaths = getRouteFilePathsFromModuleIds(bundleEntry.moduleIds) From ce55d8299bac861b790799c88e14308c49cacd9d Mon Sep 17 00:00:00 2001 From: Jimmy Multani Date: Wed, 1 Apr 2026 12:58:25 -0400 Subject: [PATCH 2/3] test(start): add scanClientChunks tests for plugin-injected entries --- .../manifestBuilder.test.ts | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts b/packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts index 40eddb745e4..d7a9f57dae6 100644 --- a/packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts +++ b/packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts @@ -19,6 +19,7 @@ function makeChunk(options: { importedCss?: Array moduleIds?: Array isEntry?: boolean + facadeModuleId?: string | null }): Rollup.OutputChunk { return { type: 'chunk', @@ -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: {}, @@ -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', () => { From d1ee1ea07326b9af0fd7898e1a0a97e6b4306d36 Mon Sep 17 00:00:00 2001 From: Jimmy Multani Date: Wed, 1 Apr 2026 13:21:58 -0400 Subject: [PATCH 3/3] test(start): add reversed traversal order test for plugin-injected entries --- .../manifestBuilder.test.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts b/packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts index d7a9f57dae6..2db8b392216 100644 --- a/packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts +++ b/packages/start-plugin-core/tests/start-manifest-plugin/manifestBuilder.test.ts @@ -235,6 +235,27 @@ describe('scanClientChunks', () => { expect(scanned.entryChunk).toBe(appEntry) }) + test('skips plugin-injected entries even when scanned before the app entry', () => { + const mfHostInit = makeChunk({ + fileName: 'hostInit.js', + isEntry: true, + facadeModuleId: + '/project/node_modules/__mf__virtual/host__H_A_I__hostAutoInit__H_A_I__.js', + }) + const appEntry = makeChunk({ + fileName: 'main.js', + isEntry: true, + facadeModuleId: '/project/src/client.tsx', + }) + + const scanned = scanClientChunks({ + 'hostInit.js': mfHostInit, + 'main.js': appEntry, + }) + + expect(scanned.entryChunk).toBe(appEntry) + }) + test('still throws on multiple non-plugin app entries', () => { const entryA = makeChunk({ fileName: 'entryA.js',