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) 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..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 @@ -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,88 @@ 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('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', + 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', () => {