Skip to content

Commit cb6a828

Browse files
committed
chore(swc-wasm): Fix and clean up various issues with swc-wasm tests
1 parent d5c5cde commit cb6a828

File tree

9 files changed

+203
-164
lines changed

9 files changed

+203
-164
lines changed

.github/workflows/build_and_test.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -473,15 +473,12 @@ jobs:
473473
skipNativeInstall: 'yes'
474474
afterBuild: |
475475
rustup target add wasm32-unknown-unknown
476-
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
477476
node ./scripts/normalize-version-bump.js
478477
pnpm dlx turbo@${TURBO_VERSION} run build-wasm -- --target nodejs
479478
git checkout .
480-
mv crates/wasm/pkg crates/wasm/pkg-nodejs
481-
node ./scripts/setup-wasm.mjs
482479
483480
export NEXT_TEST_MODE=start
484-
export TEST_WASM=true
481+
export NEXT_TEST_WASM=true
485482
node run-tests.js \
486483
test/production/pages-dir/production/test/index.test.ts \
487484
test/e2e/streaming-ssr/index.test.ts

packages/next/errors.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -706,5 +706,6 @@
706706
"705": "Route is configured with dynamic = error that cannot be statically generated.",
707707
"706": "Invariant: static responses cannot be streamed %s",
708708
"707": "Invariant app-page handler received invalid cache entry %s",
709-
"708": "Failed to persist Chrome DevTools workspace UUID. The Chrome DevTools Workspace needs to be reconnected after the next page reload."
709+
"708": "Failed to persist Chrome DevTools workspace UUID. The Chrome DevTools Workspace needs to be reconnected after the next page reload.",
710+
"709": "cannot run loadNative when `NEXT_TEST_WASM` is set"
710711
}

packages/next/src/build/swc/index.ts

Lines changed: 167 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -159,15 +159,27 @@ let lastNativeBindingsLoadErrorCode:
159159
| 'unsupported_target'
160160
| string
161161
| undefined = undefined
162+
// Used to cache calls to `loadBindings`
163+
let pendingBindings: Promise<Binding>
164+
// some things call `loadNative` directly instead of `loadBindings`... Cache calls to that
165+
// separately.
162166
let nativeBindings: Binding
167+
// can allow hacky sync access to bindings for loadBindingsSync
163168
let wasmBindings: Binding
164169
let downloadWasmPromise: any
165-
let pendingBindings: any
166170
let swcTraceFlushGuard: any
167171
let downloadNativeBindingsPromise: Promise<void> | undefined = undefined
168172

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

