Skip to content

chore(swc-wasm): Fix and clean up various issues with swc-wasm tests #80471

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: canary
Choose a base branch
from
Open
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
5 changes: 1 addition & 4 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -473,15 +473,12 @@ jobs:
skipNativeInstall: 'yes'
afterBuild: |
rustup target add wasm32-unknown-unknown
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
node ./scripts/normalize-version-bump.js
pnpm dlx turbo@${TURBO_VERSION} run build-wasm -- --target nodejs
git checkout .
mv crates/wasm/pkg crates/wasm/pkg-nodejs
node ./scripts/setup-wasm.mjs

export NEXT_TEST_MODE=start
export TEST_WASM=true
export NEXT_TEST_WASM=true
node run-tests.js \
test/production/pages-dir/production/test/index.test.ts \
test/e2e/streaming-ssr/index.test.ts
Expand Down
3 changes: 2 additions & 1 deletion packages/next/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -707,5 +707,6 @@
"706": "Invariant: static responses cannot be streamed %s",
"707": "Invariant app-page handler received invalid cache entry %s",
"708": "Failed to persist Chrome DevTools workspace UUID. The Chrome DevTools Workspace needs to be reconnected after the next page reload.",
"709": "`rspack.getModuleNamedExports` is not supported by the wasm bindings."
"709": "`rspack.getModuleNamedExports` is not supported by the wasm bindings.",
"710": "cannot run loadNative when `NEXT_TEST_WASM` is set"
}
289 changes: 175 additions & 114 deletions packages/next/src/build/swc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,15 +159,27 @@ let lastNativeBindingsLoadErrorCode:
| 'unsupported_target'
| string
| undefined = undefined
// Used to cache calls to `loadBindings`
let pendingBindings: Promise<Binding>
// some things call `loadNative` directly instead of `loadBindings`... Cache calls to that
// separately.
let nativeBindings: Binding
// can allow hacky sync access to bindings for loadBindingsSync
let wasmBindings: Binding
let downloadWasmPromise: any
let pendingBindings: any
let swcTraceFlushGuard: any
let downloadNativeBindingsPromise: Promise<void> | undefined = undefined

export const lockfilePatchPromise: { cur?: Promise<void> } = {}

