npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@apm-js-collab/code-transformer

v0.18.1

Published

This is a library to aid in instrumenting Node.js libraries at build or load time.

Readme

Orchestrion-JS / @apm-js-collab/code-transformer

This is a library to aid in instrumenting Node.js libraries at build or load time.

It uses an AST walker to inject code that calls Node.js TracingChannel.

You likely don't want to use this library directly; instead, consider using:

JavaScript

@apm-js-collab/code-transformer exposes the library.

Usage

import * as codeTransformer from "@apm-js-collab/code-transformer";

// The full instrumentation config
const instrumentation = {
    // The name of the diagnostics channel
    channelName: "my-channel",
    // Define the module you'd like to inject tracing channels into
    module: {
        name: "my-module",
        versionRange: ">=1.0.0",
        filePath: "./dist/index.js",
    },
    // Define the function you'd like to instrument
    // (e.g., match a method named 'foo' that returns a Promise)
    functionQuery: {
        methodName: "fetch",
        kind: "Async",
    },
};

// Create an InstrumentationMatcher with an array of instrumentation configs
const matcher = codeTransformer.create([instrumentation]);

// Get a transformer for a specific module
const transformer = matcher.getTransformer(
    "my-module",
    "1.2.3",
    "./dist/index.js",
);

if (transformer === undefined) {
    throw new Error("No transformer found for module");
}

// Transform code
const inputCode = "async function fetch() { return 42; }";
const result = transformer.transform(inputCode, "unknown");
console.log(result.code);

Export Aliases

When a module re-exports a function or class under a different name using export { local as exported }, you can target the exported name in your FunctionQuery by setting isExportAlias: true. The transformer will resolve the alias to the local declaration before matching.

For example, given:

function f(url) { return fetch(url); }
export { f as fetchAliased };

You can target fetchAliased in your config:

const instrumentation = {
    channelName: "my-channel",
    module: { name: "my-module", versionRange: ">=1.0.0", filePath: "./index.mjs" },
    functionQuery: { functionName: "fetchAliased", kind: "Async", isExportAlias: true },
};

This also works for class exports (e.g., export { MyClass as PublicClass }).

Mutating the Return Value

A subscriber can both observe or mutate a function's return value via message.result. Reassigning message.result is useful when a function returns another function (or object) that you need to wrap, like a factory that returns a per-request handler.

For synchronous functions, reassign message.result in the end handler:

const instrumentation = {
    channelName: "create-handler",
    module: { name: "my-framework", versionRange: ">=1.0.0", filePath: "lib/router.js" },
    functionQuery: { methodName: "create", kind: "Sync" },
};
const { tracingChannel } = require("node:diagnostics_channel");

tracingChannel("orchestrion:my-framework:create-handler").subscribe({
    end(message) {
        const original = message.result;
        // Replace the returned handler with a wrapped version.
        message.result = function wrapped(...args) {
            // ...start a span, etc.
            return original.apply(this, args);
        };
    },
});

For asynchronous functions, reassign message.result in the asyncEnd handler to substitute the value the returned promise resolves to — for example, to wrap a function the promise resolves to:

const instrumentation = {
    channelName: "load-handler",
    module: { name: "my-framework", versionRange: ">=1.0.0", filePath: "lib/router.js" },
    functionQuery: { methodName: "load", kind: "Async" },
};
tracingChannel("orchestrion:my-framework:load-handler").subscribe({
    asyncEnd(message) {
        const original = message.result;
        message.result = function wrapped(...args) {
            // ...start a span, etc.
            return original.apply(this, args);
        };
    },
});

[!INFO]

Mutating the resolved value of an async function only works when it returns a native Promise (value instanceof Promise). Promise subclasses and other userland thenables are side-chained and returned to the caller unchanged so that their subclass-specific methods (e.g. APIPromise.withResponse()) remain accessible; their resolved value therefore cannot be mutated, and reassigning message.result for them has no effect.

On the throw path the original error still propagates; the substituted return value only applies when the function returns (or resolves) normally. If no subscriber reassigns message.result, the original return value is preserved unchanged.

