diff --git a/.changepacks/changepack_fixWasmLiteResolution20260830.json b/.changepacks/changepack_fixWasmLiteResolution20260830.json new file mode 100644 index 00000000..e49247b7 --- /dev/null +++ b/.changepacks/changepack_fixWasmLiteResolution20260830.json @@ -0,0 +1,7 @@ +{ + "changes": { + "packages/next-plugin/package.json": "Patch" + }, + "note": "Fall back to the full WASM engine when an installed @devup-ui/wasm package does not expose the lite entry point", + "date": "2026-08-30T00:00:00.000Z" +} diff --git a/packages/next-plugin/src/__tests__/wasm.test.ts b/packages/next-plugin/src/__tests__/wasm.test.ts index 1f5d34ee..e323b41c 100644 --- a/packages/next-plugin/src/__tests__/wasm.test.ts +++ b/packages/next-plugin/src/__tests__/wasm.test.ts @@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from 'bun:test' import { loadWasm, loadWebpackPlugin, + requireWasm, setWasmForTesting, setWebpackPluginForTesting, } from '../wasm' @@ -26,6 +27,39 @@ describe('WASM selection', () => { expect(typeof loadWasm(true).codeExtract).toBe('function') }) + it.each(['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'])( + 'falls back to the full package when the lite export fails with %s', + (code) => { + const specifiers: string[] = [] + const loaded = requireWasm(true, (specifier) => { + specifiers.push(specifier) + if (specifier.endsWith('/lite')) { + const message = + code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' + ? "Package subpath './lite' is not defined by exports in node_modules/@devup-ui/wasm/package.json" + : "Cannot find module '@devup-ui/wasm/lite'" + throw Object.assign(new Error(message), { code }) + } + return wasm + }) + + expect(loaded).toBe(wasm) + expect(specifiers).toEqual(['@devup-ui/wasm/lite', '@devup-ui/wasm']) + }, + ) + + it('does not hide failures while loading an available lite export', () => { + const loadError = Object.assign(new Error('invalid WASM binary'), { + code: 'MODULE_NOT_FOUND', + }) + + expect(() => + requireWasm(true, () => { + throw loadError + }), + ).toThrow(loadError) + }) + it('loads or injects the Webpack plugin without a static dependency', () => { expect(typeof loadWebpackPlugin().DevupUIWebpackPlugin).toBe('function') setWebpackPluginForTesting(webpackPlugin) diff --git a/packages/next-plugin/src/wasm.ts b/packages/next-plugin/src/wasm.ts index c7d3e7cf..50bd97e1 100644 --- a/packages/next-plugin/src/wasm.ts +++ b/packages/next-plugin/src/wasm.ts @@ -28,13 +28,45 @@ function requireFromPlugin(specifier: string): T { return createRequire(requireBase)(specifier) as T } +type WasmLoader = (specifier: string) => DevupWasm + +function isMissingLiteWasm(error: unknown): boolean { + if (!(error instanceof Error) || !('code' in error)) return false + if (error.code === 'ERR_PACKAGE_PATH_NOT_EXPORTED') { + return ( + error.message.includes("Package subpath './lite'") && + error.message.includes('@devup-ui') && + error.message.includes('wasm') + ) + } + return ( + error.code === 'MODULE_NOT_FOUND' && + error.message.includes('@devup-ui/wasm/lite') + ) +} + +/** @internal Resolve the lite engine, falling back for older WASM packages. */ +export function requireWasm( + lite: boolean, + requireModule: WasmLoader = (specifier) => + requireFromPlugin(specifier), +): DevupWasm { + if (!lite) return requireModule('@devup-ui/wasm') + try { + return requireModule('@devup-ui/wasm/lite') + } catch (error) { + if (!isMissingLiteWasm(error)) throw error + return requireModule('@devup-ui/wasm') + } +} + /** Load exactly one extraction engine for the lifetime of a Next config. */ export function loadWasm(lite: boolean): DevupWasm { if (wasmForTesting) return wasmForTesting if (lite) { - return (liteWasm ??= requireFromPlugin('@devup-ui/wasm/lite')) + return (liteWasm ??= requireWasm(true)) } - return (fullWasm ??= requireFromPlugin('@devup-ui/wasm')) + return (fullWasm ??= requireWasm(false)) } /** Keep the Webpack adapter (and its full WASM) out of Turbopack startup. */