/**
* Attempts to load a native or wasm binding.
*
* By default, this first tries to use a native binding, falling back to a wasm binding if that
* fails.
*
* This function is `async` as wasm requires an asynchronous import in browsers.
Copy link
Member

@lubieowoce lubieowoce Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the browser behavior relevant? this doesn't ever get used browser side

Suggested change
* This function is `async` as wasm requires an asynchronous import in browsers.
* This function is `async` as wasm requires an asynchronous import.

Copy link
Member Author

@bgw bgw Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We build and publish both "nodejs" and "web" targets: https://rustwasm.github.io/docs/wasm-pack/commands/build.html#target

The web target is needed for things like webcontainers (i.e. stackblitz).

Sidenote: this seems pretty annoying as AFAICT, the wasm binary blobs are basically the same and just the JS shim is different, so it doesn't make sense why these need to be entirely different packages

*/
export async function loadBindings(
useWasmBinary: boolean = false
): Promise<Binding> {
Expand All @@ -180,6 +192,10 @@ export async function loadBindings(
return pendingBindings
}

if (process.env.NEXT_TEST_WASM) {
useWasmBinary = true
}

// rust needs stdout to be blocking, otherwise it will throw an error (on macOS at least) when writing a lot of data (logs) to it
// see https://github.com/napi-rs/napi-rs/issues/1630
// and https://github.com/nodejs/node/blob/main/doc/api/process.md#a-note-on-process-io
Expand Down Expand Up @@ -291,7 +307,10 @@ async function tryLoadNativeWithFallback(attempts: Array<string>) {
return undefined
}

async function tryLoadWasmWithFallback(attempts: any[]) {
// helper for loadBindings
async function tryLoadWasmWithFallback(
attempts: any[]
): Promise<Binding | undefined> {
try {
let bindings = await loadWasm('')
// @ts-expect-error TODO: this event has a wrong type.
Expand Down Expand Up @@ -343,8 +362,8 @@ function loadBindingsSync() {
attempts = attempts.concat(a)
}

// we can leverage the wasm bindings if they are already
// loaded
// HACK: we can leverage the wasm bindings if they are already loaded
// this may introduce race conditions
if (wasmBindings) {
return wasmBindings
}
Expand Down Expand Up @@ -1058,132 +1077,174 @@ function bindingToApi(
}
}

// helper for tryLoadWasmWithFallback / loadBindings.
async function loadWasm(importPath = '') {
if (wasmBindings) {
return wasmBindings
}

let attempts = []
for (let pkg of ['@next/swc-wasm-nodejs', '@next/swc-wasm-web']) {
try {
let pkgPath = pkg
let rawBindings: RawWasmBindings | null = null

if (importPath) {
// the import path must be exact when not in node_modules
pkgPath = path.join(importPath, pkg, 'wasm.js')
}
let bindings: RawWasmBindings = await import(
pathToFileURL(pkgPath).toString()
)
if (pkg === '@next/swc-wasm-web') {
bindings = await bindings.default!()
}
infoLog('next-swc build: wasm build @next/swc-wasm-web')

// Note wasm binary does not support async intefaces yet, all async
// interface coereces to sync interfaces.
wasmBindings = {
css: {
lightning: {
transform: function (_options: any) {
throw new Error(
'`css.lightning.transform` is not supported by the wasm bindings.'
)
},
transformStyleAttr: function (_options: any) {
throw new Error(
'`css.lightning.transformStyleAttr` is not supported by the wasm bindings.'
)
},
},
},
isWasm: true,
transform(src: string, options: any) {
// TODO: we can remove fallback to sync interface once new stable version of next-swc gets published (current v12.2)
return bindings?.transform
? bindings.transform(src.toString(), options)
: Promise.resolve(bindings.transformSync(src.toString(), options))
},
transformSync(src: string, options: any) {
return bindings.transformSync(src.toString(), options)
},
minify(src: string, options: any) {
return bindings?.minify
? bindings.minify(src.toString(), options)
: Promise.resolve(bindings.minifySync(src.toString(), options))
},
minifySync(src: string, options: any) {
return bindings.minifySync(src.toString(), options)
},
parse(src: string, options: any) {
return bindings?.parse
? bindings.parse(src.toString(), options)
: Promise.resolve(bindings.parseSync(src.toString(), options))
},
getTargetTriple() {
return undefined
},
turbo: {
createProject: function (
_options: ProjectOptions,
_turboEngineOptions?: TurboEngineOptions | undefined
): Promise<Project> {
throw new Error(
'`turbo.createProject` is not supported by the wasm bindings.'
)
},
startTurbopackTraceServer: function (_traceFilePath: string): void {
throw new Error(
'`turbo.startTurbopackTraceServer` is not supported by the wasm bindings.'
)
},
},
mdx: {
compile(src: string, options: any) {
return bindings.mdxCompile(src, getMdxOptions(options))
},
compileSync(src: string, options: any) {
return bindings.mdxCompileSync(src, getMdxOptions(options))
},
},
reactCompiler: {
isReactCompilerRequired(_filename: string) {
return Promise.resolve(true)
},
},
rspack: {
getModuleNamedExports: function (
_resourcePath: string
): Promise<string[]> {
throw new Error(
'`rspack.getModuleNamedExports` is not supported by the wasm bindings.'
)
},
},
}
return wasmBindings
} catch (e: any) {
// Only log attempts for loading wasm when loading as fallback
if (importPath) {
if (e?.code === 'ERR_MODULE_NOT_FOUND') {
attempts.push(`Attempted to load ${pkg}, but it was not installed`)
// Used by `run-tests` to force use of a locally-built wasm binary. This environment variable is
// unstable and subject to change.
const testWasmDir = process.env.NEXT_TEST_WASM_DIR

if (testWasmDir) {
// assume these are node.js bindings and don't need a call to `.default()`
rawBindings = await import(
pathToFileURL(path.join(testWasmDir, 'wasm.js')).toString()
)
infoLog(`next-swc build: wasm build ${testWasmDir}`)
} else {
for (let pkg of ['@next/swc-wasm-nodejs', '@next/swc-wasm-web']) {
try {
let pkgPath = pkg

if (importPath) {
// the import path must be exact when not in node_modules
pkgPath = path.join(importPath, pkg, 'wasm.js')
}
const importedRawBindings = await import(
pathToFileURL(pkgPath).toString()
)
if (pkg === '@next/swc-wasm-web') {
// https://rustwasm.github.io/docs/wasm-bindgen/examples/without-a-bundler.html
// `default` must be called to initialize the module
rawBindings = await importedRawBindings.default!()
} else {
attempts.push(
`Attempted to load ${pkg}, but an error occurred: ${e.message ?? e}`
)
rawBindings = importedRawBindings
}
infoLog(`next-swc build: wasm build ${pkg}`)
} catch (e: any) {
// Only log attempts for loading wasm when loading as fallback
if (importPath) {
if (e?.code === 'ERR_MODULE_NOT_FOUND') {
attempts.push(`Attempted to load ${pkg}, but it was not installed`)
} else {
attempts.push(
`Attempted to load ${pkg}, but an error occurred: ${e.message ?? e}`
)
}
}
}
}
}

throw attempts
if (rawBindings == null) {
throw attempts
}

function removeUndefined(obj: any): any {
// serde-wasm-bindgen expect that `undefined` values map to `()` in rust, but we want to treat
// those fields as non-existent, so remove them before passing them to rust.
//
// The native (non-wasm) bindings use `JSON.stringify`, which strips undefined values.
if (typeof obj !== 'object' || obj === null) {
return obj
}
if (Array.isArray(obj)) {
return obj.map(removeUndefined)
}
const newObj: { [key: string]: any } = {}
for (const [k, v] of Object.entries(obj)) {
if (typeof v !== 'undefined') {
newObj[k] = removeUndefined(v)
}
}
return newObj
}

// Note wasm binary does not support async intefaces yet, all async
// interface coereces to sync interfaces.
wasmBindings = {
css: {
lightning: {
transform: function (_options: any) {
throw new Error(
'`css.lightning.transform` is not supported by the wasm bindings.'
)
},
transformStyleAttr: function (_options: any) {
throw new Error(
'`css.lightning.transformStyleAttr` is not supported by the wasm bindings.'
)
},
},
},
isWasm: true,
transform(src: string, options: any): Promise<any> {
return rawBindings.transform(src.toString(), removeUndefined(options))
},
transformSync(src: string, options: any) {
return rawBindings.transformSync(src.toString(), removeUndefined(options))
},
minify(src: string, options: any): Promise<any> {
return rawBindings.minify(src.toString(), removeUndefined(options))
},
minifySync(src: string, options: any) {
return rawBindings.minifySync(src.toString(), removeUndefined(options))
},
parse(src: string, options: any): Promise<any> {
return rawBindings.parse(src.toString(), removeUndefined(options))
},
getTargetTriple() {
return undefined
},
turbo: {
createProject(
_options: ProjectOptions,
_turboEngineOptions?: TurboEngineOptions | undefined
): Promise<Project> {
throw new Error(
'`turbo.createProject` is not supported by the wasm bindings.'
)
},
startTurbopackTraceServer(_traceFilePath: string): void {
throw new Error(
'`turbo.startTurbopackTraceServer` is not supported by the wasm bindings.'
)
},
},
mdx: {
compile(src: string, options: any) {
return rawBindings.mdxCompile(
src,
removeUndefined(getMdxOptions(options))
)
},
compileSync(src: string, options: any) {
return rawBindings.mdxCompileSync(
src,
removeUndefined(getMdxOptions(options))
)
},
},
reactCompiler: {
isReactCompilerRequired(_filename: string) {
return Promise.resolve(true)
},
},
rspack: {
getModuleNamedExports(_resourcePath: string): Promise<string[]> {
throw new Error(
'`rspack.getModuleNamedExports` is not supported by the wasm bindings.'
)
},
},
}
return wasmBindings
}

/**
* Loads the native (non-wasm) bindings. Prefer `loadBindings` over this API, as that includes a
* wasm fallback.
*/
function loadNative(importPath?: string) {
if (nativeBindings) {
return nativeBindings
}

if (process.env.NEXT_TEST_WASM) {
throw new Error('cannot run loadNative when `NEXT_TEST_WASM` is set')
}

const customBindings: RawBindings = !!__INTERNAL_CUSTOM_TURBOPACK_BINDINGS
? require(__INTERNAL_CUSTOM_TURBOPACK_BINDINGS)
: null
Expand Down
Loading
Loading