AST Query

The name-based FunctionQuery variants cover the common cases (named functions, class/object methods, expressions). When you need to target a node they can't express, such as an anonymous function returned by a factory (a decorator factory, a per-request handler), you can set astQuery to a raw esquery selector instead.

When present, astQuery chooses the nodes to instrument and takes precedence over functionQuery's matching fields; functionQuery then only supplies behaviour (kind, index, callbackIndex) and may be omitted (it defaults to kind: "Sync").

For example, to instrument the decorator returned by a factory:

function Injectable(options) {
    return (target) => { /* applied to the decorated class */ };
}
const instrumentation = {
    channelName: "injectable-apply",
    module: { name: "@nestjs/common", versionRange: ">=8.0.0", filePath: "decorators/core/injectable.decorator.js" },
    // Match the arrow returned from `Injectable`. There is no name to target!
    astQuery: 'FunctionDeclaration[id.name="Injectable"] ReturnStatement > ArrowFunctionExpression',
    functionQuery: { kind: "Sync" },
};

The channel then fires each time the decorator is applied, with the decorated target available as message.arguments[0], which a subscriber can mutate, for example to wrap prototype methods.

An astQuery is used verbatim, so it can match any node, including ones the name-based variants don't expose, such as anonymous or deeply nested functions. (Both name-based and astQuery matching work on synchronous and async functions alike.)

If an astQuery matches no nodes, the "failed to find injection points" error includes the selector so it can be debugged.

Custom Transforms

The built-in operators (traceSync, tracePromise, traceCallback, traceAuto, selected by functionQuery.kind) cover diagnostics-channel tracing. When you need to rewrite a matched node differently, register a custom operator with matcher.addTransform(name, fn) and reference it from a config's transform field. When transform is set it takes precedence over functionQuery.kind.

A custom transform receives the same arguments an esquery traversal yields, plus the merged instrumentation state, and mutates the matched node in place:

type CustomTransform = (state, node, parent, ancestry) => void;
  • state - the config merged with runtime fields (moduleVersion, moduleType, operator, the resolved functionQuery, …).
  • node - the matched AST node (the function/expression selected by functionQuery or astQuery).
  • parent, ancestry - the parent node and full ancestor chain (root last); ancestry[ancestry.length - 1] is the Program.

A custom transform fully owns how node is rewritten. It is not required to inject a tracing channel at all.

const matcher = codeTransformer.create([
    {
        channelName: "fetch",
        module: { name: "my-module", versionRange: ">=1.0.0", filePath: "index.js" },
        functionQuery: { functionName: "fetch", kind: "Sync" },
        transform: "myCustomTransform",
    },
]);

matcher.addTransform("myCustomTransform", (state, node) => {
    // Mutate the matched node's AST however you need.
    node.body.body.unshift(/* an ESTree statement */);
});

Registering a name matching a built-in transform (traceSync, tracePromise, traceCallback, traceAuto, tracingChannelImport, tracingChannelDeclaration) overrides it everywhere it is dispatched — including when one built-in invokes another internally (e.g. the trace operators calling tracingChannelDeclaration, which in turn calls tracingChannelImport). state.transforms holds the merged map (built-ins plus registered overrides) used for this dispatch; to delegate to the original built-in from an override, call it via the @apm-js-collab/code-transformer/lib/transforms module directly.

(The CLI accepts the same transforms via the customTransforms field of a configuration module. See: CLI Tool.)

API Reference

type ModuleType = "esm" | "cjs" | "unknown";
type FunctionKind = "Sync" | "Async" | "Callback" | "Auto";

FunctionQuery Variants

type FunctionQuery =
    | // Match class constructor
    { className: string; index?: number | null; isExportAlias?: boolean }
    | // Match class method
    {
        className: string;
        methodName: string;
        kind: FunctionKind;
        index?: number | null;
        callbackIndex?: number;
        isExportAlias?: boolean;
    }
    | // Match method on objects
    { methodName: string; kind: FunctionKind; index?: number | null; callbackIndex?: number }
    | // Match standalone function
    { functionName: string; kind: FunctionKind; index?: number | null; callbackIndex?: number; isExportAlias?: boolean }
    | // Match arrow function or function expression
    { expressionName: string; kind: FunctionKind; index?: number | null; callbackIndex?: number; isExportAlias?: boolean };
    | // Match private class methods
    { className: string; privateMethodName: string; kind: FunctionKind; index?: number | null; callbackIndex?: number };

