Skip to content

fix(build): auto-filter non-Linux platform-specific native binaries#1117

Merged
conico974 merged 3 commits intoopennextjs:mainfrom
clichedmoog:feat/filter-non-linux-platform-binaries
Mar 13, 2026
Merged

fix(build): auto-filter non-Linux platform-specific native binaries#1117
conico974 merged 3 commits intoopennextjs:mainfrom
clichedmoog:feat/filter-non-linux-platform-binaries

Conversation

@clichedmoog
Copy link
Copy Markdown
Contributor

@clichedmoog clichedmoog commented Feb 13, 2026

Motivation

When building on macOS, npm/pnpm installs platform-matching native binaries (e.g. @swc/core-darwin-arm64, @esbuild/darwin-arm64) via optionalDependencies. Next.js output file tracing picks these up because they exist on disk, causing two problems:

  1. esbuild build crash — esbuild has no loader for .node files and fails with "No loader is configured for .node files" (next-intl#2255)
  2. Bundle bloatcopyTracedFiles copies darwin/win32 binaries into the Lambda bundle where they can never work (~65MB in a production app)

The current user-side workaround is outputFileTracingExcludes in next.config.ts, but this requires manually listing every platform-specific package.

This mainly affects local macOS/Windows builds — CI environments on Linux wouldn't install these binaries in the first place.

Local development is not affected — these changes only run during the OpenNext build step. next dev, next build, and the local OpenNext server (openbuild:local:start) all work normally on macOS.

Changes

1. helper.ts — Prevent esbuild crash on .node files

Added loader: { ".node": "empty" } to both esbuildSync and esbuildAsync, so esbuild skips native binaries instead of crashing.

2. copyTracedFiles.ts — Filter non-Linux platform binaries

Added isNonLinuxPlatformPackage() that auto-detects and excludes darwin/win32/freebsd/android packages matching the {pkg}-{platform}-{arch} naming convention. Operates independently from existing EXCLUDED_PACKAGES.

Both linux-arm64 and linux-x64 packages are kept since the target architecture is unknown at build time.

This filter complements the existing install feature (InstallOptions with --os/--arch): install provides the correct platform binaries for runtime dependencies, while this filter removes the wrong ones that came from the host machine.

Related issues

Verification

Tested on a production Next.js app (next-intl + Prisma + esbuild) by completely removing outputFileTracingExcludes — both Next.js and OpenNext builds succeed, and no darwin binaries end up in the Lambda bundle.

…rom Lambda bundles

Native packages like @swc/core-darwin-arm64 (~22MB) are included by
Next.js output file tracing when building on macOS but are never needed
on Lambda (Linux). This adds automatic detection and exclusion of
darwin/win32/freebsd/android platform binaries during the traced files
copy step, reducing bundle size significantly.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 13, 2026

🦋 Changeset detected

Latest commit: 558568e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@opennextjs/aws Patch
app-pages-router Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vicb
Copy link
Copy Markdown
Contributor

vicb commented Feb 13, 2026

@clichedmoog Thanks for the PR.

Could you please expand about the rationale for this PR?

Would I still be able to dev on my local Mac Book if we merge this?

@clichedmoog
Copy link
Copy Markdown
Contributor Author

@vicb Thanks for the review!

Rationale:

When building on macOS, npm/pnpm installs platform-matching native binaries (e.g. @swc/core-darwin-arm64, @esbuild/darwin-arm64) via optionalDependencies. Next.js output file tracing picks these up because they exist on disk, and copyTracedFiles copies them into the Lambda bundle
— where they won't work due to the binary format mismatch with Linux.

I ran into this in a production project using next-intl (which depends on @swc/core). The workaround I ended up with was adding outputFileTracingExcludes to next.config.ts to manually list each platform-specific package. It works, but needs to be updated whenever a new native dependency is added.

In that project, I measured ~65MB of darwin-only binaries in a 209MB standalone output - @swc/core-darwin-arm64 (22MB), @esbuild/darwin-arm64(10MB), Prisma darwin engine (18MB), etc.

This PR tries to automate that filtering by matching the {pkg}-{platform}-{arch} naming convention that most native packages follow.

I should note that this mainly affects local macOS builds — CI environments on Linux wouldn't install darwin binaries in the first place (same reason Vercel doesn't hit this issue).

Local Mac development:

Yes — this filter only runs inside copyTracedFiles(), which is part of the OpenNext build step for creating the Lambda bundle. next dev and next build are not affected since they don't go through OpenNext.

