@apm-js-collab/code-transformer 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -44
- package/index.d.ts +111 -3
- package/index.js +3 -18
- package/lib/index.js +17 -0
- package/lib/matcher.js +83 -0
- package/lib/transformer.js +282 -0
- package/lib/transforms.js +459 -0
- package/package.json +17 -13
- package/pkg/orchestrion_js.d.ts +0 -97
- package/pkg/orchestrion_js.js +0 -563
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
This is a library to aid in instrumenting Node.js libraries at build or load
|
|
4
4
|
time.
|
|
5
5
|
|
|
6
|
-
It uses
|
|
6
|
+
It uses an AST walker to inject code that calls Node.js
|
|
7
7
|
[`TracingChannel`](https://nodejs.org/api/diagnostics_channel.html#class-tracingchannel).
|
|
8
8
|
|
|
9
9
|
You likely don't want to use this library directly; instead, consider using:
|
|
@@ -16,18 +16,7 @@ You likely don't want to use this library directly; instead, consider using:
|
|
|
16
16
|
|
|
17
17
|
## JavaScript
|
|
18
18
|
|
|
19
|
-
`@apm-js-collab/code-transformer` exposes the
|
|
20
|
-
module.
|
|
21
|
-
|
|
22
|
-
### Building
|
|
23
|
-
|
|
24
|
-
To build the JavaScript module:
|
|
25
|
-
|
|
26
|
-
- Ensure you have [Rust installed](https://www.rust-lang.org/tools/install)
|
|
27
|
-
- Install the wasm toolchain\
|
|
28
|
-
`rustup target add wasm32-unknown-unknown --toolchain stable`
|
|
29
|
-
- Install dependencies and build the module\
|
|
30
|
-
`npm install && npm run build`
|
|
19
|
+
`@apm-js-collab/code-transformer` exposes the library.
|
|
31
20
|
|
|
32
21
|
### Usage
|
|
33
22
|
|
|
@@ -70,10 +59,6 @@ if (transformer === undefined) {
|
|
|
70
59
|
const inputCode = "async function fetch() { return 42; }";
|
|
71
60
|
const result = transformer.transform(inputCode, "unknown");
|
|
72
61
|
console.log(result.code);
|
|
73
|
-
|
|
74
|
-
// Both the matcher and transformer should be freed after use!
|
|
75
|
-
matcher.free();
|
|
76
|
-
transformer.free();
|
|
77
62
|
```
|
|
78
63
|
|
|
79
64
|
### Export Aliases
|
|
@@ -114,23 +99,23 @@ type FunctionKind = "Sync" | "Async";
|
|
|
114
99
|
```ts
|
|
115
100
|
type FunctionQuery =
|
|
116
101
|
| // Match class constructor
|
|
117
|
-
{ className: string; index?: number; isExportAlias?: boolean }
|
|
102
|
+
{ className: string; index?: number | null; isExportAlias?: boolean }
|
|
118
103
|
| // Match class method
|
|
119
104
|
{
|
|
120
105
|
className: string;
|
|
121
106
|
methodName: string;
|
|
122
107
|
kind: FunctionKind;
|
|
123
|
-
index?: number;
|
|
108
|
+
index?: number | null;
|
|
124
109
|
isExportAlias?: boolean;
|
|
125
110
|
}
|
|
126
111
|
| // Match method on objects
|
|
127
|
-
{ methodName: string; kind: FunctionKind; index?: number }
|
|
112
|
+
{ methodName: string; kind: FunctionKind; index?: number | null }
|
|
128
113
|
| // Match standalone function
|
|
129
|
-
{ functionName: string; kind: FunctionKind; index?: number; isExportAlias?: boolean }
|
|
114
|
+
{ functionName: string; kind: FunctionKind; index?: number | null; isExportAlias?: boolean }
|
|
130
115
|
| // Match arrow function or function expression
|
|
131
|
-
{ expressionName: string; kind: FunctionKind; index?: number; isExportAlias?: boolean };
|
|
116
|
+
{ expressionName: string; kind: FunctionKind; index?: number | null; isExportAlias?: boolean };
|
|
132
117
|
| // Match private class methods
|
|
133
|
-
{ className: string; privateMethodName: string; kind: FunctionKind; index?: number };
|
|
118
|
+
{ className: string; privateMethodName: string; kind: FunctionKind; index?: number | null };
|
|
134
119
|
```
|
|
135
120
|
|
|
136
121
|
#### **`ModuleMatcher`**
|
|
@@ -139,7 +124,7 @@ type FunctionQuery =
|
|
|
139
124
|
type ModuleMatcher = {
|
|
140
125
|
name: string; // Module name
|
|
141
126
|
versionRange: string; // Matching semver range
|
|
142
|
-
filePath: string; //
|
|
127
|
+
filePath: string; // Relative Unix-style path to the file from the module root (e.g. "lib/index.js")
|
|
143
128
|
};
|
|
144
129
|
```
|
|
145
130
|
|
|
@@ -156,18 +141,18 @@ type InstrumentationConfig = {
|
|
|
156
141
|
### Functions
|
|
157
142
|
|
|
158
143
|
```ts
|
|
159
|
-
create(configs: InstrumentationConfig[],
|
|
144
|
+
create(configs: InstrumentationConfig[], dcModule?: string | null): InstrumentationMatcher;
|
|
160
145
|
```
|
|
161
146
|
|
|
162
147
|
Create a matcher for one or more instrumentation configurations.
|
|
163
148
|
|
|
164
149
|
- `configs` - Array of instrumentation configurations.
|
|
165
|
-
- `
|
|
150
|
+
- `dcModule` - Optional module to import `diagnostics_channel` API from.
|
|
166
151
|
|
|
167
152
|
#### **`InstrumentationMatcher`**
|
|
168
153
|
|
|
169
154
|
```ts
|
|
170
|
-
getTransformer(
|
|
155
|
+
getTransformer(moduleName: string, version: string, filePath: string): Transformer | undefined;
|
|
171
156
|
```
|
|
172
157
|
|
|
173
158
|
Gets a transformer for a specific module and file.
|
|
@@ -175,36 +160,24 @@ Gets a transformer for a specific module and file.
|
|
|
175
160
|
Returns a `Transformer` for the given module, or `undefined` if there were no
|
|
176
161
|
matching instrumentation configurations.
|
|
177
162
|
|
|
178
|
-
- `
|
|
163
|
+
- `moduleName` - Name of the module.
|
|
179
164
|
- `version` - Version of the module.
|
|
180
|
-
- `
|
|
181
|
-
|
|
182
|
-
```ts
|
|
183
|
-
free(): void;
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
Free the matcher memory when it's no longer needed.
|
|
165
|
+
- `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.
|
|
187
166
|
|
|
188
167
|
#### **`Transformer`**
|
|
189
168
|
|
|
190
169
|
```ts
|
|
191
|
-
transform(code: string,
|
|
170
|
+
transform(code: string | Buffer, moduleType: ModuleType, sourcemap?: string | undefined): TransformOutput;
|
|
192
171
|
```
|
|
193
172
|
|
|
194
173
|
Transforms the code, injecting tracing as configured.
|
|
195
174
|
|
|
196
175
|
Returns `{ code, map }`. `map` will be undefined if no sourcemap was supplied.
|
|
197
176
|
|
|
198
|
-
- `code` - The JavaScript
|
|
199
|
-
- `
|
|
177
|
+
- `code` - The JavaScript code to transform.
|
|
178
|
+
- `moduleType` - The type of module being transformed.
|
|
200
179
|
- `sourcemap` - Optional existing source map for the code.
|
|
201
180
|
|
|
202
|
-
```ts
|
|
203
|
-
free(): void;
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
Free the transformer memory when it's no longer needed.
|
|
207
|
-
|
|
208
181
|
## License
|
|
209
182
|
|
|
210
183
|
See LICENSE
|
package/index.d.ts
CHANGED
|
@@ -1,6 +1,114 @@
|
|
|
1
|
-
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
import type { Node } from 'estree';
|
|
2
4
|
/**
|
|
3
5
|
* Create a new instrumentation matcher from an array of instrumentation configs.
|
|
4
6
|
*/
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
+
export function create(configs: InstrumentationConfig[], dc_module?: string | null): InstrumentationMatcher;
|
|
8
|
+
/**
|
|
9
|
+
* Output of a transformation operation
|
|
10
|
+
*/
|
|
11
|
+
export interface TransformOutput {
|
|
12
|
+
/**
|
|
13
|
+
* The transformed JavaScript code
|
|
14
|
+
*/
|
|
15
|
+
code: string;
|
|
16
|
+
/**
|
|
17
|
+
* The sourcemap for the transformation (if generated)
|
|
18
|
+
*/
|
|
19
|
+
map: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The kind of function
|
|
24
|
+
*/
|
|
25
|
+
export type FunctionKind = "Sync" | "Async" | "Callback";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Describes which function to instrument
|
|
29
|
+
*/
|
|
30
|
+
export type FunctionQuery = { className: string; methodName: string; kind: FunctionKind; index?: number | null; isExportAlias?: boolean } | { className: string; privateMethodName: string; kind: FunctionKind; index?: number | null } | { className: string; index?: number | null; isExportAlias?: boolean } | { methodName: string; kind: FunctionKind; index?: number | null } | { functionName: string; kind: FunctionKind; index?: number | null; isExportAlias?: boolean } | { expressionName: string; kind: FunctionKind; index?: number | null; isExportAlias?: boolean };
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A custom transform function registered via `addTransform`.
|
|
34
|
+
* Receives the instrumentation state and the matched AST node.
|
|
35
|
+
*/
|
|
36
|
+
export type CustomTransform = (state: unknown, node: Node, parent: Node, ancestry: Node[]) => void;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Configuration for injecting instrumentation code
|
|
40
|
+
*/
|
|
41
|
+
export interface InstrumentationConfig {
|
|
42
|
+
/**
|
|
43
|
+
* The name of the diagnostics channel to publish to
|
|
44
|
+
*/
|
|
45
|
+
channelName: string;
|
|
46
|
+
/**
|
|
47
|
+
* The module matcher to identify the module and file to instrument
|
|
48
|
+
*/
|
|
49
|
+
module: ModuleMatcher;
|
|
50
|
+
/**
|
|
51
|
+
* The function query to identify the function to instrument
|
|
52
|
+
*/
|
|
53
|
+
functionQuery: FunctionQuery;
|
|
54
|
+
/**
|
|
55
|
+
* The name of a custom transform registered via `addTransform`.
|
|
56
|
+
* When set, takes precedence over `functionQuery.kind`.
|
|
57
|
+
*/
|
|
58
|
+
transform?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Describes the module and file path you would like to match
|
|
63
|
+
*/
|
|
64
|
+
export interface ModuleMatcher {
|
|
65
|
+
/**
|
|
66
|
+
* The name of the module you want to match
|
|
67
|
+
*/
|
|
68
|
+
name: string;
|
|
69
|
+
/**
|
|
70
|
+
* The semver range that you want to match
|
|
71
|
+
*/
|
|
72
|
+
versionRange: string;
|
|
73
|
+
/**
|
|
74
|
+
* The path of the file you want to match from the module root
|
|
75
|
+
*/
|
|
76
|
+
filePath: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The type of module being passed - ESM, CJS or unknown
|
|
81
|
+
*/
|
|
82
|
+
export type ModuleType = "esm" | "cjs" | "unknown";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The InstrumentationMatcher is responsible for matching specific modules
|
|
86
|
+
*/
|
|
87
|
+
export class InstrumentationMatcher {
|
|
88
|
+
private constructor();
|
|
89
|
+
free(): void;
|
|
90
|
+
/**
|
|
91
|
+
* Get a transformer for the given module name, version and file path.
|
|
92
|
+
* Returns `undefined` if no matching instrumentations are found.
|
|
93
|
+
*/
|
|
94
|
+
getTransformer(moduleName: string, version: string, filePath: string): Transformer | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* Register a custom transform function under the given name.
|
|
97
|
+
* The name can then be referenced via the `transform` option in an `InstrumentationConfig`.
|
|
98
|
+
*/
|
|
99
|
+
addTransform(name: string, fn: CustomTransform): void;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The Transformer is responsible for transforming JavaScript code.
|
|
103
|
+
*/
|
|
104
|
+
export class Transformer {
|
|
105
|
+
private constructor();
|
|
106
|
+
free(): void;
|
|
107
|
+
/**
|
|
108
|
+
* Transform JavaScript code and optionally sourcemap.
|
|
109
|
+
*
|
|
110
|
+
* # Errors
|
|
111
|
+
* Returns an error if the transformation fails to find injection points.
|
|
112
|
+
*/
|
|
113
|
+
transform(code: string | Buffer, moduleType: ModuleType, sourcemap?: string | null): TransformOutput;
|
|
114
|
+
}
|
package/index.js
CHANGED
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports
|
|
4
|
-
// ./pkg/orchestrion_js.js has a side effect of loading the wasm binary.
|
|
5
|
-
// We only want that if the library is actually used!
|
|
6
|
-
var cachedCreate;
|
|
7
|
-
/**
|
|
8
|
-
* Create a new instrumentation matcher from an array of instrumentation configs.
|
|
9
|
-
*/
|
|
10
|
-
function create(configs, dc_module) {
|
|
11
|
-
if (!cachedCreate) {
|
|
12
|
-
cachedCreate = require('./pkg/orchestrion_js.js').create;
|
|
13
|
-
}
|
|
14
|
-
if (cachedCreate === undefined) {
|
|
15
|
-
throw new Error("Failed to load '@apm-js-collab/code-transformer'");
|
|
16
|
-
}
|
|
17
|
-
return cachedCreate(configs, dc_module);
|
|
18
|
-
}
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
module.exports = require('./lib')
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { InstrumentationMatcher } = require('./matcher')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a new {@link InstrumentationMatcher} from the given instrumentation configs.
|
|
7
|
+
*
|
|
8
|
+
* @param {object[]} configs - Instrumentation configuration objects.
|
|
9
|
+
* @param {string} [dcModule] - The diagnostics_channel module specifier to use for imports.
|
|
10
|
+
* Deprecated: pass `dcModule` via each config's `dcModule` field instead.
|
|
11
|
+
* @returns {InstrumentationMatcher}
|
|
12
|
+
*/
|
|
13
|
+
function create (configs, dcModule) {
|
|
14
|
+
return new InstrumentationMatcher(configs, dcModule)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { create }
|
package/lib/matcher.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const semifies = require('semifies')
|
|
4
|
+
const { Transformer } = require('./transformer')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Matches instrumentation configs against a given module/file/version triple and
|
|
8
|
+
* returns a cached {@link Transformer} for matching configs.
|
|
9
|
+
*/
|
|
10
|
+
class InstrumentationMatcher {
|
|
11
|
+
#configs = []
|
|
12
|
+
#dcModule = null
|
|
13
|
+
#transformers = {}
|
|
14
|
+
#customTransforms = {}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {object[]} configs - Array of instrumentation configuration objects.
|
|
18
|
+
* @param {string} [dcModule] - The diagnostics_channel module specifier to inject.
|
|
19
|
+
* Defaults to `'diagnostics_channel'`.
|
|
20
|
+
*/
|
|
21
|
+
constructor (configs, dcModule) {
|
|
22
|
+
this.#configs = configs
|
|
23
|
+
this.#dcModule = dcModule || 'diagnostics_channel'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Releases all cached transformers, freeing any associated resources. */
|
|
27
|
+
free () {
|
|
28
|
+
this.#transformers = {}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Registers a custom transform function under the given operator name.
|
|
33
|
+
*
|
|
34
|
+
* Custom transforms override built-in ones when an instrumentation config
|
|
35
|
+
* specifies the same `transform` value.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} name - Operator name (e.g. `'traceSync'`).
|
|
38
|
+
* @param {Function} fn - Transform function `(state, node, parent, ancestry) => void`.
|
|
39
|
+
*/
|
|
40
|
+
addTransform (name, fn) {
|
|
41
|
+
this.#customTransforms[name] = fn
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns a {@link Transformer} for the given module/file/version, or `undefined`
|
|
46
|
+
* if no registered config matches.
|
|
47
|
+
*
|
|
48
|
+
* Results are cached by a `moduleName/filePath@version` key.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} moduleName - The npm package name (e.g. `'express'`).
|
|
51
|
+
* @param {string} version - The installed semver version string.
|
|
52
|
+
* @param {string} filePath - The relative file path within the package.
|
|
53
|
+
* @returns {import('./transformer').Transformer|undefined}
|
|
54
|
+
*/
|
|
55
|
+
getTransformer (moduleName, version, filePath) {
|
|
56
|
+
filePath = filePath.replace(/\\/g, '/')
|
|
57
|
+
|
|
58
|
+
const id = `${moduleName}/${filePath}@${version}`
|
|
59
|
+
|
|
60
|
+
if (this.#transformers[id]) return this.#transformers[id]
|
|
61
|
+
|
|
62
|
+
const configs = this.#configs.filter(({ module: mod }) =>
|
|
63
|
+
mod.name === moduleName &&
|
|
64
|
+
mod.filePath === filePath &&
|
|
65
|
+
semifies(version, mod.versionRange)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if (configs.length === 0) return
|
|
69
|
+
|
|
70
|
+
this.#transformers[id] = new Transformer(
|
|
71
|
+
moduleName,
|
|
72
|
+
version,
|
|
73
|
+
filePath,
|
|
74
|
+
configs,
|
|
75
|
+
this.#dcModule,
|
|
76
|
+
this.#customTransforms
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return this.#transformers[id]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { InstrumentationMatcher }
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const esquery = require('esquery')
|
|
4
|
+
const { parse } = require('meriyah')
|
|
5
|
+
const { generate } = require('astring')
|
|
6
|
+
const transforms = require('./transforms')
|
|
7
|
+
|
|
8
|
+
let SourceMapConsumer
|
|
9
|
+
let SourceMapGenerator
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Applies a set of instrumentation configs to JavaScript source code by parsing
|
|
13
|
+
* it into an AST, locating the target functions via esquery selectors, injecting
|
|
14
|
+
* diagnostics_channel tracing wrappers, and regenerating the source.
|
|
15
|
+
*/
|
|
16
|
+
class Transformer {
|
|
17
|
+
#moduleName = null
|
|
18
|
+
#version = null
|
|
19
|
+
#filePath = null
|
|
20
|
+
#configs = []
|
|
21
|
+
#dcModule = null
|
|
22
|
+
#customTransforms = {}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} moduleName - The npm package name being instrumented.
|
|
26
|
+
* @param {string} version - The installed semver version string.
|
|
27
|
+
* @param {string} filePath - The relative file path within the package.
|
|
28
|
+
* @param {object[]} configs - Instrumentation configuration objects for this file.
|
|
29
|
+
* @param {string} dcModule - The diagnostics_channel module specifier to inject.
|
|
30
|
+
* @param {Record<string, Function>} [customTransforms] - Optional custom operator overrides.
|
|
31
|
+
*/
|
|
32
|
+
constructor (moduleName, version, filePath, configs, dcModule, customTransforms = {}) {
|
|
33
|
+
this.#moduleName = moduleName // TODO: moduleName false for user module
|
|
34
|
+
this.#version = version
|
|
35
|
+
this.#filePath = filePath
|
|
36
|
+
this.#configs = configs
|
|
37
|
+
this.#dcModule = dcModule
|
|
38
|
+
this.#customTransforms = customTransforms
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** No-op — freeing resources is not needed for the JavaScript implementation. */
|
|
42
|
+
free () {}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Instruments `code` by injecting diagnostics_channel tracing around the
|
|
46
|
+
* target functions defined by this transformer's configs.
|
|
47
|
+
*
|
|
48
|
+
* @param {string|Buffer} code - Original JavaScript source, or a Buffer containing UTF-8 source.
|
|
49
|
+
* @param {'esm'|'cjs'|'unknown'} moduleType - Whether the source is an ES module or CommonJS.
|
|
50
|
+
* @param {string|object|null} [sourcemap] - Existing source map (raw string or object) to chain from.
|
|
51
|
+
* @returns {{ code: string, map?: string }}
|
|
52
|
+
* The transformed source and an optional updated source map.
|
|
53
|
+
* @throws {Error} If no injection points are found for any config.
|
|
54
|
+
*/
|
|
55
|
+
transform (code, moduleType, sourcemap) {
|
|
56
|
+
if (Buffer.isBuffer(code)) code = code.toString()
|
|
57
|
+
if (!code) return { code }
|
|
58
|
+
|
|
59
|
+
let ast
|
|
60
|
+
let aliases = {}
|
|
61
|
+
let injectionCount = 0
|
|
62
|
+
|
|
63
|
+
for (const config of this.#configs) {
|
|
64
|
+
const { astQuery, functionQuery = {} } = config
|
|
65
|
+
|
|
66
|
+
if (!ast) {
|
|
67
|
+
const options = {
|
|
68
|
+
loc: true,
|
|
69
|
+
ranges: true,
|
|
70
|
+
raw: true,
|
|
71
|
+
module: moduleType === 'esm',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
ast = parse(code, options)
|
|
76
|
+
} catch {
|
|
77
|
+
ast = parse(code, { ...options, module: !options.module })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (moduleType === 'esm') { // TODO: cjs
|
|
81
|
+
aliases = this.#collectExportAliases(ast)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const resolvedFunctionQuery = this.#resolveExportAlias(functionQuery, aliases)
|
|
86
|
+
const query = astQuery || this.#fromFunctionQuery(resolvedFunctionQuery)
|
|
87
|
+
const state = {
|
|
88
|
+
...config,
|
|
89
|
+
dcModule: this.#dcModule,
|
|
90
|
+
moduleType,
|
|
91
|
+
moduleVersion: this.#version,
|
|
92
|
+
functionQuery: resolvedFunctionQuery
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
state.operator = this.#getOperator(state)
|
|
96
|
+
|
|
97
|
+
esquery.traverse(ast, esquery.parse(query), (...args) => {
|
|
98
|
+
injectionCount++
|
|
99
|
+
this.#visit(state, ...args)
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (injectionCount === 0 && this.#configs.length > 0) {
|
|
104
|
+
const names = this.#configs.map(({ functionQuery = {} }) => {
|
|
105
|
+
const resolvedQuery = this.#resolveExportAlias(functionQuery, aliases)
|
|
106
|
+
const queryName = (q) => q.methodName || q.privateMethodName || q.functionName || q.expressionName || 'constructor'
|
|
107
|
+
const originalName = queryName(functionQuery)
|
|
108
|
+
const originalAlias = functionQuery.className || functionQuery.functionName || functionQuery.expressionName
|
|
109
|
+
const resolvedAlias = resolvedQuery.className || resolvedQuery.functionName || resolvedQuery.expressionName
|
|
110
|
+
if (originalAlias && originalAlias !== resolvedAlias) {
|
|
111
|
+
return `${originalAlias} (local name: ${resolvedAlias})`
|
|
112
|
+
}
|
|
113
|
+
return originalName
|
|
114
|
+
})
|
|
115
|
+
throw new Error(`Failed to find injection points for: ${JSON.stringify(names)}`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (ast) {
|
|
119
|
+
SourceMapConsumer ??= require('source-map').SourceMapConsumer
|
|
120
|
+
SourceMapGenerator ??= require('source-map').SourceMapGenerator
|
|
121
|
+
|
|
122
|
+
const file = `${this.#moduleName}/${this.#filePath}`
|
|
123
|
+
const sourceMapInput = sourcemap ? new SourceMapConsumer(sourcemap) : { file }
|
|
124
|
+
const sourceMap = new SourceMapGenerator(sourceMapInput)
|
|
125
|
+
const code = generate(ast, { sourceMap })
|
|
126
|
+
const map = sourceMap.toString()
|
|
127
|
+
|
|
128
|
+
return { code, map }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { code }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Visitor called for each AST node that matches a config's query.
|
|
136
|
+
* Handles index-based filtering and delegates to the appropriate transform.
|
|
137
|
+
*
|
|
138
|
+
* @param {object} state - Merged config + runtime state for this traversal.
|
|
139
|
+
* @param {...unknown} args - `(node, parent, ancestry)` from esquery traverse.
|
|
140
|
+
*/
|
|
141
|
+
#visit (state, ...args) {
|
|
142
|
+
const transform = this.#customTransforms[state.operator] ?? transforms[state.operator]
|
|
143
|
+
const { index = 0 } = state.functionQuery
|
|
144
|
+
const [node] = args
|
|
145
|
+
const type = node.init?.type || node.type
|
|
146
|
+
|
|
147
|
+
// Class nodes are visited for traceInstanceMethod (missing method patching),
|
|
148
|
+
// but when selecting by index we only want to count and match function nodes.
|
|
149
|
+
if (type !== 'ClassDeclaration' && type !== 'ClassExpression') {
|
|
150
|
+
state.functionIndex = ++state.functionIndex || 0
|
|
151
|
+
|
|
152
|
+
if (index !== null && index !== state.functionIndex) return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
transform(state, ...args)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Resolves the operator name (transform function key) for a config.
|
|
160
|
+
*
|
|
161
|
+
* If the config has an explicit `transform` name it is used directly;
|
|
162
|
+
* otherwise the operator is derived from the `kind` field of `functionQuery`.
|
|
163
|
+
*
|
|
164
|
+
* @param {{ transform?: string, functionQuery: { kind?: string } }} state
|
|
165
|
+
* @returns {string} Operator name, e.g. `'tracePromise'`.
|
|
166
|
+
*/
|
|
167
|
+
#getOperator ({ transform, functionQuery: { kind } }) {
|
|
168
|
+
if (transform) return transform
|
|
169
|
+
|
|
170
|
+
switch (kind) {
|
|
171
|
+
case 'Async': return 'tracePromise'
|
|
172
|
+
case 'Callback': return 'traceCallback'
|
|
173
|
+
case 'Sync': return 'traceSync'
|
|
174
|
+
default: return 'traceSync'
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Collects a map of exported name → local name from `export { local as exported }`
|
|
180
|
+
* declarations so that instrumentation configs that reference export names can be
|
|
181
|
+
* resolved to local identifiers.
|
|
182
|
+
*
|
|
183
|
+
* @param {import('estree').Program} ast
|
|
184
|
+
* @returns {Record<string, string>} Map of exported name to local name.
|
|
185
|
+
*/
|
|
186
|
+
#collectExportAliases (ast) {
|
|
187
|
+
const aliases = {}
|
|
188
|
+
for (const node of ast.body) {
|
|
189
|
+
if (node.type === 'ExportNamedDeclaration' && !node.source) {
|
|
190
|
+
for (const spec of node.specifiers) {
|
|
191
|
+
if (spec.exported && spec.local) {
|
|
192
|
+
const exportedName = spec.exported.name ?? spec.exported.value
|
|
193
|
+
const localName = spec.local.name ?? spec.local.value
|
|
194
|
+
if (exportedName && localName) {
|
|
195
|
+
aliases[exportedName] = localName
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return aliases
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* If `functionQuery.isExportAlias` is set, replaces the exported identifier in
|
|
206
|
+
* `functionQuery` with the corresponding local name from `aliases`.
|
|
207
|
+
*
|
|
208
|
+
* @param {object} functionQuery
|
|
209
|
+
* @param {Record<string, string>} aliases - Map produced by {@link #collectExportAliases}.
|
|
210
|
+
* @returns {object} Resolved function query (may be the original object if unchanged).
|
|
211
|
+
*/
|
|
212
|
+
#resolveExportAlias (functionQuery, aliases) {
|
|
213
|
+
if (!functionQuery.isExportAlias) return functionQuery
|
|
214
|
+
const { functionName, expressionName, className } = functionQuery
|
|
215
|
+
if (functionName && aliases[functionName]) {
|
|
216
|
+
return { ...functionQuery, functionName: aliases[functionName] }
|
|
217
|
+
}
|
|
218
|
+
if (expressionName && aliases[expressionName]) {
|
|
219
|
+
return { ...functionQuery, expressionName: aliases[expressionName] }
|
|
220
|
+
}
|
|
221
|
+
if (className && aliases[className]) {
|
|
222
|
+
return { ...functionQuery, className: aliases[className] }
|
|
223
|
+
}
|
|
224
|
+
return functionQuery
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Builds a comma-separated esquery selector string from a `functionQuery` descriptor.
|
|
229
|
+
*
|
|
230
|
+
* Handles class methods, standalone functions, and expression assignments, producing
|
|
231
|
+
* multiple selector alternatives joined with `, `.
|
|
232
|
+
*
|
|
233
|
+
* @param {object} functionQuery
|
|
234
|
+
* @param {string} [functionQuery.className]
|
|
235
|
+
* @param {string} [functionQuery.methodName]
|
|
236
|
+
* @param {string} [functionQuery.privateMethodName]
|
|
237
|
+
* @param {string} [functionQuery.functionName]
|
|
238
|
+
* @param {string} [functionQuery.expressionName]
|
|
239
|
+
* @returns {string} esquery selector.
|
|
240
|
+
*/
|
|
241
|
+
#fromFunctionQuery (functionQuery) {
|
|
242
|
+
const { functionName, expressionName, className } = functionQuery
|
|
243
|
+
const type = functionQuery.privateMethodName ? 'PrivateIdentifier' : 'Identifier'
|
|
244
|
+
const queries = []
|
|
245
|
+
|
|
246
|
+
let method = functionQuery.methodName || functionQuery.privateMethodName
|
|
247
|
+
|
|
248
|
+
if (className) {
|
|
249
|
+
method ??= 'constructor'
|
|
250
|
+
queries.push(
|
|
251
|
+
`[id.name="${className}"]`,
|
|
252
|
+
`[id.name="${className}"] > ClassExpression`,
|
|
253
|
+
`[id.name="${className}"] > ClassBody > [key.name="${method}"][key.type=${type}] > [async]`,
|
|
254
|
+
`[id.name="${className}"] > ClassExpression > ClassBody > [key.name="${method}"][key.type=${type}] > [async]`
|
|
255
|
+
)
|
|
256
|
+
} else if (method) {
|
|
257
|
+
queries.push(
|
|
258
|
+
`ClassBody > [key.name="${method}"][key.type=${type}] > [async]`,
|
|
259
|
+
`Property[key.name="${method}"][key.type=${type}] > [async]`
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (functionName) {
|
|
264
|
+
queries.push(`FunctionDeclaration[id.name="${functionName}"][async]`)
|
|
265
|
+
} else if (expressionName) {
|
|
266
|
+
queries.push(
|
|
267
|
+
`FunctionExpression[id.name="${expressionName}"][async]`,
|
|
268
|
+
`ArrowFunctionExpression[id.name="${expressionName}"][async]`,
|
|
269
|
+
`VariableDeclarator[id.name="${expressionName}"] > FunctionExpression[async]`,
|
|
270
|
+
`VariableDeclarator[id.name="${expressionName}"] > ArrowFunctionExpression[async]`,
|
|
271
|
+
`AssignmentExpression[left.property.name="${expressionName}"] > FunctionExpression[async]`,
|
|
272
|
+
`AssignmentExpression[left.property.name="${expressionName}"] > ArrowFunctionExpression[async]`,
|
|
273
|
+
`AssignmentExpression[left.name="${expressionName}"] > FunctionExpression[async]`,
|
|
274
|
+
`AssignmentExpression[left.name="${expressionName}"] > ArrowFunctionExpression[async]`
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return queries.join(', ')
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = { Transformer }
|