diff --git a/.changeset/shiny-planes-hope.md b/.changeset/shiny-planes-hope.md new file mode 100644 index 000000000..074d0b37d --- /dev/null +++ b/.changeset/shiny-planes-hope.md @@ -0,0 +1,5 @@ +--- +"@callstack/repack": patch +--- + +Use `done` hook inside of `OutputPlugin` diff --git a/packages/repack/src/webpack/plugins/OutputPlugin.ts b/packages/repack/src/webpack/plugins/OutputPlugin.ts index e23e185ed..37689abb1 100644 --- a/packages/repack/src/webpack/plugins/OutputPlugin.ts +++ b/packages/repack/src/webpack/plugins/OutputPlugin.ts @@ -183,6 +183,11 @@ export class OutputPlugin implements WebpackPlugin { return; } + const outputPath = compiler.options.output?.path; + if (!outputPath) { + throw new Error('Cannot infer output path from compilation'); + } + const logger = compiler.getInfrastructureLogger('RepackOutputPlugin'); const extraAssets = (this.config.extraChunks ?? []).map((spec) => @@ -213,205 +218,202 @@ export class OutputPlugin implements WebpackPlugin { } } } - return false; }; - let entryGroup: webpack.Compilation['chunkGroups'][0] | undefined; - let entryChunk: webpack.Chunk | undefined; - const entryChunkName = this.config.entryName ?? 'main'; - const localChunks: webpack.Chunk[] = []; - const remoteChunks: webpack.Chunk[] = []; - const auxiliaryAssets: Set = new Set(); - - compiler.hooks.compilation.tap('RepackOutputPlugin', (compilation) => { - compilation.hooks.processAssets.tap( - { - name: 'RepackOutputPlugin', - stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, - }, - () => { - entryGroup = compilation.chunkGroups.find((group) => - group.isInitial() - ); - entryChunk = entryGroup?.chunks.find( - (chunk) => chunk.name === entryChunkName - ); - const sharedChunks = new Set(); - - for (const chunk of compilation.chunks) { - // Do not process shared chunks right now. - if (sharedChunks.has(chunk)) { - continue; - } - - [...chunk.getAllInitialChunks()] - .filter((sharedChunk) => sharedChunk !== chunk) - .forEach((sharedChunk) => { - sharedChunks.add(sharedChunk); - }); + const getRelatedSourceMap = (chunk: webpack.StatsChunk) => { + return chunk.auxiliaryFiles?.find((file) => /\.map$/.test(file)); + }; - // Entry chunk - if (entryChunk && entryChunk === chunk) { - localChunks.push(chunk); - } else if (isLocalChunk(chunk.name ?? chunk.id?.toString())) { - localChunks.push(chunk); - } else { - remoteChunks.push(chunk); - } - } + const getAllInitialChunks = ( + chunk: webpack.StatsChunk, + chunks: Map + ): Array => { + if (!chunk.parents?.length) return [chunk]; + return chunk.parents.flatMap((parent) => { + return getAllInitialChunks(chunks.get(parent)!, chunks); + }); + }; - // Process shared chunks to add them either as local or remote chunk. - for (const sharedChunk of sharedChunks) { - const isUsedByLocalChunk = localChunks.some((localChunk) => { - return [...localChunk.getAllInitialChunks()].includes( - sharedChunk - ); - }); - if ( - isUsedByLocalChunk || - isLocalChunk(sharedChunk.name ?? sharedChunk.id?.toString()) - ) { - localChunks.push(sharedChunk); - } else { - remoteChunks.push(sharedChunk); - } - } + compiler.hooks.done.tapPromise('RepackOutputPlugin', async (stats) => { + const compilationStats = stats.toJson({ + all: false, + assets: true, + chunks: true, + chunkRelations: true, + ids: true, + }); + const statsChunkMap = new Map( + compilationStats.chunks!.map((chunk) => [chunk.id!, chunk]) + ); + const entryChunkName = this.config.entryName ?? 'main'; + const localChunks: webpack.StatsChunk[] = []; + const remoteChunks: webpack.StatsChunk[] = []; + const sharedChunks = new Set(); + const auxiliaryAssets: Set = new Set(); + + const entryChunk = compilationStats.chunks!.find((chunk) => { + return chunk.initial && chunk.names?.includes(entryChunkName); + }); + + for (const chunk of compilationStats.chunks!) { + // Do not process shared chunks right now. + if (sharedChunks.has(chunk)) { + continue; + } - if (!entryChunk) { - throw new Error( - 'Cannot infer entry chunk - this should have not happened.' - ); - } + getAllInitialChunks(chunk, statsChunkMap) + .filter((sharedChunk) => sharedChunk !== chunk) + .forEach((sharedChunk) => { + sharedChunks.add(sharedChunk); + }); - // Collect auxiliary assets (only remote-assets for now) - Object.keys(compilation.assets) - .filter((filename) => /^remote-assets/.test(filename)) - .forEach((asset) => auxiliaryAssets.add(asset)); + // Entry chunk + if (entryChunk === chunk) { + localChunks.push(chunk); + } else if (isLocalChunk(chunk.name ?? chunk.id?.toString())) { + localChunks.push(chunk); + } else { + remoteChunks.push(chunk); } - ); - }); + } - compiler.hooks.afterEmit.tapPromise( - 'RepackOutputPlugin', - async (compilation) => { - const outputPath = compilation.outputOptions.path; - if (!outputPath) { - throw new Error('Cannot infer output path from compilation'); + // Process shared chunks to add them either as local or remote chunk. + for (const sharedChunk of sharedChunks) { + const isUsedByLocalChunk = localChunks.some((localChunk) => + getAllInitialChunks(localChunk, statsChunkMap).includes(sharedChunk) + ); + if ( + isUsedByLocalChunk || + isLocalChunk(sharedChunk.name ?? sharedChunk.id?.toString()) + ) { + localChunks.push(sharedChunk); + } else { + remoteChunks.push(sharedChunk); } + } - let localAssetsCopyProcessor; - - let { bundleFilename, sourceMapFilename, assetsPath } = - this.config.output; - if (bundleFilename) { - if (!path.isAbsolute(bundleFilename)) { - bundleFilename = path.join(this.config.context, bundleFilename); - } + if (!entryChunk) { + throw new Error( + 'Cannot infer entry chunk - this should have not happened.' + ); + } - const bundlePath = path.dirname(bundleFilename); + const assets = compilationStats.assets!; + // Collect auxiliary assets (only remote-assets for now) + assets + .filter((asset) => /^remote-assets/.test(asset.name)) + .forEach((asset) => auxiliaryAssets.add(asset.name)); - if (!sourceMapFilename) { - sourceMapFilename = `${bundleFilename}.map`; - } + let localAssetsCopyProcessor; - if (!path.isAbsolute(sourceMapFilename)) { - sourceMapFilename = path.join( - this.config.context, - sourceMapFilename - ); - } + let { bundleFilename, sourceMapFilename, assetsPath } = + this.config.output; - if (!assetsPath) { - assetsPath = bundlePath; - } + if (bundleFilename) { + if (!path.isAbsolute(bundleFilename)) { + bundleFilename = path.join(this.config.context, bundleFilename); + } - logger.debug('Detected output paths:', { - bundleFilename, - bundlePath, - sourceMapFilename, - assetsPath, - }); + const bundlePath = path.dirname(bundleFilename); - localAssetsCopyProcessor = new AssetsCopyProcessor({ - platform: this.config.platform, - compilation, - outputPath, - bundleOutput: bundleFilename, - bundleOutputDir: bundlePath, - sourcemapOutput: sourceMapFilename, - assetsDest: assetsPath, - logger, - }); + if (!sourceMapFilename) { + sourceMapFilename = `${bundleFilename}.map`; } - const remoteAssetsCopyProcessors: Record = - {}; - - for (const chunk of localChunks) { - // Process entry chunk - localAssetsCopyProcessor?.enqueueChunk(chunk, { - isEntry: entryChunk === chunk, - }); + if (!path.isAbsolute(sourceMapFilename)) { + sourceMapFilename = path.join(this.config.context, sourceMapFilename); } - for (const chunk of remoteChunks) { - const spec = extraAssets.find((spec) => - webpack.ModuleFilenameHelpers.matchObject( - { - test: spec.test, - include: spec.include, - exclude: spec.exclude, - }, - chunk.name || chunk.id?.toString() - ) - ); - - if (spec?.type === 'remote') { - if (!remoteAssetsCopyProcessors[spec.outputPath]) { - remoteAssetsCopyProcessors[spec.outputPath] = - new AssetsCopyProcessor({ - platform: this.config.platform, - compilation, - outputPath, - bundleOutput: '', - bundleOutputDir: spec.outputPath, - sourcemapOutput: '', - assetsDest: spec.outputPath, - logger, - }); - } - - remoteAssetsCopyProcessors[spec.outputPath].enqueueChunk(chunk, { - isEntry: false, - }); - } + if (!assetsPath) { + assetsPath = bundlePath; } - let auxiliaryAssetsCopyProcessor; - const { auxiliaryAssetsPath } = this.config.output; - if (auxiliaryAssetsPath) { - auxiliaryAssetsCopyProcessor = new AuxiliaryAssetsCopyProcessor({ - platform: this.config.platform, - outputPath, - assetsDest: auxiliaryAssetsPath, - logger, - }); + logger.debug('Detected output paths:', { + bundleFilename, + bundlePath, + sourceMapFilename, + assetsPath, + }); + + localAssetsCopyProcessor = new AssetsCopyProcessor({ + platform: this.config.platform, + outputPath, + bundleOutput: bundleFilename, + bundleOutputDir: bundlePath, + sourcemapOutput: sourceMapFilename, + assetsDest: assetsPath, + logger, + }); + } - for (const asset of auxiliaryAssets) { - auxiliaryAssetsCopyProcessor.enqueueAsset(asset); + const remoteAssetsCopyProcessors: Record = + {}; + + for (const chunk of localChunks) { + // Process entry chunk + localAssetsCopyProcessor?.enqueueChunk(chunk, { + isEntry: entryChunk === chunk, + sourceMapFile: getRelatedSourceMap(chunk), + }); + } + + for (const chunk of remoteChunks) { + const spec = extraAssets.find((spec) => + webpack.ModuleFilenameHelpers.matchObject( + { + test: spec.test, + include: spec.include, + exclude: spec.exclude, + }, + chunk.name || chunk.id?.toString() + ) + ); + + if (spec?.type === 'remote') { + if (!remoteAssetsCopyProcessors[spec.outputPath]) { + remoteAssetsCopyProcessors[spec.outputPath] = + new AssetsCopyProcessor({ + platform: this.config.platform, + outputPath, + bundleOutput: '', + bundleOutputDir: spec.outputPath, + sourcemapOutput: '', + assetsDest: spec.outputPath, + logger, + }); } + + remoteAssetsCopyProcessors[spec.outputPath].enqueueChunk(chunk, { + isEntry: false, + sourceMapFile: getRelatedSourceMap(chunk), + }); } + } - await Promise.all([ - ...(localAssetsCopyProcessor?.execute() ?? []), - ...Object.values(remoteAssetsCopyProcessors).reduce( - (acc, processor) => acc.concat(...processor.execute()), - [] as Promise[] - ), - ...(auxiliaryAssetsCopyProcessor?.execute() ?? []), - ]); + let auxiliaryAssetsCopyProcessor; + const { auxiliaryAssetsPath } = this.config.output; + if (auxiliaryAssetsPath) { + auxiliaryAssetsCopyProcessor = new AuxiliaryAssetsCopyProcessor({ + platform: this.config.platform, + outputPath, + assetsDest: auxiliaryAssetsPath, + logger, + }); + + for (const asset of auxiliaryAssets) { + auxiliaryAssetsCopyProcessor.enqueueAsset(asset); + } } - ); + + await Promise.all([ + ...(localAssetsCopyProcessor?.execute() ?? []), + ...Object.values(remoteAssetsCopyProcessors).reduce( + (acc, processor) => acc.concat(...processor.execute()), + [] as Promise[] + ), + ...(auxiliaryAssetsCopyProcessor?.execute() ?? []), + ]); + }); } } diff --git a/packages/repack/src/webpack/plugins/utils/AssetsCopyProcessor.ts b/packages/repack/src/webpack/plugins/utils/AssetsCopyProcessor.ts index 9091a5c86..25fe0e17d 100644 --- a/packages/repack/src/webpack/plugins/utils/AssetsCopyProcessor.ts +++ b/packages/repack/src/webpack/plugins/utils/AssetsCopyProcessor.ts @@ -9,7 +9,6 @@ export class AssetsCopyProcessor { constructor( public readonly config: { platform: string; - compilation: webpack.Compilation; outputPath: string; bundleOutput: string; bundleOutputDir: string; @@ -29,9 +28,11 @@ export class AssetsCopyProcessor { await this.filesystem.copyFile(from, to); } - enqueueChunk(chunk: webpack.Chunk, { isEntry }: { isEntry: boolean }) { + enqueueChunk( + chunk: webpack.StatsChunk, + { isEntry, sourceMapFile }: { isEntry: boolean; sourceMapFile?: string } + ) { const { - compilation, outputPath, bundleOutput, sourcemapOutput, @@ -44,22 +45,19 @@ export class AssetsCopyProcessor { : bundleOutputDir; // Chunk bundle e.g: `index.bundle`, `src_App_js.chunk.bundle` - const [chunkFile] = [...chunk.files]; + // There might be more than 1 file associated with the chunk - + // this happens e.g. on web when importing CSS files into JS. + // TBD whether this can ever occur in React Native. + const chunkFile = chunk.files?.[0]; // Sometimes there are no files associated with the chunk and the OutputPlugin fails // Skipping such chunks is a temporary workaround resulting in proper behaviour - // TODO: determine the real cause of this issue + // This can happen when Module Federation is used and some chunks are not emitted + // and are only used as temporary during compilation. if (!chunkFile) { return; } - const relatedSourceMap = - compilation.assetsInfo.get(chunkFile)?.related?.sourceMap; - // Source map for the chunk e.g: `index.bundle.map`, `src_App_js.chunk.bundle.map` - const sourceMapFile = Array.isArray(relatedSourceMap) - ? relatedSourceMap[0] - : relatedSourceMap; - // Target file path where to save the bundle. const bundleDestination = isEntry ? bundleOutput @@ -135,7 +133,7 @@ export class AssetsCopyProcessor { } // Copy regular assets - const mediaAssets = [...chunk.auxiliaryFiles] + const mediaAssets = [...chunk.auxiliaryFiles!] .filter((file) => !/\.(map|bundle\.json)$/.test(file)) .filter((file) => !/^remote-assets/.test(file)); @@ -150,7 +148,7 @@ export class AssetsCopyProcessor { ); // Manifest file name e.g: `index.bundle.json`, src_App_js.chunk.bundle.json` - const [manifest] = [...chunk.auxiliaryFiles].filter((file) => + const [manifest] = [...chunk.auxiliaryFiles!].filter((file) => /\.bundle\.json$/.test(file) ); if (manifest) { diff --git a/packages/repack/src/webpack/plugins/utils/__tests__/AssetsCopyProcessor.test.ts b/packages/repack/src/webpack/plugins/utils/__tests__/AssetsCopyProcessor.test.ts index 1b561cf50..b81aec5d8 100644 --- a/packages/repack/src/webpack/plugins/utils/__tests__/AssetsCopyProcessor.test.ts +++ b/packages/repack/src/webpack/plugins/utils/__tests__/AssetsCopyProcessor.test.ts @@ -9,26 +9,6 @@ describe('AssetsCopyProcessor', () => { describe('for ios', () => { const acpConfigStub = { platform: 'ios', - compilation: { - assetsInfo: new Map([ - [ - 'index.bundle', - { - related: { - sourceMap: 'index.bundle.map', - }, - }, - ], - [ - 'src_Async_js.chunk.bundle', - { - related: { - sourceMap: 'src_Async_js.chunk.bundle.map', - }, - }, - ], - ]), - } as unknown as webpack.Compilation, outputPath: '/dist', bundleOutput: '/target/ios/build/Release-iphonesimulator/main.jsbundle', bundleOutputDir: '/target/ios/build/Release-iphonesimulator', @@ -64,8 +44,8 @@ describe('AssetsCopyProcessor', () => { 'assets/node_modules/react-native/libraries/newappscreen/components/logo.png', 'index.bundle.map', ], - } as unknown as webpack.Chunk, - { isEntry: true } + } as unknown as webpack.StatsChunk, + { isEntry: true, sourceMapFile: 'index.bundle.map' } ); await Promise.all(acp.execute()); expect(1).toBe(1); @@ -110,8 +90,8 @@ describe('AssetsCopyProcessor', () => { 'src_Async_js.chunk.bundle.map', 'src_Async_js.chunk.bundle.json', ], - } as unknown as webpack.Chunk, - { isEntry: false } + } as unknown as webpack.StatsChunk, + { isEntry: false, sourceMapFile: 'src_Async_js.chunk.bundle.map' } ); await Promise.all(acp.execute()); @@ -136,26 +116,6 @@ describe('AssetsCopyProcessor', () => { describe('for android', () => { const acpConfigStub = { platform: 'android', - compilation: { - assetsInfo: new Map([ - [ - 'index.bundle', - { - related: { - sourceMap: 'index.bundle.map', - }, - }, - ], - [ - 'src_Async_js.chunk.bundle', - { - related: { - sourceMap: 'src_Async_js.chunk.bundle.map', - }, - }, - ], - ]), - } as unknown as webpack.Compilation, outputPath: '/dist', bundleOutput: '/target/generated/assets/react/release/index.android.bundle', @@ -190,8 +150,8 @@ describe('AssetsCopyProcessor', () => { 'drawable-mdpi/node_modules_reactnative_libraries_newappscreen_components_logo.png', 'index.bundle.map', ], - } as unknown as webpack.Chunk, - { isEntry: true } + } as unknown as webpack.StatsChunk, + { isEntry: true, sourceMapFile: 'index.bundle.map' } ); await Promise.all(acp.execute()); @@ -235,8 +195,8 @@ describe('AssetsCopyProcessor', () => { 'src_Async_js.chunk.bundle.map', 'src_Async_js.chunk.bundle.json', ], - } as unknown as webpack.Chunk, - { isEntry: false } + } as unknown as webpack.StatsChunk, + { isEntry: false, sourceMapFile: 'src_Async_js.chunk.bundle.map' } ); await Promise.all(acp.execute());