fix(rt): support host-FS CJS files in the standalone runtime (#34560)
## What
In a `deno compile`d binary, importing a named export from a CJS module
that lives **outside the embedded VFS** silently failed.
The user-visible symptom is a `SyntaxError: The requested module '…'
does not provide an export named 'X'` even though the module clearly
does export `X` — `@opentelemetry/api` is a real-world example, and
several Fresh apps hit it because `@fresh/core/.../otel.ts` does `import
{ SpanStatusCode } from "@opentelemetry/api"`. The same chain works
under `deno run` / `deno serve`; only the standalone runtime broke.
## Why it broke
The standalone runtime (`denort`) is built around the assumption that
everything live in the embedded VFS:
1. **CJS export analysis** is pre-computed at compile time and
serialized into the VFS entry. At runtime, the analyzer in
`cli/rt/node.rs::inner_cjs_analysis` reads that pre-computed list. When
the analysis is missing (file not in VFS), it dropped to *"assume ESM"*
— so any real CJS file read from the host filesystem looked exports-less
to the loader, and `import { X }` failed the static export check.
2. **The CJS require-loader** (`NodeRequireLoader::load_text_file_lossy`
in `cli/rt/run.rs`) only looked in the VFS. If a `require()` resolved to
a path outside the VFS, it returned `path not found` even when the file
existed on disk and permissions had already passed.
The non-VFS path matters because:
- `deno compile` can land here whenever a runtime-resolved import or
`require` reaches a path the compile-time graph walker couldn't
statically follow (dynamic `Function(…)` imports, eval, certain
`__dirname`-relative requires).
- The upcoming `deno desktop` subcommand (#33441) runs the user's
project from the host FS by design — every project file goes through
this fallback.
## What this PR does
Two small, surgical changes in `cli/rt/`:
- `node.rs::inner_cjs_analysis`: when the embedded analysis is absent
**and** the file isn't in the VFS, run `deno_ast`'s CJS analyzer
(`analyze_cjs`) live instead of guessing ESM. Mirrors what
`DenoCjsCodeAnalyzer` does at compile time. Enabled by adding the `cjs`
feature on `deno_ast` in `cli/rt/Cargo.toml`.
- `run.rs::NodeRequireLoader::load_text_file_lossy`: on `NotFound` from
the VFS, fall through to `RealSys.fs_read`. Mirrors the fallback
`StandaloneModules::read` already does for the ESM loader path.
Together these let `denort` load and correctly interop with CJS modules
sitting on the real filesystem.
## Test plan
- New spec test: `tests/specs/compile/fallback_cjs_define_property/` —
compiles a binary whose entry uses `new Function("p", "return
import(p)")(…)` so the compile-time graph walker can't reach the CJS
modules, then the runtime imports a name from a CJS file using the
TypeScript-emitted `Object.defineProperty(exports, "name", { get })`
pattern (this is the exact pattern `@opentelemetry/api` uses) with an
inner `require()` to a second CJS file. Expects `ok 42`. Without either
of the two fixes above it errors.
- Manual verification against the Fresh app that originally surfaced
this — `SpanStatusCode` now resolves. L
Leo Kettmeir committed
15a4c1aee0dbd048db5022772e29b083200587a4
Parent: 93d5bfa
Committed by GitHub <noreply@github.com>
on 6/2/2026, 4:22:23 AM