From 061f35f1e8556384b3cf4a59a7ae8cb5b7d7927b Mon Sep 17 00:00:00 2001 From: Benson Pan Date: Mon, 27 Apr 2026 13:19:00 -0700 Subject: [PATCH] fix: coerce chunk ids to string before comparison Webpack can assign numeric chunk ids, which causes strict equality checks to fail when compared against string values. Coerce all chunk id values with String() before comparison in ChunkExtractor and loadableReady. Co-Authored-By: Claude Opus 4.7 --- packages/component/src/loadableReady.js | 4 +- packages/component/src/loadableReady.test.js | 63 ++++++++++++++++++++ packages/server/src/ChunkExtractor.js | 20 ++++--- packages/server/src/ChunkExtractor.test.js | 23 +++++++ 4 files changed, 101 insertions(+), 9 deletions(-) create mode 100644 packages/component/src/loadableReady.test.js diff --git a/packages/component/src/loadableReady.js b/packages/component/src/loadableReady.js index 11e8dedf4..689ff37d7 100644 --- a/packages/component/src/loadableReady.js +++ b/packages/component/src/loadableReady.js @@ -56,7 +56,9 @@ export default function loadableReady( function checkReadyState() { if ( requiredChunks.every(chunk => - loadedChunks.some(([chunks]) => chunks.indexOf(chunk) > -1), + loadedChunks.some(([chunks]) => + chunks.some(c => String(c) === String(chunk)), + ), ) ) { if (!resolved) { diff --git a/packages/component/src/loadableReady.test.js b/packages/component/src/loadableReady.test.js new file mode 100644 index 000000000..7726ab8f8 --- /dev/null +++ b/packages/component/src/loadableReady.test.js @@ -0,0 +1,63 @@ +/* eslint-disable no-underscore-dangle */ +import 'regenerator-runtime/runtime' +import loadableReady from './loadableReady' +import { LOADABLE_SHARED } from './shared' + +beforeEach(() => { + document.body.innerHTML = '' + delete window.__LOADABLE_LOADED_CHUNKS__ + LOADABLE_SHARED.initialChunks = {} +}) + +function setupRequiredChunks(requiredChunks, namedChunks = []) { + const id = '__LOADABLE_REQUIRED_CHUNKS__' + const script = document.createElement('script') + script.id = id + script.type = 'application/json' + script.textContent = JSON.stringify(requiredChunks) + document.body.appendChild(script) + + const extScript = document.createElement('script') + extScript.id = `${id}_ext` + extScript.type = 'application/json' + extScript.textContent = JSON.stringify({ namedChunks }) + document.body.appendChild(extScript) +} + +describe('loadableReady', () => { + it('should resolve when all string chunks are loaded', async () => { + setupRequiredChunks(['letters-A']) + window.__LOADABLE_LOADED_CHUNKS__ = [[['letters-A'], []]] + + const done = jest.fn() + await loadableReady(done) + expect(done).toHaveBeenCalled() + }) + + it('should resolve when required chunks are numeric and loaded chunks are numeric', async () => { + setupRequiredChunks([1, 2]) + window.__LOADABLE_LOADED_CHUNKS__ = [[[1, 2], []]] + + const done = jest.fn() + await loadableReady(done) + expect(done).toHaveBeenCalled() + }) + + it('should resolve when required chunks are string but loaded chunks are numeric', async () => { + setupRequiredChunks(['1', '2']) + window.__LOADABLE_LOADED_CHUNKS__ = [[[1, 2], []]] + + const done = jest.fn() + await loadableReady(done) + expect(done).toHaveBeenCalled() + }) + + it('should resolve when required chunks are numeric but loaded chunks are string', async () => { + setupRequiredChunks([1, 2]) + window.__LOADABLE_LOADED_CHUNKS__ = [[['1', '2'], []]] + + const done = jest.fn() + await loadableReady(done) + expect(done).toHaveBeenCalled() + }) +}) diff --git a/packages/server/src/ChunkExtractor.js b/packages/server/src/ChunkExtractor.js index 80e95f2b6..3f4878691 100644 --- a/packages/server/src/ChunkExtractor.js +++ b/packages/server/src/ChunkExtractor.js @@ -225,7 +225,9 @@ class ChunkExtractor { } getChunkInfo(chunkId) { - const chunkInfo = this.stats.chunks.find(chunk => chunk.id === chunkId) + const chunkInfo = this.stats.chunks.find( + chunk => String(chunk.id) === String(chunkId), + ) invariant(chunkInfo, `cannot find chunk (chunkId: ${chunkId}) in stats`) return chunkInfo } @@ -300,15 +302,17 @@ class ChunkExtractor { const chunkGroup = this.getChunkGroup(chunk) // ignore chunk that only contains css files. - return chunkGroup.chunks.filter(chunkId => { - const chunkInfo = this.getChunkInfo(chunkId) + return chunkGroup.chunks + .map(chunkId => String(chunkId)) + .filter(chunkId => { + const chunkInfo = this.getChunkInfo(chunkId) - if (!chunkInfo) { - return false - } + if (!chunkInfo) { + return false + } - return checkIfChunkIncludesJs(chunkInfo) - }) + return checkIfChunkIncludesJs(chunkInfo) + }) } if (Array.isArray(chunks)) { diff --git a/packages/server/src/ChunkExtractor.test.js b/packages/server/src/ChunkExtractor.test.js index 2551235ca..ee1625c7e 100644 --- a/packages/server/src/ChunkExtractor.test.js +++ b/packages/server/src/ChunkExtractor.test.js @@ -929,4 +929,27 @@ describe('ChunkExtrator', () => { expect(x.hello).toBe('hello') }) }) + + describe('numeric chunk ids', () => { + const numericIdStats = { + ...stats, + chunks: [{ id: 0, files: ['main.css', 'main.js'] }], + } + + it('#getChunkInfo should find chunks with numeric ids', () => { + const ext = new ChunkExtractor({ + stats: numericIdStats, + outputPath: targetPath, + }) + expect(ext.getChunkInfo(0).id).toBe(0) + }) + + it('#getChunkInfo should find numeric chunk when given string id', () => { + const ext = new ChunkExtractor({ + stats: numericIdStats, + outputPath: targetPath, + }) + expect(ext.getChunkInfo('0').id).toBe(0) + }) + }) })