I tested on macOS and confirmed pnpm dev works normally, including next-intl i18n which uses @swc/core at dev time.

@vicb
Copy link
Copy Markdown
Contributor

vicb commented Feb 13, 2026

Local Mac development:

Yes — this filter only runs inside copyTracedFiles(), which is part of the OpenNext build step for creating the Lambda bundle. next dev and next build are not affected since they don't go through OpenNext.

I tested on macOS and confirmed pnpm dev works normally, including next-intl i18n which uses @swc/core at dev time.

I was referring the launching a local Open Next server, i.e. what we do for local e2e tests.

Maybe you can try if this still works.

Edit: also I think we have a feature to install external packages and specify what arch to install - I don't have time to lookup for the details but you should be able to find something in the code or looking at the Open Next config.

Add empty loader for .node files in both esbuildSync and esbuildAsync,
preventing build crashes when packages like @swc/core include
platform-specific native binaries that esbuild cannot bundle.

Uses loader: { ".node": "empty" } approach which works for both sync
and async builds, replacing .node imports with empty modules.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@clichedmoog
Copy link
Copy Markdown
Contributor Author

@vicb Thanks for the clarification! I haven't tested the full local OpenNext e2e flow yet — I'll set up the examples and try openbuild:local + openbuild:local:start on macOS.

That said, I think it should be fine: the packages typically matched by this filter (SWC, esbuild, Rollup, etc.) are build-time native binaries that aren't needed by the server at runtime — Next.js pre-compiles everything in standalone mode. The .node empty loader similarly only affects the esbuild bundling step, not runtime loading.

Good point about the install feature — I found installDeps.ts with --os defaulting to "linux". For packages that genuinely need native binaries at runtime (like Prisma engines or sharp), users would use install to get the correct platform binaries. So the two features seem complementary: install provides the right binaries, and this filter removes the wrong ones that came from the host machine.

I'll follow up with local e2e test results.

@clichedmoog
Copy link
Copy Markdown
Contributor Author

Update: Tested the local OpenNext e2e flow on macOS (M1):

  1. pnpm build — all packages compiled
  2. pnpm -r openbuild:local — all 4 examples (app-router, pages-router, app-pages-router, experimental) built successfully with OpenNext
  3. openbuild:local:start — all servers started on ports 3001-3004
  4. Verified SSR pages serve correctly via curl on all 4 servers

Local OpenNext development on macOS is not affected by this change.

@vicb
Copy link
Copy Markdown
Contributor

vicb commented Feb 13, 2026

Local OpenNext development on macOS is not affected by this change.

Which as you touch base in your last message tend to confirm that they are not used.
Would make them external be a better way to solve your initial issue?
(Only asking the question here, I have not looked deeply into the issue)

Copy link
Copy Markdown
Contributor

@conico974 conico974 left a comment

Choose a reason for hiding this comment

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

I don't think the esbuild thing is doing anything, or if it is we have a bigger problem that we cannot just dismiss like that.

The esbuild issue is more likely coming from here

await build({
entryPoints: [configPath],
outfile: path.join(buildOptions.tempBuildDir, "next.config.mjs"),
bundle: true,
format: "esm",
platform: "node",
plugins: [inlineRequireResolvePlugin],
});
, we don't need to bundle anything here, just build the config file.

I'd prefer this esbuild fix to be in another PR as well, these are 2 different issue

);
}

const NON_LINUX_PLATFORMS = ["darwin", "win32", "freebsd", "android"];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pretty sure you can drop android 😄

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've dropped the android

