From 2b9e69a00ccb5538a9540e491fab6cb5d2097e02 Mon Sep 17 00:00:00 2001 From: Steven Nance Date: Fri, 7 Aug 2026 01:51:08 +0200 Subject: [PATCH 1/3] fix(core): warn loudly when falling back to the WebAssembly runtime When the platform native binary is missing from node_modules, the generated native-bindings.js silently falls back to the bundled WASM runtime. That fallback always succeeds, so nx keeps running but the workspace context and hasher can spin at 100% CPU for hours with no output, which reads as a hang rather than a broken install. The one existing WASM warning lives behind the daemon-enabled check, and in CI the isCI/isDocker branch above it wins first, so it never fires in the environment where this actually bites. Warn on stderr as soon as IS_WASM is observed, naming the missing platform package and the lockfile cause behind it. The warning uses a synchronous fd write because a queued async write cannot drain once the process blocks in synchronous WASM work, and dedupes through NX_WASM_FALLBACK_WARNED so the processes nx spawns do not each repeat it. NX_ALLOW_WASM_FALLBACK=true silences it, and genuine WASM-only platforms stay quiet. Also surface the runtime in nx report so the fallback is visible when diagnosing after the fact. --- packages/nx/project.json | 4 +- packages/nx/src/command-line/report/report.ts | 8 ++ packages/nx/src/native/index.js | 36 ++++++++ .../nx/src/native/wasm-fallback-warning.js | 60 ++++++++++++ .../src/native/wasm-fallback-warning.spec.ts | 91 +++++++++++++++++++ 5 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 packages/nx/src/native/wasm-fallback-warning.js create mode 100644 packages/nx/src/native/wasm-fallback-warning.spec.ts diff --git a/packages/nx/project.json b/packages/nx/project.json index 670a0379956..2a8616260b1 100644 --- a/packages/nx/project.json +++ b/packages/nx/project.json @@ -15,7 +15,7 @@ "inputs": ["native"], "outputs": [ "{projectRoot}/src/native/*.wasm", - "{projectRoot}/src/native/!(index|browser).js", + "{projectRoot}/src/native/!(index|browser|wasm-fallback-warning).js", "{projectRoot}/src/native/*.cjs", "{projectRoot}/src/native/*.mjs", "{projectRoot}/src/native/index.d.ts" @@ -32,7 +32,7 @@ "outputs": [ "{projectRoot}/src/native/*.node", "{projectRoot}/src/native/*.wasm", - "{projectRoot}/src/native/!(index|browser).js", + "{projectRoot}/src/native/!(index|browser|wasm-fallback-warning).js", "{projectRoot}/src/native/index.d.ts" ], "executor": "@monodon/rust:napi", diff --git a/packages/nx/src/command-line/report/report.ts b/packages/nx/src/command-line/report/report.ts index fffcc720374..ac37de722f2 100644 --- a/packages/nx/src/command-line/report/report.ts +++ b/packages/nx/src/command-line/report/report.ts @@ -86,6 +86,7 @@ export async function reportHandler() { mismatchedNxVersions, projectGraphError, nativeTarget, + nativeRuntime, cache, daemon, } = await getReportData(); @@ -94,6 +95,7 @@ export async function reportHandler() { ['Node', process.versions.node], ['OS', `${process.platform}-${process.arch}`], ['Native Target', nativeTarget ?? 'Unavailable'], + ['Native runtime', nativeRuntime ?? 'Unavailable'], [pm, pmVersion], [ 'daemon', @@ -309,6 +311,7 @@ export interface ReportData { }>; projectGraphError?: Error | null; nativeTarget: string | null; + nativeRuntime: string | null; cache: { max: number; used: number; @@ -443,6 +446,11 @@ export async function getReportData(): Promise { mismatchedNxVersions, projectGraphError, nativeTarget: native ? native.getBinaryTarget() : null, + nativeRuntime: native + ? native.IS_WASM + ? 'wasm (fallback)' + : 'native' + : null, cache, daemon: await getDaemonStatus(), }; diff --git a/packages/nx/src/native/index.js b/packages/nx/src/native/index.js index e549ed19cfe..229b11900a7 100644 --- a/packages/nx/src/native/index.js +++ b/packages/nx/src/native/index.js @@ -3,13 +3,16 @@ const { copyFileSync, existsSync, mkdirSync, + readFileSync, renameSync, statSync, unlinkSync, + writeSync, } = require('fs'); const Module = require('module'); const { nxVersion } = require('../utils/versions'); const { getNativeFileCacheLocation } = require('./native-file-cache-location'); +const { getWasmFallbackWarning } = require('./wasm-fallback-warning'); const MAX_COPY_RETRIES = 3; @@ -77,6 +80,19 @@ function isNoExecError(e) { return e.code === 'EACCES' || e.code === 'EPERM'; } +// Mirrors the musl detection in the generated native-bindings.js, which does not export it. +function isMusl() { + if (process.platform !== 'linux') { + return false; + } + try { + return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl'); + } catch (e) { + process.report.excludeNetwork = true; + return !process.report.getReport().header.glibcVersionRuntime; + } +} + // We override the _load function so that when a native file is required, // we copy it to a cache directory and require it from there. // This prevents the file being loaded from node_modules and causing file locking issues. @@ -169,5 +185,25 @@ const indexModulePath = require.resolve('./native-bindings.js'); delete require.cache[indexModulePath]; const indexModule = require('./native-bindings.js'); +if (indexModule.IS_WASM) { + try { + const warning = getWasmFallbackWarning({ + platform: process.platform, + arch: process.arch, + isMusl, + env: process.env, + }); + if (warning) { + // Sync write: once the synchronous WASM work blocks the event loop a queued async + // stderr write cannot drain, so console/logger output would be lost on a piped stderr. + writeSync(process.stderr.fd, warning); + // Propagates to the many processes nx spawns so they do not each repeat the warning. + process.env.NX_WASM_FALLBACK_WARNED = 'true'; + } + } catch (e) { + // a failed warning must never stop nx from loading + } +} + module.exports = indexModule; Module._load = originalLoad; diff --git a/packages/nx/src/native/wasm-fallback-warning.js b/packages/nx/src/native/wasm-fallback-warning.js new file mode 100644 index 00000000000..6062b76855c --- /dev/null +++ b/packages/nx/src/native/wasm-fallback-warning.js @@ -0,0 +1,60 @@ +// Keep in sync with the `nxPackages` set in ./index.js. A platform missing here is treated as +// WASM-only and stays silent, so an entry that drifts turns the warning off rather than breaking it. +const NATIVE_PACKAGES = { + 'android-arm': '@nx/nx-android-arm-eabi', + 'android-arm64': '@nx/nx-android-arm64', + 'darwin-arm64': '@nx/nx-darwin-arm64', + 'darwin-x64': '@nx/nx-darwin-x64', + 'freebsd-x64': '@nx/nx-freebsd-x64', + 'linux-arm': '@nx/nx-linux-arm-gnueabihf', + 'linux-arm64': '@nx/nx-linux-arm64-{libc}', + 'linux-x64': '@nx/nx-linux-x64-{libc}', + 'win32-arm64': '@nx/nx-win32-arm64-msvc', + 'win32-ia32': '@nx/nx-win32-ia32-msvc', + 'win32-x64': '@nx/nx-win32-x64-msvc', +}; + +function getWasmFallbackWarning({ platform, arch, isMusl, env = {} } = {}) { + if ( + env.NAPI_RS_FORCE_WASI || + env.NX_ALLOW_WASM_FALLBACK === 'true' || + env.NX_WASM_FALLBACK_WARNED === 'true' + ) { + return null; + } + + const template = NATIVE_PACKAGES[`${platform}-${arch}`]; + if (!template) { + return null; + } + + let libc; + try { + libc = isMusl() ? 'musl' : 'gnu'; + } catch (e) { + libc = 'gnu'; + } + const pkg = template.replace('{libc}', libc); + + return [ + '', + ' NX Nx could not load its native binary and fell back to the WebAssembly runtime.', + '', + ' The WebAssembly runtime is much slower than the native binary and can look like a hang', + ' on large workspaces.', + '', + ` This install is missing ${pkg}.`, + '', + ' The most common cause is a lockfile that lost its platform optionalDependencies, for', + ' example one regenerated on a different operating system (npm/cli#4828) or resolved', + ' after a merge conflict.', + '', + ` Recreate the lockfile so that ${pkg} is present, then reinstall.`, + '', + ' Set NX_ALLOW_WASM_FALLBACK=true to silence this warning.', + '', + '', + ].join('\n'); +} + +module.exports = { getWasmFallbackWarning }; diff --git a/packages/nx/src/native/wasm-fallback-warning.spec.ts b/packages/nx/src/native/wasm-fallback-warning.spec.ts new file mode 100644 index 00000000000..7617f8e4e1a --- /dev/null +++ b/packages/nx/src/native/wasm-fallback-warning.spec.ts @@ -0,0 +1,91 @@ +const { getWasmFallbackWarning } = require('./wasm-fallback-warning'); + +const gnu = () => false; +const musl = () => true; + +describe('getWasmFallbackWarning', () => { + it('names the gnu package on linux x64', () => { + const warning = getWasmFallbackWarning({ + platform: 'linux', + arch: 'x64', + isMusl: gnu, + env: {}, + }); + + expect(warning).toContain('@nx/nx-linux-x64-gnu'); + }); + + it('names the musl package on linux x64 with musl libc', () => { + const warning = getWasmFallbackWarning({ + platform: 'linux', + arch: 'x64', + isMusl: musl, + env: {}, + }); + + expect(warning).toContain('@nx/nx-linux-x64-musl'); + }); + + it('names the darwin arm64 package', () => { + const warning = getWasmFallbackWarning({ + platform: 'darwin', + arch: 'arm64', + isMusl: gnu, + env: {}, + }); + + expect(warning).toContain('@nx/nx-darwin-arm64'); + }); + + it('falls back to the gnu package when libc detection throws', () => { + const warning = getWasmFallbackWarning({ + platform: 'linux', + arch: 'x64', + isMusl: () => { + throw new Error('ldd is not available'); + }, + env: {}, + }); + + expect(warning).toContain('@nx/nx-linux-x64-gnu'); + }); + + it('stays quiet on platforms that have no prebuilt native package', () => { + expect( + getWasmFallbackWarning({ + platform: 'sunos', + arch: 'x64', + isMusl: gnu, + env: {}, + }) + ).toBeNull(); + }); + + it.each([ + ['NAPI_RS_FORCE_WASI', '1'], + ['NX_ALLOW_WASM_FALLBACK', 'true'], + ['NX_WASM_FALLBACK_WARNED', 'true'], + ])('stays quiet when %s is set', (name, value) => { + expect( + getWasmFallbackWarning({ + platform: 'linux', + arch: 'x64', + isMusl: gnu, + env: { [name]: value }, + }) + ).toBeNull(); + }); + + it('explains the cause and the escape hatch', () => { + const warning = getWasmFallbackWarning({ + platform: 'linux', + arch: 'x64', + isMusl: gnu, + env: {}, + }); + + expect(warning).toContain('WebAssembly'); + expect(warning).toContain('optionalDependencies'); + expect(warning).toContain('NX_ALLOW_WASM_FALLBACK=true'); + }); +}); From 9ba13cac6eeff3cd72f0e554406c465e2a536f75 Mon Sep 17 00:00:00 2001 From: "nx-cloud[bot]" <71083854+nx-cloud[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 01:25:18 +0000 Subject: [PATCH 2/3] docs(misc): document WebAssembly fallback env vars Co-authored-by: llwt --- .../src/content/docs/reference/environment-variables.mdoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/astro-docs/src/content/docs/reference/environment-variables.mdoc b/astro-docs/src/content/docs/reference/environment-variables.mdoc index 3cdd98d4af1..c756afdc9c8 100644 --- a/astro-docs/src/content/docs/reference/environment-variables.mdoc +++ b/astro-docs/src/content/docs/reference/environment-variables.mdoc @@ -168,6 +168,7 @@ The following environment variables are ones that you can set to change the beha | Property | Type | Description | | ----------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `NX_ALLOW_WASM_FALLBACK` | boolean | If set to `true`, silences the warning Nx prints when it falls back to the WebAssembly runtime because the native binary for the current platform is not installed. Useful when running on a platform without a prebuilt native package and WASM fallback is intentional. | | `NX_BAIL` | boolean | If set to `true`, Nx will stop command execution after the first failed task. Can be overridden on the command line with `--nxBail`. | | `NX_BASE` | string | The default base branch to use when calculating the affected projects. Can be overridden on the command line with `--base`. | | `NX_BATCH_MODE` | boolean | If set to `true`, Nx will run task(s) in batches for executors which support batches. | @@ -238,6 +239,7 @@ The following environment variables are ones that you can set to change the beha | `NX_TASKS_RUNNER` | string | The name of task runner from the config to use. Can be overridden on the command line with `--runner`. Preferred over `NX_RUNNER`. | | `NX_TASKS_RUNNER_DYNAMIC_OUTPUT` | boolean | If set to `false`, will use non-dynamic terminal output strategy (what you see in CI), even when you terminal can support the dynamic version | | `NX_USE_V8_SERIALIZER` | boolean | If set to `true`, Nx will use v8 serialization in the pseudo-IPC channel between the daemon and task processes instead of JSON. Improves throughput for workspaces with large task payloads. | +| `NX_WASM_FALLBACK_WARNED` | boolean | Internal. Propagated by Nx to child processes to suppress repeated WASM-fallback warnings within the same process tree. Do not set this manually; set `NX_ALLOW_WASM_FALLBACK=true` instead to silence the warning. | | `NX_WRAPPER_SKIP_INSTALL` | boolean | If set to `true`, the `.nx/nxw.js` wrapper skips verifying and self-installing the pinned Nx version before each command. | ## Plugin environment variables From 94ac920a5de3b9546a43efc076c5df5118c19778 Mon Sep 17 00:00:00 2001 From: Steven Nance Date: Fri, 7 Aug 2026 03:47:00 +0200 Subject: [PATCH 3/3] fix(core): only flag the wasm runtime as a fallback when the native package is unresolvable Gating the warning on platform and arch alone produces a false positive. WebContainers and StackBlitz report linux/x64 and install @nx/nx-linux-x64-gnu normally, but dlopen fails there, so nx correctly runs WASM. Every StackBlitz boot printed a broken lockfile diagnosis that did not apply, and nx report called the runtime a fallback for the same reason. Gate both surfaces on the signature of the install this actually targets: the expected native package cannot be resolved at all. A package that resolves but will not load is left alone, as is NAPI_RS_FORCE_WASI and any platform outside the prebuilt matrix. Only a MODULE_NOT_FOUND counts as absent, so any other resolution error is treated as present and stays quiet. The expected package name and the resolvability probe are shared between the startup warning and nx report so the two cannot disagree. The report field is now a neutral native or wasm, and only appends (missing ) under that same predicate. --- packages/nx/src/command-line/report/report.ts | 16 ++- packages/nx/src/native/index.js | 19 +-- .../src/native/native-package-resolution.ts | 46 +++++++ .../nx/src/native/wasm-fallback-warning.js | 39 ++++-- .../src/native/wasm-fallback-warning.spec.ts | 130 ++++++++++-------- 5 files changed, 163 insertions(+), 87 deletions(-) create mode 100644 packages/nx/src/native/native-package-resolution.ts diff --git a/packages/nx/src/command-line/report/report.ts b/packages/nx/src/command-line/report/report.ts index ac37de722f2..9888abb55e8 100644 --- a/packages/nx/src/command-line/report/report.ts +++ b/packages/nx/src/command-line/report/report.ts @@ -41,6 +41,7 @@ import { } from '../../tasks-runner/cache'; import { daemonClient } from '../../daemon/client/client'; import { readNxPackageGroup } from '../../utils/nx-package-group'; +import { getMissingNativePackage } from '../../native/native-package-resolution'; const nxPackageJson = readJsonFile( require.resolve('nx/package.json') @@ -446,11 +447,7 @@ export async function getReportData(): Promise { mismatchedNxVersions, projectGraphError, nativeTarget: native ? native.getBinaryTarget() : null, - nativeRuntime: native - ? native.IS_WASM - ? 'wasm (fallback)' - : 'native' - : null, + nativeRuntime: native ? getNativeRuntime(native.IS_WASM) : null, cache, daemon: await getDaemonStatus(), }; @@ -607,6 +604,15 @@ export function findInstalledPackagesWeCareAbout() { })); } +function getNativeRuntime(isWasm: boolean): string { + if (!isWasm) { + return 'native'; + } + // Only name a package when it is genuinely absent; WASM is expected where the binary cannot load. + const missing = getMissingNativePackage(); + return missing ? `wasm (missing ${missing})` : 'wasm'; +} + function isNativeAvailable(): typeof import('../../native') | false { try { return require('../../native'); diff --git a/packages/nx/src/native/index.js b/packages/nx/src/native/index.js index 229b11900a7..cfcee740451 100644 --- a/packages/nx/src/native/index.js +++ b/packages/nx/src/native/index.js @@ -3,7 +3,6 @@ const { copyFileSync, existsSync, mkdirSync, - readFileSync, renameSync, statSync, unlinkSync, @@ -13,6 +12,10 @@ const Module = require('module'); const { nxVersion } = require('../utils/versions'); const { getNativeFileCacheLocation } = require('./native-file-cache-location'); const { getWasmFallbackWarning } = require('./wasm-fallback-warning'); +const { + isMusl, + getMissingNativePackage, +} = require('./native-package-resolution'); const MAX_COPY_RETRIES = 3; @@ -80,19 +83,6 @@ function isNoExecError(e) { return e.code === 'EACCES' || e.code === 'EPERM'; } -// Mirrors the musl detection in the generated native-bindings.js, which does not export it. -function isMusl() { - if (process.platform !== 'linux') { - return false; - } - try { - return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl'); - } catch (e) { - process.report.excludeNetwork = true; - return !process.report.getReport().header.glibcVersionRuntime; - } -} - // We override the _load function so that when a native file is required, // we copy it to a cache directory and require it from there. // This prevents the file being loaded from node_modules and causing file locking issues. @@ -192,6 +182,7 @@ if (indexModule.IS_WASM) { arch: process.arch, isMusl, env: process.env, + nativePackageResolvable: getMissingNativePackage() === null, }); if (warning) { // Sync write: once the synchronous WASM work blocks the event loop a queued async diff --git a/packages/nx/src/native/native-package-resolution.ts b/packages/nx/src/native/native-package-resolution.ts new file mode 100644 index 00000000000..ae37ea01e0f --- /dev/null +++ b/packages/nx/src/native/native-package-resolution.ts @@ -0,0 +1,46 @@ +import { readFileSync } from 'fs'; + +// require rather than import: the helper is plain CommonJS and is not part of this project's ts file list. +const { getExpectedNativePackage } = require('./wasm-fallback-warning'); + +// Mirrors the musl detection in the generated native-bindings.js, which does not export it. +export function isMusl(): boolean { + if (process.platform !== 'linux') { + return false; + } + try { + return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl'); + } catch (e) { + (process.report as any).excludeNetwork = true; + return !(process.report.getReport() as any).header.glibcVersionRuntime; + } +} + +function canResolve(request: string): boolean { + try { + require.resolve(request); + return true; + } catch (e) { + // Anything other than a missing module means the binary is installed but will not load, which + // is normal in WebContainers. Treat it as present so only a genuinely absent package is flagged. + return e?.code !== 'MODULE_NOT_FOUND'; + } +} + +/** + * The native package this platform expects, but only when it cannot be resolved at all. + * Returns null when the package is installed, or when the platform ships no prebuilt binary. + */ +export function getMissingNativePackage(): string | null { + const expected = getExpectedNativePackage({ + platform: process.platform, + arch: process.arch, + isMusl, + }); + if (!expected) { + return null; + } + // native-bindings.js accepts either the npm package or a binary built into this directory. + const localBinary = `./nx.${expected.slice('@nx/nx-'.length)}.node`; + return canResolve(expected) || canResolve(localBinary) ? null : expected; +} diff --git a/packages/nx/src/native/wasm-fallback-warning.js b/packages/nx/src/native/wasm-fallback-warning.js index 6062b76855c..e3678f41910 100644 --- a/packages/nx/src/native/wasm-fallback-warning.js +++ b/packages/nx/src/native/wasm-fallback-warning.js @@ -14,7 +14,28 @@ const NATIVE_PACKAGES = { 'win32-x64': '@nx/nx-win32-x64-msvc', }; -function getWasmFallbackWarning({ platform, arch, isMusl, env = {} } = {}) { +function getExpectedNativePackage({ platform, arch, isMusl } = {}) { + const template = NATIVE_PACKAGES[`${platform}-${arch}`]; + if (!template) { + return null; + } + + let libc; + try { + libc = isMusl() ? 'musl' : 'gnu'; + } catch (e) { + libc = 'gnu'; + } + return template.replace('{libc}', libc); +} + +function getWasmFallbackWarning({ + platform, + arch, + isMusl, + env = {}, + nativePackageResolvable, +} = {}) { if ( env.NAPI_RS_FORCE_WASI || env.NX_ALLOW_WASM_FALLBACK === 'true' || @@ -23,18 +44,16 @@ function getWasmFallbackWarning({ platform, arch, isMusl, env = {} } = {}) { return null; } - const template = NATIVE_PACKAGES[`${platform}-${arch}`]; - if (!template) { + // The package being resolvable means it installed fine and failed to load, which is normal in + // WebContainers/StackBlitz. Only an unresolvable package is the broken install this warns about. + if (nativePackageResolvable === true) { return null; } - let libc; - try { - libc = isMusl() ? 'musl' : 'gnu'; - } catch (e) { - libc = 'gnu'; + const pkg = getExpectedNativePackage({ platform, arch, isMusl }); + if (!pkg) { + return null; } - const pkg = template.replace('{libc}', libc); return [ '', @@ -57,4 +76,4 @@ function getWasmFallbackWarning({ platform, arch, isMusl, env = {} } = {}) { ].join('\n'); } -module.exports = { getWasmFallbackWarning }; +module.exports = { getExpectedNativePackage, getWasmFallbackWarning }; diff --git a/packages/nx/src/native/wasm-fallback-warning.spec.ts b/packages/nx/src/native/wasm-fallback-warning.spec.ts index 7617f8e4e1a..5d0804aa6c1 100644 --- a/packages/nx/src/native/wasm-fallback-warning.spec.ts +++ b/packages/nx/src/native/wasm-fallback-warning.spec.ts @@ -1,63 +1,95 @@ -const { getWasmFallbackWarning } = require('./wasm-fallback-warning'); +const { + getExpectedNativePackage, + getWasmFallbackWarning, +} = require('./wasm-fallback-warning'); const gnu = () => false; const musl = () => true; -describe('getWasmFallbackWarning', () => { - it('names the gnu package on linux x64', () => { - const warning = getWasmFallbackWarning({ - platform: 'linux', - arch: 'x64', - isMusl: gnu, - env: {}, - }); +const brokenInstall = (overrides: Record = {}) => ({ + platform: 'linux', + arch: 'x64', + isMusl: gnu, + env: {}, + nativePackageResolvable: false, + ...overrides, +}); - expect(warning).toContain('@nx/nx-linux-x64-gnu'); +describe('getExpectedNativePackage', () => { + it('names the gnu package on linux x64', () => { + expect( + getExpectedNativePackage({ platform: 'linux', arch: 'x64', isMusl: gnu }) + ).toBe('@nx/nx-linux-x64-gnu'); }); it('names the musl package on linux x64 with musl libc', () => { - const warning = getWasmFallbackWarning({ - platform: 'linux', - arch: 'x64', - isMusl: musl, - env: {}, - }); - - expect(warning).toContain('@nx/nx-linux-x64-musl'); + expect( + getExpectedNativePackage({ platform: 'linux', arch: 'x64', isMusl: musl }) + ).toBe('@nx/nx-linux-x64-musl'); }); it('names the darwin arm64 package', () => { - const warning = getWasmFallbackWarning({ - platform: 'darwin', - arch: 'arm64', - isMusl: gnu, - env: {}, - }); + expect( + getExpectedNativePackage({ + platform: 'darwin', + arch: 'arm64', + isMusl: gnu, + }) + ).toBe('@nx/nx-darwin-arm64'); + }); - expect(warning).toContain('@nx/nx-darwin-arm64'); + it('ignores libc where only one variant is published', () => { + expect( + getExpectedNativePackage({ platform: 'linux', arch: 'arm', isMusl: musl }) + ).toBe('@nx/nx-linux-arm-gnueabihf'); }); it('falls back to the gnu package when libc detection throws', () => { - const warning = getWasmFallbackWarning({ - platform: 'linux', - arch: 'x64', - isMusl: () => { - throw new Error('ldd is not available'); - }, - env: {}, - }); + expect( + getExpectedNativePackage({ + platform: 'linux', + arch: 'x64', + isMusl: () => { + throw new Error('ldd is not available'); + }, + }) + ).toBe('@nx/nx-linux-x64-gnu'); + }); + + it('returns null on platforms that have no prebuilt native package', () => { + expect( + getExpectedNativePackage({ platform: 'sunos', arch: 'x64', isMusl: gnu }) + ).toBeNull(); + }); +}); + +describe('getWasmFallbackWarning', () => { + it('warns when the expected package cannot be resolved', () => { + const warning = getWasmFallbackWarning(brokenInstall()); expect(warning).toContain('@nx/nx-linux-x64-gnu'); + expect(warning).toContain('WebAssembly'); + expect(warning).toContain('optionalDependencies'); + expect(warning).toContain('NX_ALLOW_WASM_FALLBACK=true'); + }); + + it('names the musl package when the install is musl based', () => { + expect(getWasmFallbackWarning(brokenInstall({ isMusl: musl }))).toContain( + '@nx/nx-linux-x64-musl' + ); + }); + + // WebContainers/StackBlitz report linux/x64 and install @nx/nx-linux-x64-gnu normally, but + // dlopen fails, so nx legitimately runs WASM. That is not a broken install. + it('stays quiet when the expected package resolves but the binary will not load', () => { + expect( + getWasmFallbackWarning(brokenInstall({ nativePackageResolvable: true })) + ).toBeNull(); }); it('stays quiet on platforms that have no prebuilt native package', () => { expect( - getWasmFallbackWarning({ - platform: 'sunos', - arch: 'x64', - isMusl: gnu, - env: {}, - }) + getWasmFallbackWarning(brokenInstall({ platform: 'sunos' })) ).toBeNull(); }); @@ -67,25 +99,7 @@ describe('getWasmFallbackWarning', () => { ['NX_WASM_FALLBACK_WARNED', 'true'], ])('stays quiet when %s is set', (name, value) => { expect( - getWasmFallbackWarning({ - platform: 'linux', - arch: 'x64', - isMusl: gnu, - env: { [name]: value }, - }) + getWasmFallbackWarning(brokenInstall({ env: { [name]: value } })) ).toBeNull(); }); - - it('explains the cause and the escape hatch', () => { - const warning = getWasmFallbackWarning({ - platform: 'linux', - arch: 'x64', - isMusl: gnu, - env: {}, - }); - - expect(warning).toContain('WebAssembly'); - expect(warning).toContain('optionalDependencies'); - expect(warning).toContain('NX_ALLOW_WASM_FALLBACK=true'); - }); });