ModuleMatcher

type ModuleMatcher = {
    /** Module name */
    name: string; 
    /** Matching semver range */
    versionRange: string;
    /** 
     * Relative Unix-style path to the file from the module root (e.g. "lib/index.js") 
     * Or a regular expression to test against the Unix-style path.
     */
    filePath: string | RegExp;
};

InstrumentationConfig

// Behaviour-only fields, used when `astQuery` does the matching.
type FunctionBehavior = {
    kind?: FunctionKind;
    index?: number | null;
    callbackIndex?: number;
};

type InstrumentationConfig =
    | {
        channelName: string; // Name of the diagnostics channel
        module: ModuleMatcher;
        functionQuery: FunctionQuery; // Name-based matching
        astQuery?: string; // Raw esquery selector; takes precedence over functionQuery matching
        transform?: string; // Name of a custom transform registered via addTransform
    }
    | {
        channelName: string;
        module: ModuleMatcher;
        astQuery: string; // Raw esquery selector chooses the node(s)
        functionQuery?: FunctionBehavior; // Behaviour only; matching fields ignored
        transform?: string;
    };

Functions

create(configs: InstrumentationConfig[], dcModule?: string | null): InstrumentationMatcher;

Create a matcher for one or more instrumentation configurations.

  • configs - Array of instrumentation configurations.
  • dcModule - Optional module to import diagnostics_channel API from.

InstrumentationMatcher

getTransformer(moduleName: string, version: string, filePath: string): Transformer | undefined;

Gets a transformer for a specific module and file.

Returns a Transformer for the given module, or undefined if there were no matching instrumentation configurations.

  • moduleName - Name of the module.
  • version - Version of the module.
  • filePath - Relative Unix-style path to the file from the module root (e.g. "lib/index.js"). Windows-style backslash paths are also accepted and will be normalized automatically.
addTransform(name: string, fn: CustomTransform): void;

Registers a custom transform operator under name, referenced from a config's transform field. See Custom Transforms.

  • name - Operator name (also used as the config's transform value).
  • fn - (state, node, parent, ancestry) => void; mutates the matched node.

Transformer

transform(code: string | Buffer, moduleType: ModuleType, sourcemap?: string | undefined): TransformOutput;

Transforms the code, injecting tracing as configured.

Returns { code, map }. map will be undefined if no sourcemap was supplied.

  • code - The JavaScript code to transform.
  • moduleType - The type of module being transformed.
  • sourcemap - Optional existing source map for the code.

CLI Tool

The package includes a CLI tool for applying transformations to source files:

npx @apm-js-collab/code-transformer transformer.js source-file.js

CLI Usage

The CLI tool takes two arguments:

  1. transformer.js - A file that exports instrumentation configuration(s)
  2. source-file.js - The source file to transform (can be any path, including node_modules)

The transformed code is written to stdout, which you can redirect to a file or pipe to other commands.

CLI Example

Create a transformer configuration file:

// my-transformer.js
module.exports = [{
  channelName: 'my-fetch-channel',
  module: {
    name: 'my-module',
    versionRange: '>=1.0.0',
    filePath: 'index.js'
  },
  functionQuery: {
    functionName: 'fetch',
    kind: 'Async'
  }
}]

Apply the transformation:

npx @apm-js-collab/code-transformer my-transformer.js lib/index.js > instrumented.js

The transformer configuration file can also export an object with additional options:

module.exports = {
  configs: [/* array of configs */],
  dcModule: './custom-diagnostics-channel.js', // optional custom dc module
  customTransforms: {  // optional custom transform functions
    myCustomTransform: (state, node) => {
      // custom AST transformation logic
    }
  }
}

License

See LICENSE