Comment thread packages/open-next/src/build/helper.ts Outdated
sourcesContent: false,
...esbuildOptions,
// Native .node binaries cannot be bundled by esbuild
loader: { ".node": "empty", ...esbuildOptions.loader },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is supposed to be called only for OpenNext code, if not then we have a way bigger issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That makes sense — moved to #1120 so we can discuss it separately there.

}
// Skip non-Linux platform-specific native binaries (e.g. @swc/core-darwin-arm64)
if (isNonLinuxPlatformPackage(from)) {
const match = from.match(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is always a bit dangerous to skip things like that. Could you add a bypass for that (like an env variable to bypass this check)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good idea. Added OPEN_NEXT_SKIP_PLATFORM_FILTER env variable for that.

… split esbuild change

- Remove android from platform filter list
- Add OPEN_NEXT_SKIP_PLATFORM_FILTER env variable to bypass the filter
- Revert esbuild .node loader change (to be addressed in a separate PR)
Copy link
Copy Markdown
Contributor

@conico974 conico974 left a comment

Choose a reason for hiding this comment

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

LGTM, thanks for the PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Feb 21, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@opennextjs/aws@1117

commit: 558568e

@conico974 conico974 merged commit 3a2b1b9 into opennextjs:main Mar 13, 2026
3 checks passed
@github-actions github-actions Bot mentioned this pull request Mar 13, 2026
conico974 added a commit to opennextjs/adapters-api that referenced this pull request Mar 21, 2026
conico974 added a commit to opennextjs/adapters-api that referenced this pull request Mar 29, 2026
* Add SKILL.md for porting PRs and AGENTS.md for coding guidelines

* update skill

* Port opennextjs/opennextjs-aws#1118 as a test

* Port opennextjs/opennextjs-aws#1117

* update skill

* Port opennextjs/opennextjs-aws#1114

* Port PR opennextjs/opennextjs-aws#1107

* update skills

* Port PR opennextjs/opennextjs-aws#1108

* Port PR opennextjs/opennextjs-aws#1104

* Port PR opennextjs/opennextjs-aws#1101

* Port PR opennextjs/opennextjs-aws#1098

* review
conico974 added a commit to opennextjs/adapters-api that referenced this pull request Mar 29, 2026
* Add SKILL.md for porting PRs and AGENTS.md for coding guidelines

* update skill

* Port opennextjs/opennextjs-aws#1118 as a test

* Port opennextjs/opennextjs-aws#1117

* update skill

* Port opennextjs/opennextjs-aws#1114

* Port PR opennextjs/opennextjs-aws#1107

* update skills

* Port PR opennextjs/opennextjs-aws#1108

* Port PR opennextjs/opennextjs-aws#1104

* Port PR opennextjs/opennextjs-aws#1101

* Port PR opennextjs/opennextjs-aws#1098

* chore: port PR #1083 from source repository

opennextjs/opennextjs-cloudflare#1083

Changeset: .changeset/port-pr-1083.md

* chore: port PR #1105 from source repository

opennextjs/opennextjs-cloudflare#1105

Changeset: .changeset/port-pr-1105.md

* chore: port PR #1097 from source repository

opennextjs/opennextjs-cloudflare#1097

Changeset: .changeset/port-pr-1097.md

* chore: port PR #1122 from source repository

opennextjs/opennextjs-cloudflare#1122

Applied bugfixes and improvements to the 'migrate' command:
- Fixed extra newlines when appending to files (updated conditionalAppendFileSync signature)
- Fixed error when 'public' directory is missing (creates parent directories automatically)
- Fixed Next.js config file update to check if the file exists first
- Updated checkRunningInsideNextjsApp to accept { appPath: string } instead of full BuildOptions

Changesets:
- .changeset/port-pr-1122-cloudflare.md
- .changeset/port-pr-1122-aws.md

* chore: update port PR skill instructions for staging and committing changes

* chore: port PR #1126 from source repository

opennextjs/opennextjs-cloudflare#1126

Fix: prevent Worker hang on HEAD requests to static assets

When run_worker_first is enabled, HEAD requests to static assets hang
the Worker because response.body is null (per HTTP spec) and the
fallback new ReadableStream() creates a stream that never closes.

Changes:
- Return null body for HEAD requests instead of falling through to
the hanging ReadableStream fallback
- Add tests for maybeGetAssetResult covering GET, HEAD, 404, POST,
and run_worker_first=false cases

Changeset: .changeset/port-pr-1126.md

* linting

* chore: port PR #1127 from source repository

opennextjs/opennextjs-cloudflare#1127

Changeset: .changeset/port-pr-1127.md

* chore: port PR #1138 from source repository

opennextjs/opennextjs-cloudflare#1138

Changeset: .changeset/port-pr-1138.md

* chore: port PR #1133 from source repository

opennextjs/opennextjs-cloudflare#1133

Changeset: .changeset/port-pr-1133.md

Update the migrate command to attempt to create an R2 bucket for caching
as part of the migration process, if that is not possible an application
without caching enabled will be generated instead.

* chore: port PR #1142 from source repository

opennextjs/opennextjs-cloudflare#1142

Changeset: .changeset/port-pr-1142.md

* chore: port PR #1147 from source repository

opennextjs/opennextjs-cloudflare#1147

make dev /cdn-cgi/image behaves like prod for consistency

Changeset: .changeset/port-pr-1147.md

* chore: port PR #1150 from source repository

opennextjs/opennextjs-cloudflare#1150

Changeset: .changeset/port-pr-1150.md

* fix lockfile

* fix test
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants