Skip to content

fix stack trace filename format for transpiled files#398

Open
kirkwaiblinger wants to merge 14 commits into
unjs:mainfrom
kirkwaiblinger:fix-ts-stack-traces
Open

fix stack trace filename format for transpiled files#398
kirkwaiblinger wants to merge 14 commits into
unjs:mainfrom
kirkwaiblinger:fix-ts-stack-traces

Conversation

@kirkwaiblinger
Copy link
Copy Markdown

@kirkwaiblinger kirkwaiblinger commented Sep 1, 2025

resolves #397

Summary by CodeRabbit

  • New Features

    • Improved module-type detection and transpilation behavior: treat .mts as ESM, include .cts as CommonJS, always transpile TypeScript, and provide ESM runs with file-URL paths for accurate runtime reporting.
  • Tests

    • Added end-to-end fixtures to validate filename/URL and stack-trace resolution across CJS, CTS, ESM, and MTS scenarios.

@kirkwaiblinger kirkwaiblinger marked this pull request as ready for review September 1, 2025 03:12
@pi0
Copy link
Copy Markdown
Member

pi0 commented Oct 1, 2025

Hi dear @kirkwaiblinger

Do you think you can minimize changes (changes to isESM seem not necessary), also making it an opt-in behavior? (Using slashes is not a bug and is supported in modern Windows APIs, but I understand it might be a wanted behavior to use backslash)

Also, I noticed typescript-eslint/typescript-eslint#11546 is already merged, fixing the original typescript-eslint issue. Do you think it is still worth upstreaming it?

@kirkwaiblinger
Copy link
Copy Markdown
Author

kirkwaiblinger commented Oct 1, 2025

Hi dear @kirkwaiblinger

👋

Do you think you can minimize changes (changes to isESM seem not necessary)..

It is, actually! This is what decides between a file:// URL in the stack trace (for ESM) and an ordinary path (for CJS) - which applies regardless of whether the file is TS or JS. This reminds me, though, I had meant to create test cases to illustrate this, but I forgot. I'll get on that.

...also making it an opt-in behavior? (Using slashes is not a bug and is supported in modern Windows APIs, but I understand it might be a wanted behavior to use backslash)

What would be the value in making this configurable? The most (and only?) important consideration I could think of in deciding how to present the filename would be alignment with node, which uses backslashes on CJS paths (and in particular, a path such that path.resolve(filename) === filename regardless of the platform), and file:// URLS for ESM locations, which is more than just a forwardslash-vs-backslash issue.

Also, I noticed typescript-eslint/typescript-eslint#11546 is already merged, fixing the original typescript-eslint issue. Do you think it is still worth upstreaming it?

Yes.

@kirkwaiblinger kirkwaiblinger changed the title fix windows stack traces for transpiled files fix stack trace filename format for transpiled files Oct 1, 2025
@kirkwaiblinger
Copy link
Copy Markdown
Author

@pi0 I've updated the tests. If you try out various combinations of reverting the changes to eval.ts and running the tests with windows/non-windows, it should hopefully demonstrate pretty clearly the things I'm trying to point out! 🙂

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 418d076c-dfbf-4c85-b96f-4e36211203d9

📥 Commits

Reviewing files that changed from the base of the PR and between 61ce691 and 486a8bc.

📒 Files selected for processing (2)
  • src/eval.ts
  • test/fixtures/filename/get-stack-trace.cjs
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/fixtures/filename/get-stack-trace.cjs
  • src/eval.ts

📝 Walkthrough

Walkthrough

Updated module-type detection and transpile decision logic; resolve module filenames to absolute paths and use file:// URLs for ESM when invoking vm.runInThisContext; set Module.filename to the resolved path and compute __dirname via nodeDirname; added fixtures and a helper to validate stack-trace path formats across CJS/CTS/ESM/MTS modules.

Changes

Module type detection, filename formatting, transpile gating, and test fixtures

