From 06eac146abb24c68b503d99a2b819cd6027995ee Mon Sep 17 00:00:00 2001 From: Menci Date: Sun, 8 Mar 2026 18:15:55 +0800 Subject: [PATCH] fix(wasm): reset assetUrlRE.lastIndex before .test() in SSR builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit assetUrlRE is a global regex (/g flag), making .test() stateful via lastIndex. Consecutive calls without resetting lastIndex cause alternating success/failure, so ~half of non-inlined wasm files miss the __VITE_ASSET__ -> __VITE_WASM_INIT__ replacement. Without that marker, renderChunk cannot transform the path to import.meta.url- relative, producing an absolute web path like /assets/foo.wasm that resolves to file:///assets/foo.wasm in Node.js SSR — ENOENT. --- .../src/node/__tests__/plugins/wasm.spec.ts | 42 +++++++++++++++++++ packages/vite/src/node/plugins/wasm.ts | 1 + 2 files changed, 43 insertions(+) create mode 100644 packages/vite/src/node/__tests__/plugins/wasm.spec.ts diff --git a/packages/vite/src/node/__tests__/plugins/wasm.spec.ts b/packages/vite/src/node/__tests__/plugins/wasm.spec.ts new file mode 100644 index 00000000000000..85e2a9760cb129 --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/wasm.spec.ts @@ -0,0 +1,42 @@ +import { describe, expect, test } from 'vitest' +import { assetUrlRE } from '../../plugins/asset' + +describe('wasm plugin assetUrlRE usage', () => { + // Regression test: assetUrlRE has the /g flag, which makes .test() + // stateful via lastIndex. When the wasm plugin called assetUrlRE.test() + // on consecutive asset URLs without resetting lastIndex, the second call + // would fail because lastIndex was already past the end of the string. + // This caused a non-deterministic bug where ~half of wasm files would + // miss the __VITE_ASSET__ -> __VITE_WASM_INIT__ replacement in SSR + // builds, leading to ENOENT errors at runtime. + test('assetUrlRE.test() fails on second call without lastIndex reset', () => { + const url1 = '__VITE_ASSET__abc123__' + const url2 = '__VITE_ASSET__def456__' + + // Simulate the bug: two consecutive .test() calls without resetting lastIndex + assetUrlRE.lastIndex = 0 + expect(assetUrlRE.test(url1)).toBe(true) + // After the first successful match, lastIndex is advanced past the string + expect(assetUrlRE.lastIndex).toBeGreaterThan(0) + // The second call fails because lastIndex > url2.length + expect(assetUrlRE.test(url2)).toBe(false) + + // Clean up + assetUrlRE.lastIndex = 0 + }) + + test('assetUrlRE.test() succeeds on consecutive calls with lastIndex reset', () => { + const url1 = '__VITE_ASSET__abc123__' + const url2 = '__VITE_ASSET__def456__' + + assetUrlRE.lastIndex = 0 + expect(assetUrlRE.test(url1)).toBe(true) + + // The fix: reset lastIndex before the next call + assetUrlRE.lastIndex = 0 + expect(assetUrlRE.test(url2)).toBe(true) + + // Clean up + assetUrlRE.lastIndex = 0 + }) +}) diff --git a/packages/vite/src/node/plugins/wasm.ts b/packages/vite/src/node/plugins/wasm.ts index a6361f655e3294..56199377036043 100644 --- a/packages/vite/src/node/plugins/wasm.ts +++ b/packages/vite/src/node/plugins/wasm.ts @@ -98,6 +98,7 @@ export default ${wasmHelperCode} id = id.split('?')[0] let url = await fileToUrl(this, id, ssr) + assetUrlRE.lastIndex = 0 if (ssr && assetUrlRE.test(url)) { url = url.replace('__VITE_ASSET__', '__VITE_WASM_INIT__') }