Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b17de5f
wip
rochdev Mar 6, 2026
8521cef
fix transformer tests
rochdev Mar 6, 2026
f5cf14f
fix typo
rochdev Mar 6, 2026
a070d3e
rewrite tests to js and fix super support in constructors
rochdev Mar 6, 2026
82f2d96
add support for isExportAlias
rochdev Mar 9, 2026
5cd96c7
add missing features from rust
rochdev Mar 9, 2026
41c8860
add oxc + tests and remove build requirement
rochdev Mar 10, 2026
73f786d
update type definitions
rochdev Mar 10, 2026
dec3766
add custom transforms
rochdev Mar 10, 2026
c39a725
rename old ci tests
rochdev Mar 10, 2026
65bd0c8
switch to npm in ci and fix typo
rochdev Mar 10, 2026
cbbb118
downgrade meriyah for node 18 compat
rochdev Mar 10, 2026
4117dbe
add linting and fix errors
rochdev Mar 10, 2026
0eb4999
add jsdoc
rochdev Mar 10, 2026
66c92c0
add test for astQuery
rochdev Mar 10, 2026
14164d4
add source map test
rochdev Mar 10, 2026
5fa920f
fix support for node 18
rochdev Mar 10, 2026
8a6ed12
remove iterator support for now
rochdev Mar 12, 2026
bf0c5b1
simplify code and remove custom parser for now
rochdev Mar 12, 2026
357cffb
update jsdoc
rochdev Mar 12, 2026
d4fa04f
remove all rust and update docs
rochdev Mar 12, 2026
b3b1715
update 3rd party licenses
rochdev Mar 12, 2026
4443619
remove oxc-parser
rochdev Mar 12, 2026
ef0131a
temporarily revert lockfile ignore
rochdev Mar 12, 2026
5a55f86
delete yarn lockfile
rochdev Mar 12, 2026
0587761
add back ignore for lockfiles
rochdev Mar 12, 2026
1fee50d
remove copyright from new files
rochdev Mar 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,25 @@ env:
RUST_VERSION: 1.87.0
jobs:
test:
name: Test (${{ matrix.node-version }})
strategy:
matrix:
node-version: [18, 20, 22, 24, latest]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install
run: yarn
- name: Run tests
run: yarn test

# TODO: remove once JS rewrite lands
test-old:
name: Build, Lint & Test
runs-on: ubuntu-latest
steps:
Expand Down
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,3 @@ tests/*/instrumented.*
pkg/
node_modules/
package-lock.json
index.js
index.d.ts
114 changes: 114 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/* tslint:disable */
/* eslint-disable */
import type { Node } from 'estree';
/**
* Create a new instrumentation matcher from an array of instrumentation configs.
*/
export function create(configs: InstrumentationConfig[], dc_module?: string | null): InstrumentationMatcher;
/**
* Output of a transformation operation
*/
export interface TransformOutput {
/**
* The transformed JavaScript code
*/
code: string;
/**
* The sourcemap for the transformation (if generated)
*/
map: string | undefined;
}

/**
* The kind of function
*/
export type FunctionKind = "Sync" | "Async" | "AsyncIterator" | "Callback" | "Iterator";

/**
* Describes which function to instrument
*/
export type FunctionQuery = { className: string; methodName: string; kind: FunctionKind; index?: number; isExportAlias?: boolean } | { className: string; privateMethodName: string; kind: FunctionKind; index?: number } | { className: string; index?: number; isExportAlias?: boolean } | { methodName: string; kind: FunctionKind; index?: number } | { functionName: string; kind: FunctionKind; index?: number; isExportAlias?: boolean } | { expressionName: string; kind: FunctionKind; index?: number; isExportAlias?: boolean };

/**
* A custom transform function registered via `addTransform`.
* Receives the instrumentation state and the matched AST node.
*/
export type CustomTransform = (state: unknown, node: Node, parent: Node, ancestry: Node[]) => void;

/**
* Configuration for injecting instrumentation code
*/
export interface InstrumentationConfig {
/**
* The name of the diagnostics channel to publish to
*/
channelName: string;
/**
* The module matcher to identify the module and file to instrument
*/
module: ModuleMatcher;
/**
* The function query to identify the function to instrument
*/
functionQuery: FunctionQuery;
/**
* The name of a custom transform registered via `addTransform`.
* When set, takes precedence over `functionQuery.kind`.
*/
transform?: string;
}

/**
* Describes the module and file path you would like to match
*/
export interface ModuleMatcher {
/**
* The name of the module you want to match
*/
name: string;
/**
* The semver range that you want to match
*/
versionRange: string;
/**
* The path of the file you want to match from the module root
*/
filePath: string;
}

/**
* The type of module being passed - ESM, CJS or unknown
*/
export type ModuleType = "esm" | "cjs" | "unknown";

