Repository navigation
fix(build): fail fast when an externalised optional native dep was silently dropped - #11863
Merged
diegosouzapw merged 6 commits intoAug 30, 2026
Conversation
npm 11 refuses to run install scripts for optional dependencies unless approved, drops the package, and still exits 0. better-sqlite3 is externalised via serverExternalPackages, and the bundler must resolve an external before it can externalise it, so the build then fails minutes later with a misleading "Module not found". This gate names the package and prints the fix in about a second.
Covers the three pure exports: array extraction (ordering, comment lines, single quotes, unterminated arrays, and the prose-mention regression), optional-only scoping with an injected resolver, and the report text. One case asserts against the real next.config.mjs so the gate cannot silently stop seeing better-sqlite3.
Source-level guard: isAndroid is declared exactly once and before the try, the catch still derives its reported timeout from it, and the manual-fix guidance stays reachable. Reads the source rather than importing it, because postinstall.mjs does its work at module scope and importing it would run the real postinstall as a test side effect.
The catch block reads isAndroid to report which timeout it used, but the constant was block-scoped to the try. On the rebuild-timeout path that threw ReferenceError: isAndroid is not defined, which escaped the catch, rejected the top-level await in this module, and failed the whole npm install — on the exact path that exists to print manual-fix hints for machines without a C++ toolchain.
Adds check:native-deps alongside the other check:* scripts, and a prebuild hook so npm runs it automatically ahead of build. It fails only where the build would have failed anyway, one second in instead of four minutes. Note for reviewers: prebuild fires for `npm run build` but not for build:secure or build:backend, which call build-next-isolated.mjs directly. OMNIROUTE_SKIP_NATIVE_DEP_CHECK=1 opts out.
One fragment per PR, filename prefixed with the PR number for deterministic aggregation, per changelog.d/README.md.
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The new gate’s success log message is currently misleading about what was actually validated, which can cause confusion during diagnosis.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR strengthens the build/install ergonomics around optional native dependencies (notably better-sqlite3) by adding a fast preflight check that fails early when npm silently drops an externalised optional dep, and by fixing a scope bug in postinstall that could turn a recoverable rebuild failure into a hard npm install failure.
Changes:
- Add
check:native-depsand wire it intoprebuildto fail fast when an externalised optionalDependency is missing. - Fix
scripts/build/postinstall.mjssoisAndroidis in-scope for the rebuild timeout reporting path. - Add unit tests covering the new preflight logic and the postinstall regression scenario.
File summaries
| File | Description |
|---|---|
scripts/check/check-native-deps.mjs |
New preflight gate that cross-checks next.config.mjs serverExternalPackages vs package.json optionalDependencies and reports missing optional externals. |
package.json |
Adds check:native-deps script and runs it via the prebuild hook. |
scripts/build/postinstall.mjs |
Moves isAndroid declaration outside the try so the catch can report timeouts without throwing ReferenceError. |
tests/unit/build/check-native-deps.test.ts |
New tests for parsing serverExternalPackages, missing-externals detection, and report formatting. |
tests/unit/build/postinstall-rebuild-timeout-scope.test.ts |
New source-level regression test ensuring isAndroid remains declared once and outside the rebuild try. |
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+209
to
+212
| console.log( | ||
| `[check-native-deps] OK — all ${externals.length} externalised packages resolve ` + | ||
| `(${optionalDeps.length} optional deps checked).` | ||
| ); |
diegosouzapw
merged commit Aug 30, 2026
3852e05
into
cheaper-inference:release/v3.8.51
3 checks passed
4 of 5 tasks
muhamadgalihsaputra
pushed a commit
to niyatna/NiyatnaRoute
that referenced
this pull request
Sep 27, 2026
…lently dropped (cheaper-inference#11863) Boarded with 8 other PRs in one combined worktree: typecheck:core, check:file-size, check:changelog-integrity, check:complexity, check:cognitive-complexity, check:cycles, check-native-deps all green; 75/75 focused tests pass. Fixes a genuinely confusing failure mode — matches the documented TROUBLESHOOTING.md symptom exactly. Thanks for the preflight check and clear diagnostics.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
npm run buildnow runs a one-second preflight (npm run check:native-deps) that names any package which is both listed inserverExternalPackagesand declared inoptionalDependenciesbut absent from the tree, and prints the exact install command to fix it.ReferenceErrorinscripts/build/postinstall.mjsthat turned a failedbetter-sqlite3rebuild into a failednpm install.The failure this replaces
npm 11 (bundled with Node 24+) will not run install scripts for optional dependencies unless they are approved.
better-sqlite3is anoptionalDependencywhose install script builds a native addon, so npm skips it, removes it from the tree, and exits0. The install output never says the package is gone.The build then fails roughly four minutes later:
Being listed in
serverExternalPackagesdoes not save it — the bundler has to resolve a request before it can externalise it, so an absent package is a hard build failure. The message reads like a code bug, and both import traces point at application source rather than at the install, which is what makes it expensive to diagnose. It is already documented atdocs/guides/TROUBLESHOOTING.md#npm-v11-better-sqlite3-not-installed-cannot-find-module, but only for the runtime symptom (Cannot find module), not the build one.The gate fails only where the build would have failed anyway. It just costs one second instead of four minutes, and it names the package and the fix.
The postinstall ReferenceError
fixBetterSqliteBinary()falls back tonpm rebuild better-sqlite3under a timeout. Its catch block reports which limit was used (600s on Termux/Android, 300s otherwise) by readingisAndroid— butisAndroidwas declared withconstinside thetry, so it was not in scope in thecatch.postinstall.mjsis a top-level-await module, so theReferenceErrorescaped the catch, rejected module evaluation, and failed the entirenpm install— on the exact path whose purpose is to print manual-fix instructions for machines without a C++ toolchain. Users got a stack trace instead of the guidance. The declaration is hoisted above thetry; nothing else changes.Related Issues
Validation
Focused loop:
node --import tsx/esm --test tests/unit/build/check-native-deps.test.ts node --import tsx/esm --test tests/unit/build/postinstall-rebuild-timeout-scope.test.ts npm run check:native-deps # green tree: exits 0 npm run lintManual reproduction of the gate:
The originating failure was reproduced on Windows 11 + Node 24 + npm 11, building v3.8.51 from source:
npm installexited 0 with nonode_modules/better-sqlite3, andnpm run buildthen failed with the twoModule not founderrors quoted above.Tests Added Or Updated
tests/unit/build/check-native-deps.test.ts— new. CoversextractServerExternalPackages(ordering, comment lines, single quotes, unterminated arrays, and the case where a prose mention ofserverExternalPackagesin a comment shadowed the real declaration),findMissingExternals(optional-only scoping, order preservation, injected resolver), andformatMissingReport(empty case, package names, runnable command, troubleshooting link). One test asserts against the realnext.config.mjsso the gate cannot silently stop seeingbetter-sqlite3.tests/unit/build/postinstall-rebuild-timeout-scope.test.ts— new. Source-level regression guard:isAndroidis declared exactly once and before thetry, the catch still derives the reported timeout from it, and the manual-fix guidance is still reachable. Reads the source rather than importing it, becausepostinstall.mjsdoes its work at module scope and importing it would run the real postinstall as a test side effect.Coverage Notes
scripts/is outside thesrc//open-sse//electron//bin/coverage scope. The three exported functions incheck-native-deps.mjsare pure and covered by the new unit test;main()is the thin I/O wrapper.Reviewer Notes
prebuildhook is the only behavioural risk. npm runs it fornpm run buildbut not forbuild:secureorbuild:backend, which invokebuild-next-isolated.mjsdirectly. If any pipeline builds with optional dependencies deliberately absent, this would now fail it — setOMNIROUTE_SKIP_NATIVE_DEP_CHECK=1there, or say so in review and I will move the call insidebuild-next-isolated.mjsbehind the same flag.extractServerExternalPackagesparsesnext.config.mjsas text rather than importing it. Importing would pull in the next-intl and fumadocs-mdx plugins at module scope — the very dependency tree this gate runs before validating. The parser anchors onserverExternalPackages\s*:\s*\[specifically because the file discusses the option in a comment ~130 lines above the real declaration.optionalDependencieson purpose. A missing required dependency meansnpm installnever ran, which is already loud everywhere else.changelog.d/fragment, since the convention needs the PR number in the filename. I will push it as a follow-up commit here as soon as this PR is numbered.release/v3.8.51. Happy to retarget if a different base is the current active release.