175+
/**
176+
* Attempts to load a native or wasm binding.
177+
*
178+
* By default, this first tries to use a native binding, falling back to a wasm binding if that
179+
* fails.
180+
*
181+
* This function is `async` as wasm requires an asynchronous import in browsers.
182+
*/
171183
export async function loadBindings(
172184
useWasmBinary: boolean = false
173185
): Promise<Binding> {
@@ -180,6 +192,10 @@ export async function loadBindings(
180192
return pendingBindings
181193
}
182194

195+
if (process.env.NEXT_TEST_WASM) {
196+
useWasmBinary = true
197+
}
198+
183199
// 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
184200
// see https://github.com/napi-rs/napi-rs/issues/1630
185201
// and https://github.com/nodejs/node/blob/main/doc/api/process.md#a-note-on-process-io
@@ -291,7 +307,10 @@ async function tryLoadNativeWithFallback(attempts: Array<string>) {
291307
return undefined
292308
}
293309

294-
async function tryLoadWasmWithFallback(attempts: any[]) {
310+
// helper for loadBindings
311+
async function tryLoadWasmWithFallback(
312+
attempts: any[]
313+
): Promise<Binding | undefined> {
295314
try {
296315
let bindings = await loadWasm('')
297316
// @ts-expect-error TODO: this event has a wrong type.
@@ -343,8 +362,8 @@ function loadBindingsSync() {
343362
attempts = attempts.concat(a)
344363
}
345364

346-
// we can leverage the wasm bindings if they are already
347-
// loaded
365+
// HACK: we can leverage the wasm bindings if they are already loaded
366+
// this may introduce race conditions
348367
if (wasmBindings) {
349368
return wasmBindings
350369
}
@@ -1059,122 +1078,165 @@ function bindingToApi(
10591078
}
10601079

10611080
async function loadWasm(importPath = '') {
1062-
if (wasmBindings) {
1063-
return wasmBindings
1064-
}
1065-
10661081
let attempts = []
1067-
for (let pkg of ['@next/swc-wasm-nodejs', '@next/swc-wasm-web']) {
1068-
try {
1069-
let pkgPath = pkg
1082+
let rawBindings: RawWasmBindings | null = null
10701083

1071-
if (importPath) {
1072-
// the import path must be exact when not in node_modules
1073-
pkgPath = path.join(importPath, pkg, 'wasm.js')
1074-
}
1075-
let bindings: RawWasmBindings = await import(
1076-
pathToFileURL(pkgPath).toString()
1077-
)
1078-
if (pkg === '@next/swc-wasm-web') {
1079-
bindings = await bindings.default!()
1080-
}
1081-
infoLog('next-swc build: wasm build @next/swc-wasm-web')
1082-
1083-
// Note wasm binary does not support async intefaces yet, all async
1084-
// interface coereces to sync interfaces.
1085-
wasmBindings = {
1086-
css: {
1087-
lightning: {
1088-
transform: function (_options: any) {
1089-
throw new Error(
1090-
'`css.lightning.transform` is not supported by the wasm bindings.'
1091-
)
1092-
},
1093-
transformStyleAttr: function (_options: any) {
1094-
throw new Error(
1095-
'`css.lightning.transformStyleAttr` is not supported by the wasm bindings.'
1096-
)
1097-
},
1098-
},
1099-
},
1100-
isWasm: true,
1101-
transform(src: string, options: any) {
1102-
// TODO: we can remove fallback to sync interface once new stable version of next-swc gets published (current v12.2)
1103-
return bindings?.transform
1104-
? bindings.transform(src.toString(), options)
1105-
: Promise.resolve(bindings.transformSync(src.toString(), options))
1106-
},
1107-
transformSync(src: string, options: any) {
1108-
return bindings.transformSync(src.toString(), options)
1109-
},
1110-
minify(src: string, options: any) {
1111-
return bindings?.minify
1112-
? bindings.minify(src.toString(), options)
1113-
: Promise.resolve(bindings.minifySync(src.toString(), options))
1114-
},
1115-
minifySync(src: string, options: any) {
1116-
return bindings.minifySync(src.toString(), options)
1117-
},
1118-
parse(src: string, options: any) {
1119-
return bindings?.parse
1120-
? bindings.parse(src.toString(), options)
1121-
: Promise.resolve(bindings.parseSync(src.toString(), options))
1122-
},
1123-
getTargetTriple() {
1124-
return undefined
1125-
},
1126-
turbo: {
1127-
createProject: function (
1128-
_options: ProjectOptions,
1129-
_turboEngineOptions?: TurboEngineOptions | undefined
1130-
): Promise<Project> {
1131-
throw new Error(
1132-
'`turbo.createProject` is not supported by the wasm bindings.'
1133-
)
1134-
},
1135-
startTurbopackTraceServer: function (_traceFilePath: string): void {
1136-
throw new Error(
1137-
'`turbo.startTurbopackTraceServer` is not supported by the wasm bindings.'
1138-
)
1139-
},
1140-
},
1141-
mdx: {
1142-
compile(src: string, options: any) {
1143-
return bindings.mdxCompile(src, getMdxOptions(options))
1144-
},
1145-
compileSync(src: string, options: any) {
1146-
return bindings.mdxCompileSync(src, getMdxOptions(options))
1147-
},
1148-
},
1149-
reactCompiler: {
1150-
isReactCompilerRequired(_filename: string) {
1151-
return Promise.resolve(true)
1152-
},
1153-
},
1154-
}
1155-
return wasmBindings
1156-
} catch (e: any) {
1157-
// Only log attempts for loading wasm when loading as fallback
1158-
if (importPath) {
1159-
if (e?.code === 'ERR_MODULE_NOT_FOUND') {
1160-
attempts.push(`Attempted to load ${pkg}, but it was not installed`)
1084+
// Used by `run-tests` to force use of a locally-built wasm binary. This environment variable is
1085+
// unstable and subject to change.
1086+
const testWasmDir = process.env.NEXT_TEST_WASM_DIR
1087+
1088+
if (testWasmDir != null) {
1089+
// assume these are node.js bindings and don't need a call to `.default()`
1090+
rawBindings = await import(
1091+
pathToFileURL(path.join(testWasmDir, 'wasm.js')).toString()
1092+
)
1093+
infoLog(`next-swc build: wasm build ${testWasmDir}`)
1094+
} else {
1095+
for (let pkg of ['@next/swc-wasm-nodejs', '@next/swc-wasm-web']) {
1096+
try {
1097+
let pkgPath = pkg
1098+
1099+
if (importPath) {
1100+
// the import path must be exact when not in node_modules
1101+
pkgPath = path.join(importPath, pkg, 'wasm.js')
1102+
}
1103+
const importedRawBindings = await import(
1104+
pathToFileURL(pkgPath).toString()
1105+
)
1106+
if (pkg === '@next/swc-wasm-web') {
1107+
// https://rustwasm.github.io/docs/wasm-bindgen/examples/without-a-bundler.html
1108+
// `default` must be called to initialize the module
1109+
rawBindings = await importedRawBindings.default!()
11611110
} else {
1162-
attempts.push(
1163-
`Attempted to load ${pkg}, but an error occurred: ${e.message ?? e}`
1164-
)
1111+
rawBindings = importedRawBindings
1112+
}
1113+
infoLog(`next-swc build: wasm build ${pkg}`)
1114+
} catch (e: any) {
1115+
// Only log attempts for loading wasm when loading as fallback
1116+
if (importPath) {
1117+
if (e?.code === 'ERR_MODULE_NOT_FOUND') {
1118+
attempts.push(`Attempted to load ${pkg}, but it was not installed`)
1119+
} else {
1120+
attempts.push(
1121+
`Attempted to load ${pkg}, but an error occurred: ${e.message ?? e}`
1122+
)
1123+
}
11651124
}
11661125
}
11671126
}
11681127
}
11691128

1170-
throw attempts
1129+
if (rawBindings == null) {
1130+
throw attempts
1131+
}
1132+
1133+
function removeUndefined(obj: any): any {
1134+
// serde-wasm-bindgen expect that `undefined` values map to `()` in rust, but we want to treat
1135+
// those fields as non-existent, so remove them before passing them to rust.
1136+
//
1137+
// The native (non-wasm) bindings use `JSON.stringify`, which strips undefined values.
1138+
if (typeof obj !== 'object') {
1139+
return
1140+
}
1141+
if (Array.isArray(obj)) {
1142+
return obj.map(removeUndefined)
1143+
}
1144+
for (const [k, v] of Object.entries(obj)) {
1145+
if (typeof v === 'undefined') {
1146+
delete obj[k]
1147+
} else {
1148+
obj[k] = removeUndefined(v)
1149+
}
1150+
}
1151+
}
1152+
1153+
// Note wasm binary does not support async intefaces yet, all async
1154+
// interface coereces to sync interfaces.
1155+
wasmBindings = {
1156+
css: {
1157+
lightning: {
1158+
transform: function (_options: any) {
1159+
throw new Error(
1160+
'`css.lightning.transform` is not supported by the wasm bindings.'
1161+
)
1162+
},
1163+
transformStyleAttr: function (_options: any) {
1164+
throw new Error(
1165+
'`css.lightning.transformStyleAttr` is not supported by the wasm bindings.'
1166+
)
1167+
},
1168+
},
1169+
},
1170+
isWasm: true,
1171+
transform(src: string, options: any): Promise<any> {
1172+
return rawBindings.transform(src.toString(), removeUndefined(options))
1173+
},
1174+
transformSync(src: string, options: any) {
1175+
return rawBindings.transformSync(src.toString(), removeUndefined(options))
1176+
},
1177+
minify(src: string, options: any): Promise<any> {
1178+
return rawBindings.minify(src.toString(), removeUndefined(options))
1179+
},
1180+
minifySync(src: string, options: any) {
1181+
return rawBindings.minifySync(src.toString(), removeUndefined(options))
1182+
},
1183+
parse(src: string, options: any): Promise<any> {
1184+
return rawBindings.parse(src.toString(), removeUndefined(options))
1185+
},
1186+
getTargetTriple() {
1187+
return undefined
1188+
},
1189+
turbo: {
1190+
createProject: function (
1191+
_options: ProjectOptions,
1192+
_turboEngineOptions?: TurboEngineOptions | undefined
1193+
): Promise<Project> {
1194+
throw new Error(
1195+
'`turbo.createProject` is not supported by the wasm bindings.'
1196+
)
1197+
},
1198+
startTurbopackTraceServer: function (_traceFilePath: string): void {
1199+
throw new Error(
1200+
'`turbo.startTurbopackTraceServer` is not supported by the wasm bindings.'
1201+
)
1202+
},
1203+
},
1204+
mdx: {
1205+
compile(src: string, options: any) {
1206+
return rawBindings.mdxCompile(
1207+
src,
1208+
removeUndefined(getMdxOptions(options))
1209+
)
1210+
},
1211+
compileSync(src: string, options: any) {
1212+
return rawBindings.mdxCompileSync(
1213+
src,
1214+
removeUndefined(getMdxOptions(options))
1215+
)
1216+
},
1217+
},
1218+
reactCompiler: {
1219+
isReactCompilerRequired(_filename: string) {
1220+
return Promise.resolve(true)
1221+
},
1222+
},
1223+
}
1224+
return wasmBindings
11711225
}
11721226

1227+
/**
1228+
* Loads the native (non-wasm) bindings. Prefer `loadBindings` over this API, as that includes a
1229+
* wasm fallback.
1230+
*/
11731231
function loadNative(importPath?: string) {
11741232
if (nativeBindings) {
11751233
return nativeBindings
11761234
}
11771235

1236+
if (process.env.NEXT_TEST_WASM) {
1237+
throw new Error('cannot run loadNative when `NEXT_TEST_WASM` is set')
1238+
}
1239+
11781240
const customBindings: RawBindings = !!__INTERNAL_CUSTOM_TURBOPACK_BINDINGS
11791241
? require(__INTERNAL_CUSTOM_TURBOPACK_BINDINGS)
11801242
: null

scripts/setup-wasm.mjs

Lines changed: 0 additions & 32 deletions
This file was deleted.

0 commit comments

Comments
 (0)