npm package: @mnrendra/stack-trace
Description: A lightweight stack trace utility to retrieve CallSite objects from a specific caller.
Author: Muhammad Rendra
Latest version: 3.1.4

# @mnrendra/stack-trace

[![version](https://img.shields.io/npm/v/@mnrendra/stack-trace?logo=nodedotjs)](https://www.npmjs.com/package/@mnrendra/stack-trace?activeTab=versions)
[![downloads](https://img.shields.io/npm/dm/@mnrendra/stack-trace)](https://npm-stat.com/charts.html?package=@mnrendra/stack-trace)
[![size](https://packagephobia.now.sh/badge?p=@mnrendra/stack-trace)](https://packagephobia.com/result?p=%40mnrendra%2Fstack-trace)
[![coverage](https://codecov.io/github/mnrendra/stack-trace/graph/badge.svg?token=LSNMMJVQ77)](https://app.codecov.io/gh/mnrendra/stack-trace)
[![scorecard](https://api.securityscorecards.dev/projects/github.com/mnrendra/stack-trace/badge)](https://securityscorecards.dev/viewer/?uri=github.com/mnrendra/stack-trace)
[![release](https://github.com/mnrendra/stack-trace/actions/workflows/release.yml/badge.svg)](https://github.com/mnrendra/stack-trace/actions/workflows/release.yml)
[![semantic](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)](https://github.com/mnrendra/stack-trace/commits/main)
[![license](https://img.shields.io/npm/l/@mnrendra/stack-trace)](https://github.com/mnrendra/stack-trace/blob/main/LICENSE)

A lightweight [stack trace](https://v8.dev/docs/stack-trace-api) utility to retrieve `CallSite` objects from a specific caller.

*Useful for debugging, logging, or building tools that need to trace call origins at runtime.*

## Features
- ✅ Supports both **ES Modules** and **CommonJS** from a single source - see [package](https://www.npmjs.com/package/@mnrendra/stack-trace?activeTab=code)
- ✅ Minified and cleansed of unnecessary dependencies, files, and attributes - see [contents](https://www.npmjs.com/package/@mnrendra/stack-trace?activeTab=code)
- ✅ Tiny package - see [size](https://bundlephobia.com/package/@mnrendra/stack-trace)
- ✅ Well tested - see [coverage](https://app.codecov.io/gh/mnrendra/stack-trace)
- ✅ Security checked - see [scorecard](https://securityscorecards.dev/viewer/?uri=github.com/mnrendra/stack-trace)
- ✅ Verified all commits - see [signatures](https://github.com/mnrendra/stack-trace/commits/main)
- ✅ Semantic versioning - see [commits](https://github.com/mnrendra/stack-trace/commits/main)
- ✅ Actively maintained - [pull requests](https://github.com/mnrendra/stack-trace/pulls), [issues](https://github.com/mnrendra/stack-trace/issues), [discussions](https://github.com/mnrendra/stack-trace/discussions), and [contributions](https://github.com/mnrendra/stack-trace/blob/HEAD/CONTRIBUTING.md) are welcome!

## Install
```bash
npm i @mnrendra/stack-trace
```

## API Reference

### `stackTrace`
Captures [v8 stack trace](https://v8.dev/docs/stack-trace-api) from a specific caller.

#### Type
```typescript
(callee?: ((...args: any) => any) | null, options?: Options) => NodeJS.CallSite[]
```

#### Parameters
| Name      | Type                              | Description                                                                                                                                                                                                          |
|-----------|-----------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `callee`  | `((...args: any) => any) \| null` | Optional callee function to specify the caller. If `undefined` or `null`, tracing starts from the current caller.                                                                                                    |
| `options` | `Options`                         | Optional options to affect the captured frames. By default, the `limit` option is set to `Infinity` to capture all frames. To capture only a specific number of frames, set the `limit` option to a positive number. |

#### Return
```typescript
NodeJS.CallSite[]
```
Array of `CallSite` objects representing the captured stack trace frames.

#### Options
| Name    | Type     | Default    | Description                                                                                                                                                                                                                                                                                                                                |
|---------|----------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `limit` | `number` | `Infinity` | Specifies the number of stack frames to be collected by a stack trace. The default value is `Infinity`, but may be set to any valid JavaScript number. Changes will affect any stack trace captured after the value has been changed. If set to a non-number value, or set to a negative number, stack traces will not capture any frames. |

### `getCallerSite`
Gets the caller's `CallSite` object captured from [`stackTrace`](#stacktrace).

#### Type
```typescript
(callee?: ((...args: any) => any) | null) => NodeJS.CallSite
```

#### Parameters
| Name      | Type                              | Description                                                                                                       |
|-----------|-----------------------------------|-------------------------------------------------------------------------------------------------------------------|
| `callee`  | `((...args: any) => any) \| null` | Optional callee function to specify the caller. If `undefined` or `null`, tracing starts from the current caller. |

#### Return
```typescript
NodeJS.CallSite
```
First `CallSite` object captured in the stack trace.

### `extractFilePath`
Extracts the file name from a `CallSite` object and converts it to a file path if the value is a file URL.  
*This utility ensures that the returned value is an absolute path.*

#### Type
```typescript
(callSite: NodeJS.CallSite) => string
```

#### Parameters
| Name        | Type              | Description                                                  |
|-------------|-------------------|--------------------------------------------------------------|
| `callSite`  | `NodeJS.CallSite` | `CallSite` object captured from [`stackTrace`](#stacktrace). |

#### Return
```typescript
string
```
Absolute path of the file name extracted from a `CallSite` object.

#### Throws
If the extracted file name is not a string or not absolute.

### `getCallerFile`
Gets the caller's file extracted from the result of [`getCallerSite`](#getcallersite) and ensures it returns an absolute path using [`extractFilePath`](#extractfilepath).

#### Type
```typescript
(callee?: ((...args: any) => any) | null) => string
```

#### Parameters
| Name      | Type                              | Description                                                                                                       |
|-----------|-----------------------------------|-------------------------------------------------------------------------------------------------------------------|
| `callee`  | `((...args: any) => any) \| null` | Optional callee function to specify the caller. If `undefined` or `null`, tracing starts from the current caller. |

#### Return
```typescript
string
```
Absolute path of the caller's file.

#### Throws
If the extracted file name is not a string or not absolute.

### `getCallerDir`
Gets the caller's directory extracted from the result of [`getCallerFile`](#getcallerfile).

#### Type
```typescript
(callee?: ((...args: any) => any) | null) => string
```

#### Parameters
| Name      | Type                              | Description                                                                                                       |
|-----------|-----------------------------------|-------------------------------------------------------------------------------------------------------------------|
| `callee`  | `((...args: any) => any) \| null` | Optional callee function to specify the caller. If `undefined` or `null`, tracing starts from the current caller. |

#### Return
```typescript
string
```
Absolute path of the caller's directory.

#### Throws
If the extracted file name is not a string or not absolute.

## Usage

### ES Modules
`/foo/callee.mjs`
```javascript
import { dirname } from 'node:path'

import {
  stackTrace,
  getCallerSite,
  extractFilePath,
  getCallerFile,
  getCallerDir
} from '@mnrendra/stack-trace'

const callee = () => {
  // `stackTrace`:
  const [callSite1] = stackTrace()
  const [callSite2] = stackTrace(callee, { limit: 1 }) // Pass the `callee` function as the callee.

  console.log(callSite1.getFileName()) // Output: file:///foo/callee.mjs
  console.log(callSite2.getFileName()) // Output: file:///foo/caller.mjs

  console.log(callSite1.getFunctionName()) // Output: callee
  console.log(callSite2.getFunctionName()) // Output: caller

  // `getCallerSite`:
  const callerSite1 = getCallerSite()
  const callerSite2 = getCallerSite(callee) // Pass the `callee` function as the callee.

  console.log(callerSite1.getFileName() === callSite1.getFileName()) // Output: true
  console.log(callerSite2.getFileName() === callSite2.getFileName()) // Output: true

  console.log(callerSite1.getFileName()) // Output: file:///foo/callee.mjs
  console.log(callerSite2.getFileName()) // Output: file:///foo/caller.mjs

  console.log(callerSite1.getFunctionName() === callSite1.getFunctionName()) // Output: true
  console.log(callerSite2.getFunctionName() === callSite2.getFunctionName()) // Output: true

  console.log(callerSite1.getFunctionName()) // Output: callee
  console.log(callerSite2.getFunctionName()) // Output: caller

  // `extractFilePath`:
  const filePath1 = extractFilePath(callerSite1)
  const filePath2 = extractFilePath(callerSite2)

  console.log(filePath1) // Output: /foo/callee.mjs
  console.log(filePath2) // Output: /foo/caller.mjs

  // `getCallerFile`:
  const callerFile1 = getCallerFile()
  const callerFile2 = getCallerFile(callee) // Pass the `callee` function as the callee.

  console.log(callerFile1 === filePath1) // Output: true
  console.log(callerFile2 === filePath2) // Output: true

  console.log(callerFile1) // Output: /foo/callee.mjs
  console.log(callerFile2) // Output: /foo/caller.mjs

  // `getCallerDir`:
  const callerDir1 = getCallerDir()
  const callerDir2 = getCallerDir(callee) // Pass the `callee` function as the callee.

  console.log(callerDir1 === dirname(filePath1)) // Output: true
  console.log(callerDir2 === dirname(filePath2)) // Output: true

  console.log(callerDir1) // Output: /foo
  console.log(callerDir2) // Output: /foo
}

export default callee
```

`/foo/caller.mjs`
```javascript
import callee from './callee.mjs'
const caller = () => callee()
caller()
```

### CommonJS
`/foo/callee.cjs`
```javascript
const { dirname } = require('node:path')

const {
  stackTrace,
  getCallerSite,
  extractFilePath,
  getCallerFile,
  getCallerDir
} = require('@mnrendra/stack-trace')

const callee = () => {
  // `stackTrace`:
  const [callSite1] = stackTrace()
  const [callSite2] = stackTrace(callee, { limit: 1 }) // Pass the `callee` function as the callee.

  console.log(callSite1.getFileName()) // Output: /foo/callee.cjs
  console.log(callSite2.getFileName()) // Output: /foo/caller.cjs

  console.log(callSite1.getFunctionName()) // Output: callee
  console.log(callSite2.getFunctionName()) // Output: caller

  // `getCallerSite`:
  const callerSite1 = getCallerSite()
  const callerSite2 = getCallerSite(callee) // Pass the `callee` function as the callee.

  console.log(callerSite1.getFileName() === callSite1.getFileName()) // Output: true
  console.log(callerSite2.getFileName() === callSite2.getFileName()) // Output: true

  console.log(callerSite1.getFileName()) // Output: /foo/callee.cjs
  console.log(callerSite2.getFileName()) // Output: /foo/caller.cjs

  console.log(callerSite1.getFunctionName() === callSite1.getFunctionName()) // Output: true
  console.log(callerSite2.getFunctionName() === callSite2.getFunctionName()) // Output: true

  console.log(callerSite1.getFunctionName()) // Output: callee
  console.log(callerSite2.getFunctionName()) // Output: caller

  // `extractFilePath`:
  const filePath1 = extractFilePath(callerSite1)
  const filePath2 = extractFilePath(callerSite2)

  console.log(filePath1) // Output: /foo/callee.cjs
  console.log(filePath2) // Output: /foo/caller.cjs

  // `getCallerFile`:
  const callerFile1 = getCallerFile()
  const callerFile2 = getCallerFile(callee) // Pass the `callee` function as the callee.

  console.log(callerFile1 === filePath1) // Output: true
  console.log(callerFile2 === filePath2) // Output: true

  console.log(callerFile1) // Output: /foo/callee.cjs
  console.log(callerFile2) // Output: /foo/caller.cjs

  // `getCallerDir`:
  const callerDir1 = getCallerDir()
  const callerDir2 = getCallerDir(callee) // Pass the `callee` function as the callee.

  console.log(callerDir1 === dirname(filePath1)) // Output: true
  console.log(callerDir2 === dirname(filePath2)) // Output: true

  console.log(callerDir1) // Output: /foo
  console.log(callerDir2) // Output: /foo
}

module.exports = callee
```

`/foo/caller.cjs`
```javascript
const callee = require('./callee.cjs')
const caller = () => callee()
caller()
```

> **Note**:
>
> - In ES Modules, `getFileName` returns a **file URL** (e.g., `file:///foo`), instead of a **file path** (`/foo`).  
> *To convert it to a file path, use either `url.fileURLToPath` or the `extractFilePath` utility.*
>
> - By default `stackTrace` will capture all caller's frames.  
> *To capture only a specific number of frames, set the `limit` option to a positive number.*

### Examples

1. **Call from a development project**

`/foo/project-name/src/index.mjs`:
```javascript
import { dirname } from 'node:path'
import { fileURLToPath } from 'node:url'

import {
  stackTrace,
  getCallerSite,
  extractFilePath,
  getCallerFile,
  getCallerDir
} from '@mnrendra/stack-trace'

// `stackTrace`:
const caller1 = () => stackTrace()
const [callSite] = caller1()
const fileName = callSite.getFileName()
console.log(fileName) // Output: file:///foo/project-name/src/index.mjs
console.log(fileURLToPath(fileName)) // Output: /foo/project-name/src/index.mjs

// `getCallerSite`:
const caller2 = () => getCallerSite()
const callerSite = caller2()
const callerFileName = callerSite.getFileName()
console.log(callerFileName === fileName) // Output: true
console.log(callerFileName) // Output: file:///foo/project-name/src/index.mjs
console.log(fileURLToPath(callerFileName)) // Output: /foo/project-name/src/index.mjs

// `extractFilePath`:
const filePath = extractFilePath(callerSite)
console.log(filePath === fileURLToPath(callerFileName)) // Output: true
console.log(filePath) // Output: /foo/project-name/src/index.mjs

// `getCallerFile`:
const caller3 = () => getCallerFile()
const callerFile = caller3()
console.log(callerFile === filePath) // Output: true
console.log(callerFile) // Output: /foo/project-name/src/index.mjs

// `getCallerDir`:
const caller4 = () => getCallerDir()
const callerDir = caller4()
console.log(callerDir === dirname(filePath)) // Output: true
console.log(callerDir) // Output: /foo/project-name/src
```

2. **Call from a production package**

`/foo/consumer/node_modules/module-name/dist/index.cjs`:
```javascript
"use strict";

const { dirname } = require("node:path");

const {
  stackTrace,
  getCallerSite,
  extractFilePath,
  getCallerFile,
  getCallerDir
} = require("@mnrendra/stack-trace");

// `stackTrace`:
const caller1 = () => stackTrace();
const [callSite] = caller1();
const fileName = callSite.getFileName();
console.log(fileName); // Output: /foo/consumer/node_modules/module-name/dist/index.cjs

// `getCallerSite`:
const caller2 = () => getCallerSite();
const callerSite = caller2();
const callerFileName = callerSite.getFileName();
console.log(callerFileName === fileName); // Output: true
console.log(callerFileName); // Output: /foo/consumer/node_modules/module-name/dist/index.cjs

// `extractFilePath`:
const filePath = extractFilePath(callerSite);
console.log(filePath === callerFileName); // Output: true
console.log(filePath); // Output: /foo/consumer/node_modules/module-name/dist/index.cjs

// `getCallerFile`:
const caller3 = () => getCallerFile()
const callerFile = caller3()
console.log(callerFile === filePath) // Output: true
console.log(callerFile) // Output: /foo/consumer/node_modules/module-name/dist/index.cjs

// `getCallerDir`:
const caller4 = () => getCallerDir();
const callerDir = caller4();
console.log(callerDir === dirname(filePath)); // Output: true
console.log(callerDir); // Output: /foo/consumer/node_modules/module-name/dist
```

## Types

### `Options`
[`stackTrace`](#stacktrace)'s [options](#options) interface.

```typescript
import {
  type Options,
  stackTrace
} from '@mnrendra/stack-trace'

const options: Options = {
  limit: 1
}

const caller = (): NodeJS.CallSite[] => stackTrace(caller, options)
const callSites = caller()
console.log(callSites.length) // Output: 1
```

## Security

We take security seriously in this project. If you discover a **vulnerability**, we strongly encourage you to report it in a responsible manner.

Please open a [Security Advisory](https://github.com/mnrendra/stack-trace/security/advisories/new) to report any vulnerabilities.

For more information, please refer to our [Security Policy](https://github.com/mnrendra/stack-trace/blob/HEAD/SECURITY.md).

## Contributing

We appreciate your help in making this project better. Please follow the [guidelines](https://github.com/mnrendra/stack-trace/blob/HEAD/CONTRIBUTING.md) to ensure that your contributions are smoothly integrated.

## License
[MIT](https://github.com/mnrendra/stack-trace/blob/HEAD/LICENSE)

## Author
[@mnrendra](https://github.com/mnrendra)