@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 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 SWC's Rust AST walker to inject code that calls Node.js
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 Rust library as a WebAssembly
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; // Path to the file from the module root
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[], dc_module?: string | null): InstrumentationMatcher;
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
- - `dc_module` - Optional module to import `diagnostics_channel` API from.
150
+ - `dcModule` - Optional module to import `diagnostics_channel` API from.
166
151
 
167
152
  #### **`InstrumentationMatcher`**
168
153
 
169
154
  ```ts
170
- getTransformer(module_name: string, version: string, file_path: string): Transformer | undefined;
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
- - `module_name` - Name of the module.
163
+ - `moduleName` - Name of the module.
179
164
  - `version` - Version of the module.
180
- - `file_path` - Path to the file from the module root.
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, module_type: ModuleType, sourcemap?: string | undefined): TransformOutput;
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/TypeScript code to transform.
199
- - `module_type` - The type of module being transformed.
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
- import type { InstrumentationConfig, InstrumentationMatcher } from './pkg/orchestrion_js';
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 declare function create(configs: InstrumentationConfig[], dc_module?: string | null): InstrumentationMatcher;
6
- export type { FunctionKind, FunctionQuery, InstrumentationConfig, InstrumentationMatcher, ModuleMatcher, ModuleType, TransformOutput, Transformer, } from './pkg/orchestrion_js';
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
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.create = create;
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 }