Layer / File(s) Summary
Data / Detection
src/eval.ts
isESM now includes .mts and treats .js/.ts as ESM when nearest package.json has "type":"module". isCommonJS now includes .cts.
Transpile Decision
src/eval.ts
Refactored needsTranspile: always transpile TypeScript, skip for CommonJS, preserve native-ESM async skip, otherwise gate on updated isESM plus transform-regex / hasESMSyntax.
Path Resolution / VM Filename
src/eval.ts
Resolve filename via node:path.resolve; when ESM use pathToFileURL(resolved) for the vm.runInThisContext filename option, otherwise pass resolved path. Set created Module instance filename to the resolved absolute path.
__dirname Calculation
src/eval.ts
When invoking compiled wrapper, pass nodeDirname(mod.filename) instead of dirname(mod.filename) for __dirname.
Test Fixtures — helper
test/fixtures/filename/get-stack-trace.cjs
New CommonJS helper that captures the top stack frame by temporarily overriding Error.stackTraceLimit and Error.prepareStackTrace; exports getStackTrace.
Test Fixtures — modules
test/fixtures/filename/cjs-module.cjs, test/fixtures/filename/cts-module.cts, test/fixtures/filename/esm-module.mjs, test/fixtures/filename/mts-module.mts
Added fixtures exposing __filename/__dirname (CJS/CTS) or import.meta.url (ESM/MTS) and stackTop via the helper to validate path formats.
Test Fixtures — runner
test/fixtures/filename/index.ts
Added runner that loads each fixture (createRequire/dynamic import), compares module-provided paths/URLs with stack-derived top-frame paths, and logs resolution/format results.

Sequence Diagram(s)

