fix(commonjs): treat a require in a catch clause as guarded - #2018
Open
Stadly wants to merge 1 commit into
Open
Conversation
`ignoreTryCatch` defaults to `true` so that a `require` of a possibly-uninstalled external is left alone rather than hoisted into a top-level import. The guard window was computed from the try block alone (`node.block.end`), so a `require` in the `catch` clause fell outside it and was converted anyway. That turns a failure the author chose to handle into an unconditional one: the emitted `import` is evaluated before any of the surrounding code runs, so the module cannot be loaded at all even when the guarded branch would never execute. When the specifier is unresolvable the import also has no chance of ever succeeding, so the whole bundle becomes unloadable. Track the end of the whole `TryStatement` separately from the end of its block, and use that for the `ignoreTryCatch` decision. `currentTryBlockEnd` is deliberately left as it was, because it also feeds the `toBeRemoved` comparison against `currentConditionalNodeEnd`; widening it in place would have changed which nodes get removed.
This was referenced Aug 5, 2026
fix(adapter-node): fail the build on imports that resolve to no installed package
sveltejs/kit#16655
Closed
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.
Rollup Plugin Name:
commonjsThis PR contains:
Are tests included?
Breaking Changes?
List any relevant issue numbers:
Surfaced via sveltejs/kit#16653 (adapter-node produced an unloadable server bundle because of this).
Description
ignoreTryCatchdefaults totrueprecisely so that arequireof a possibly-uninstalled external is left alone instead of being hoisted into a top-levelimport. But the guard window is computed from the try block alone:A
requirein thecatchclause starts afternode.block.end, soisInsideTryBlockisfalsefor it and it is converted like any unguarded require.That inverts the intent of the option. The emitted
importis evaluated before any of the surrounding code runs, so a failure the author explicitly chose to handle becomes an unconditional one — and when the specifier is unresolvable, the module can never be loaded at all, even though the guarded branch may never execute.Real-world case
@babel/core's.ctsconfig loader has this shape:@babel/preset-typescriptis neither a dependency nor a peer of@babel/core— it is a soft lookup that is legitimately absent, which is why the author guarded it. Today it becomes:at the top of the output. Tree-shaking removes the surrounding Babel code but keeps the dangling side-effect import, and any bundle carrying it dies on evaluation with
ERR_MODULE_NOT_FOUND. This took down a production deployment; the build succeeded and the server exited at startup.Note the sibling
require('@babel/preset-typescript')a few lines away — in thetryblock — is already left alone today. Only thecatchone leaks, which is a fairly direct sign the window is the bug rather than the policy.What changed
Track the end of the whole
TryStatementalongside the end of its block, and use that for theignoreTryCatchdecision:currentTryBlockEndis deliberately not widened in place: it also feeds thetoBeRemovedcomparison againstcurrentConditionalNodeEnd, and changing it there would alter which nodes get removed. A separatecurrentTryStatementEndkeeps the two concerns independent.isInsideTryBlockhad no other reader, so it is replaced rather than duplicated.finallyclauses are covered by the same window, which seems consistent — arequirethere is equally not something the plugin should hoist out.Tests
test/fixtures/function/try-catch-handler, requiring an uninstalled external from inside acatchclause on a branch that never runs.mainwithCannot find module 'uninstalled-external-module'— the hoisted import breaking evaluation — and passes with this change.tryblocks or elsewhere.I also tried a broader variant — treating any unresolvable conditional require this way — and discarded it: it dropped
require('node:crypto')and broke 2 tests, because "unresolvable at build time" is not the same as "unavailable at runtime". The narrow change above is the one that holds up.Docs
Updated the
ignoreTryCatchsection to say the option covers the wholetry-catchstatement, including thecatchandfinallyclauses.