Skip to content

fix stack trace filename format for transpiled files#398

Open
kirkwaiblinger wants to merge 11 commits intounjs:mainfrom
kirkwaiblinger:fix-ts-stack-traces
Open

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

Conversation

@kirkwaiblinger
Copy link

@kirkwaiblinger kirkwaiblinger commented Sep 1, 2025

resolves #397

Summary by CodeRabbit

Release Notes

  • New Features
    • Added support for TypeScript module extensions (.mts for ES modules, .cts for CommonJS).
    • Enhanced module type detection and transpilation handling.
    • Improved runtime filename handling with proper URL formatting for ES modules.
    • Better support for projects using mixed module formats.

@kirkwaiblinger kirkwaiblinger marked this pull request as ready for review September 1, 2025 03:12
@pi0
Copy link
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
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
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

coderabbitai bot commented Feb 27, 2026

📝 Walkthrough

Walkthrough

This pull request fixes Windows path normalization in stack traces by enhancing module type detection (treating .mts/.cts extensions appropriately) and introducing formattedFileName logic that converts ESM module paths to file URLs while preserving CommonJS paths as resolved file paths.

Changes

Cohort / File(s) Summary
Module Type Detection & Path Formatting
src/eval.ts
Enhanced ESM detection to treat .mts as ESM and .cts as CommonJS; added .js/.ts with package.json type:module as ESM. Adjusted needsTranspile logic for TypeScript files. Introduced formattedFileName that converts to file URL for ESM via pathToFileURL, otherwise uses resolved path. Updated vm.runInThisContext to use formattedFileName for proper stack trace paths.
Test Fixtures - Module Implementations
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 test modules across CommonJS and ES module formats (both plain and TypeScript variants) that export filename/dirname metadata and stack trace information for validation.
Test Fixtures - Stack Trace Helper
test/fixtures/filename/get-stack-trace.cjs
Added CommonJS helper module that captures and returns the top stack frame by temporarily adjusting Error.stackTraceLimit and Error.prepareStackTrace.
Test Fixtures - Validation Script
test/fixtures/filename/index.ts
Added comprehensive test script that loads and inspects file paths for all four module types (CJS, CTS, ESM, MTS), logging comparisons between module-provided paths and stack-derived paths with platform resolution checks.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A path once tangled, now runs straight,
Windows slashes normalized, no more wait,
ESM gets its file:// coat,
CJS its resolved note,
Stack traces clear—our fuzzy fates! 🎉

🚥 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 PR title 'fix stack trace filename format for transpiled files' accurately describes the main change—normalizing Windows path formatting in stack traces for transpiled TypeScript files.
Linked Issues check ✅ Passed The PR addresses all coding requirements from issue #397: enhanced module type detection for ESM/CJS, formatted filename handling (pathToFileURL for ESM), and Windows path normalization via vm.runInThisContext.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing Windows stack trace formatting: module type detection logic, filename formatting, and comprehensive test fixtures for CJS/CTS/ESM/MTS modules.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

@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 on lines +3 to +10
const oldStackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 1;
const oldPrepareStackTrace = Error.prepareStackTrace;
Error.prepareStackTrace = (_, stack) => stack;
Error.captureStackTrace(dummyObj, getTopOfStackTrace);
const stack = dummyObj.stack;
Error.prepareStackTrace = oldPrepareStackTrace;
Error.stackTraceLimit = oldStackTraceLimit;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "get-stack-trace.cjs" -type f

Repository: unjs/jiti

Length of output: 97


🏁 Script executed:

cat -n ./test/fixtures/filename/get-stack-trace.cjs

Repository: unjs/jiti

Length of output: 675


🏁 Script executed:

# Check how this function is used in tests to understand the impact
rg "getStackTrace|get-stack-trace" test/ --type js --type cjs -B 2 -A 2

Repository: unjs/jiti

Length of output: 80


🏁 Script executed:

# Check how this function is used in tests to understand the impact
rg "getStackTrace|get-stack-trace" test/ -B 2 -A 2

Repository: unjs/jiti

Length of output: 1392


🏁 Script executed:

# Check if there are any tests that exercise this function or if there's error handling around it
rg "Error\.captureStackTrace|Error\.prepareStackTrace|Error\.stackTraceLimit" --type-list | head -20

Repository: unjs/jiti

Length of output: 442


🏁 Script executed:

# Check if Error properties are used elsewhere
rg "Error\.captureStackTrace|Error\.prepareStackTrace|Error\.stackTraceLimit"

Repository: unjs/jiti

Length of output: 813


Restore global Error hooks via finally block.

Error.prepareStackTrace and Error.stackTraceLimit are process-global properties. If an exception occurs during stack capture (lines 6–8), the restoration on lines 9–10 will not execute, leaving the Error object in a modified state and potentially affecting subsequent code execution.

Suggested fix
 function getTopOfStackTrace() {
   const dummyObj = {};
   const oldStackTraceLimit = Error.stackTraceLimit;
-  Error.stackTraceLimit = 1;
   const oldPrepareStackTrace = Error.prepareStackTrace;
-  Error.prepareStackTrace = (_, stack) => stack;
-  Error.captureStackTrace(dummyObj, getTopOfStackTrace);
-  const stack = dummyObj.stack;
-  Error.prepareStackTrace = oldPrepareStackTrace;
-  Error.stackTraceLimit = oldStackTraceLimit;
-  return stack.at(-1);
+  try {
+    Error.stackTraceLimit = 1;
+    Error.prepareStackTrace = (_, stack) => stack;
+    Error.captureStackTrace(dummyObj, getTopOfStackTrace);
+    const stack = dummyObj.stack;
+    return stack.at(-1);
+  } finally {
+    Error.prepareStackTrace = oldPrepareStackTrace;
+    Error.stackTraceLimit = oldStackTraceLimit;
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/fixtures/filename/get-stack-trace.cjs` around lines 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.

((ext === ".js" || ext === ".ts") &&
readNearestPackageJSON(filename)?.type === "module");
const isCommonJS = ext === ".cjs" || ext === ".cts";
const needsTranspile =
Copy link
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.

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