Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changepacks/changepack_fixWasmLiteResolution20260830.json
Original file line number Diff line number Diff line change
@@ -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"
}
34 changes: 34 additions & 0 deletions packages/next-plugin/src/__tests__/wasm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from 'bun:test'
import {
loadWasm,
loadWebpackPlugin,
requireWasm,
setWasmForTesting,
setWebpackPluginForTesting,
} from '../wasm'
Expand All @@ -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)
Expand Down
36 changes: 34 additions & 2 deletions packages/next-plugin/src/wasm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,45 @@ function requireFromPlugin<T>(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<DevupWasm>(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<DevupWasm>('@devup-ui/wasm/lite'))
return (liteWasm ??= requireWasm(true))
}
return (fullWasm ??= requireFromPlugin<DevupWasm>('@devup-ui/wasm'))
return (fullWasm ??= requireWasm(false))
}

/** Keep the Webpack adapter (and its full WASM) out of Turbopack startup. */
Expand Down
Loading