/**
* The InstrumentationMatcher is responsible for matching specific modules
*/
export class InstrumentationMatcher {
private constructor();
free(): void;
/**
* Get a transformer for the given module name, version and file path.
* Returns `undefined` if no matching instrumentations are found.
*/
getTransformer(module_name: string, version: string, file_path: string): Transformer | undefined;
/**
* Register a custom transform function under the given name.
* The name can then be referenced via the `transform` option in an `InstrumentationConfig`.
*/
addTransform(name: string, fn: CustomTransform): void;
}
/**
* The Transformer is responsible for transforming JavaScript code.
*/
export class Transformer {
private constructor();
free(): void;
/**
* Transform JavaScript code and optionally sourcemap.
*
* # Errors
* Returns an error if the transformation fails to find injection points.
*/
transform(code: string, module_type: ModuleType, sourcemap?: string | null): TransformOutput;
}
3 changes: 3 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"use strict";

module.exports = require('./lib')
31 changes: 0 additions & 31 deletions index.ts

This file was deleted.

107 changes: 107 additions & 0 deletions lib/compiler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
'use strict'

const runtimeRequire = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require

const { ORCHESTRION_PARSER } = process.env

const compiler = {
parse: (sourceText, options) => {
const useOxc = () => {
// TODO: Figure out ESBuild `createRequire` issue and remove this hack.
const oxc = runtimeRequire(['oxc', 'parser'].join('-'))

compiler.parse = (sourceText, options) => {
const { program, errors } = oxc.parseSync('index.js', sourceText, {
...options,
preserveParens: false,
})

if (errors?.length > 0) throw errors[0]

if (options?.range) addLoc(program, sourceText.toString())

return program
}
}

const useMeriyah = () => {
const meriyah = require('meriyah')

compiler.parse = (sourceText, { range, sourceType } = {}) => {
return meriyah.parse(sourceText.toString(), {
loc: range,
ranges: range,
raw: true,
module: sourceType === 'module',
})
}
}

if (ORCHESTRION_PARSER === 'meriyah') {
useMeriyah()
} else {
try {
useOxc()
} catch (e) {
if (ORCHESTRION_PARSER === 'oxc') throw e
useMeriyah() // Fallback for when OXC is not available.
}
}

return compiler.parse(sourceText, options)
},

generate: (...args) => {
const astring = require('astring')

compiler.generate = astring.generate

return compiler.generate(...args)
},

traverse: (ast, query, visitor) => {
const esquery = require('esquery')

compiler.traverse = (ast, query, visitor) => {
return esquery.traverse(ast, esquery.parse(query), visitor)
}

return compiler.traverse(ast, query, visitor)
},

query: (ast, query) => {
const esquery = require('esquery')

compiler.query = esquery.query

return compiler.query(ast, query)
},
}

function addLoc (node, sourceText) {
const lineOffsets = [0]
for (let i = 0; i < sourceText.length; i++) {
if (sourceText[i] === '\n') lineOffsets.push(i + 1)
}
const offsetToLoc = (offset) => {
let lo = 0, hi = lineOffsets.length - 1
while (lo < hi) {
const mid = (lo + hi + 1) >> 1
if (lineOffsets[mid] <= offset) lo = mid; else hi = mid - 1
}
return { line: lo + 1, column: offset - lineOffsets[lo] }
}

compiler.traverse(node, '*', (node) => {
if ('start' in node && !node.loc) {
node.loc = { start: offsetToLoc(node.start), end: offsetToLoc(node.end) }
}
})
}

module.exports = {
parse: (...args) => compiler.parse(...args),
generate: (...args) => compiler.generate(...args),
traverse: (...args) => compiler.traverse(...args),
query: (...args) => compiler.query(...args),
}
9 changes: 9 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict'

const { InstrumentationMatcher } = require('./matcher')

function create (configs, dc_module) {
return new InstrumentationMatcher(configs, dc_module)
}

module.exports = { create }
53 changes: 53 additions & 0 deletions lib/matcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict'

const semifies = require('semifies')
const { Transformer } = require('./transformer')

class InstrumentationMatcher {
#configs = []
#dc_module = null
#transformers = {}
#customTransforms = {}

constructor (configs, dc_module) {
this.#configs = configs
this.#dc_module = dc_module || 'diagnostics_channel'
}

free () {
this.#transformers = {}
}

addTransform (name, fn) {
this.#customTransforms[name] = fn
}

getTransformer (module_name, version, file_path) {
file_path = file_path.replace(/\\/g, '/')

const id = `${module_name}/${file_path}@${version}`

if (this.#transformers[id]) return this.#transformers[id]

const configs = this.#configs.filter(({ module: { name, filePath, versionRange } }) =>
name === module_name &&
filePath === file_path &&
semifies(version, versionRange)
)

if (configs.length === 0) return

this.#transformers[id] = new Transformer(
module_name,
version,
file_path,
configs,
this.#dc_module,
this.#customTransforms
)

return this.#transformers[id]
}
}

module.exports = { InstrumentationMatcher }
Loading
Loading