sequenceDiagram
    participant Loader
    participant FS as Filesystem
    participant Analyzer as TypeDetector
    participant Formatter as PathFormatter
    participant VM
    participant ModuleWrapper

    Loader->>FS: resolve requested filename (path.resolve)
    Loader->>Analyzer: determine isESM / isCommonJS (ext, nearest package.json)
    Analyzer-->>Loader: module type
    Loader->>Formatter: format filename (file:// URL if ESM, else resolved path)
    Loader->>VM: vm.runInThisContext(compiledCode, { filename: formattedFilename })
    VM->>ModuleWrapper: instantiate Module with filename = resolvedPath
    Loader->>ModuleWrapper: invoke wrapper(..., __dirname = nodeDirname(mod.filename), ...)
    ModuleWrapper->>ModuleWrapper: execute module code (CJS or ESM evaluation path)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • pi0

Poem

🐰 I hopped through paths both near and far,
I turned resolved paths into file:// stars,
I peeked the stack and fetched its top,
CJS stays plain while ESM gets its prop. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix stack trace filename format for transpiled files' directly describes the main change: correcting how filenames appear in stack traces for TypeScript files.
Linked Issues check ✅ Passed The PR addresses issue #397 by implementing both ESM file:// URL formatting and CJS backslash path normalization; changes to isESM enable ESM URL format detection, and path resolution ensures CJS backslashes on Windows.
Out of Scope Changes check ✅ Passed All changes align with issue #397 scope: eval.ts modifications fix filename formatting for transpiled files, and new test fixtures validate both CJS and ESM stack-trace filename formats.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/fixtures/filename/get-stack-trace.cjs`:
- Around line 3-10: Wrap the temporary modifications of Error.stackTraceLimit
and Error.prepareStackTrace and the call to Error.captureStackTrace(dummyObj,
getTopOfStackTrace) in a try/finally block so that Error.prepareStackTrace and
Error.stackTraceLimit are always restored even if an exception is thrown;
specifically save oldStackTraceLimit and oldPrepareStackTrace before changing
them, perform the capture in the try, and restore oldPrepareStackTrace and
oldStackTraceLimit in the finally block to guarantee global state is reset.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a3e705d and 6dbb5f6.

⛔ Files ignored due to path filters (1)
  • test/__snapshots__/fixtures.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (7)
  • src/eval.ts
  • test/fixtures/filename/cjs-module.cjs
  • test/fixtures/filename/cts-module.cts
  • test/fixtures/filename/esm-module.mjs
  • test/fixtures/filename/get-stack-trace.cjs
  • test/fixtures/filename/index.ts
  • test/fixtures/filename/mts-module.mts

Comment thread test/fixtures/filename/get-stack-trace.cjs Outdated
Comment thread src/eval.ts
((ext === ".js" || ext === ".ts") &&
readNearestPackageJSON(filename)?.type === "module");
const isCommonJS = ext === ".cjs" || ext === ".cts";
const needsTranspile =
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

btw this logic is obviously convoluted. It is equivalent to the following:

const needsTranspile = (() => {
  // force takes precedence
  if (evalOptions.forceTranspile != null) {
    return evalOptions.forceTranspile;
  }

  // TS always needs transpile, regardless whether ESM or CJS
  if (isTypescript) {
    return true;
  }

  // CommonJS skips transpile
  if (isCommonJS) {
    return false;
  }

  // In async mode, we can skip transpiling native ESM as well
  if (isESM && evalOptions.async) {
    return false;
  }

  return isESM || ctx.isTransformRe.test(filename) || hasESMSyntax(source);
})();

which I'd actually prefer to transcribe as

const needsTranspile = (() => {
  // force takes precedence
  if (evalOptions.forceTranspile != null) {
    return evalOptions.forceTranspile;
  }

  // TS always needs transpile, regardless whether ESM or CJS
  if (isTypescript) {
    return true;
  }

  // CommonJS skips transpile
  if (isCommonJS) {
    return false;
  }

  
  if (isESM) {
    // In async mode, we can skip transpiling native ESM as well
    // otherwise it needs to be transformed
    return evalOptions.async;
  }

  // I don't know what this does
  if (ctx.isTransformRe.test(filename)) {
    return true;
  }

  if (hasESMSyntax(source)) {
     // Neither explicitly specified as ESM nor CJS, but contains 
     // ESM syntax. This should be transpiled.
     // See https://nodejs.org/api/packages.html#syntax-detection
     return true;
  }

  return false;
})();

The only reason I didn't do one of these is that the prettier-ignore made it seem like a previous author wanted the code to presented exactly as it is rather than in a more explicit manner.

The important logic change here is that isTypescript is no longer disjoint from isESM and isCJS. Distinguishing isTypescript && isESM from isTypescript && isCJS is necessary to present stack traces correctly for transpiled TS. Thus, now that isTypescript && isCommonJS is possible, the isTypescript check needed to be moved before the isCommonJS, else the isCommonJS check would cause needsTranspile to short-circuit to false.

pi0 and others added 2 commits May 5, 2026 18:21
Use mlly's `pathToFileURL` (returns a string) instead of the
`node:url` one, and inline the IIFE.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/eval.ts`:
- Around line 148-154: mod.filename and the injected __filename are inconsistent
on Windows because resolvePath(filename) is computed only for
vm.runInThisContext (formattedFileName) but mod.filename is left as the original
pathe-normalized filename; to fix, compute resolved = resolvePath(filename) once
early (reuse the same value) and set mod.filename and the value passed into the
module wrapper's __filename to the OS-native resolved/formatted variant (i.e.,
reuse formattedFileName for vm.runInThisContext and ensure __filename uses the
same resolved path for CommonJS); update usages in resolvePath,
formattedFileName, mod.filename, and the module wrapper invocation so __filename
and stack-trace filenames match on Windows.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a81d94eb-8020-45d8-8a2b-c5a70fa8f5a5

📥 Commits

Reviewing files that changed from the base of the PR and between cccfe73 and 61ce691.

📒 Files selected for processing (1)
  • src/eval.ts

Comment thread src/eval.ts
@kirkwaiblinger
Copy link
Copy Markdown
Author

kirkwaiblinger commented May 5, 2026

Looks like the test failure is due to bun formatting the filenames in stack traces differently (path rather than file URL) for ESM. Filed oven-sh/bun#30298.

Regardless of whether the bun issue is accepted and/or fixed, I guess it wouldn't hurt to update the code/tests to be more lenient around whether a url vs a path is detected? Though one might need to have separate snapshot outputs for node vs bun vs deno then. Please advise 🙏

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.

Stack trace does not normalize windows paths

2 participants