# @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)