|
| 1 | +'use strict' |
| 2 | + |
| 3 | +/* |
| 4 | +This rewriter is basically a JavaScript version of Orchestrion-JS. The goal is |
| 5 | +not to replace Orchestrion-JS, but rather to make it easier and faster to write |
| 6 | +new integrations in the short-term, especially as many changes to the rewriter |
| 7 | +will be needed as all the patterns we need have not been identified yet. This |
| 8 | +will avoid the back and forth of having to make Rust changes to an external |
| 9 | +library for every integration change or addition that requires something new. |
| 10 | +
|
| 11 | +In the meantime, we'll work concurrently on a change to Orchestrion-JS that |
| 12 | +adds an "arbitrary transform" or "plugin" system that can be used from |
| 13 | +JavaScript, in order to enable quick iteration while still using Orchestrion-JS. |
| 14 | +Once that's done we'll use that, so that we can remove this JS approach and |
| 15 | +return to using Orchestrion-JS. |
| 16 | +
|
| 17 | +The long term goal is to backport any additional features we add to the JS |
| 18 | +rewriter (or using the plugin system in Orchestrion-JS once we're using that) |
| 19 | +to Orchestrion-JS once we're confident that the implementation is fairly |
| 20 | +complete and has all features we need. |
| 21 | +
|
| 22 | +Here is a list of the additions and changes in this rewriter compared to |
| 23 | +Orchestrion-JS that will need to be backported: |
| 24 | +
|
| 25 | +(NOTE: Please keep this list up-to-date whenever new features are added) |
| 26 | +
|
| 27 | +- Supports an `astQuery` field to filter AST nodes with an esquery query. This |
| 28 | + is mostly meant to be used when experimenting or if what needs to be queried |
| 29 | + is not a function. We'll see over time if something like this is needed to be |
| 30 | + backported or if it can be replaced by simpler queries. |
| 31 | +- Supports replacing methods of child class instances in the base constructor. |
| 32 | +*/ |
| 33 | + |
| 34 | +const { readFileSync } = require('fs') |
| 35 | +const { join } = require('path') |
| 36 | +const semifies = require('semifies') |
| 37 | +const transforms = require('./transforms') |
| 38 | +const { generate, parse, traverse } = require('./compiler') |
| 39 | +const log = require('../../../../dd-trace/src/log') |
| 40 | +const instrumentations = require('./instrumentations') |
| 41 | +const { getEnvironmentVariable } = require('../../../../dd-trace/src/config-helper') |
| 42 | + |
| 43 | +const NODE_OPTIONS = getEnvironmentVariable('NODE_OPTIONS') |
| 44 | + |
| 45 | +const supported = {} |
| 46 | +const disabled = new Set() |
| 47 | + |
| 48 | +// TODO: Source maps without `--enable-source-maps`. |
| 49 | +const enableSourceMaps = NODE_OPTIONS?.includes('--enable-source-maps') || |
| 50 | + process.execArgv?.some(arg => arg.includes('--enable-source-maps')) |
| 51 | + |
| 52 | +let SourceMapGenerator |
| 53 | + |
| 54 | +function rewrite (content, filename, format) { |
| 55 | + if (!content) return content |
| 56 | + |
| 57 | + try { |
| 58 | + let ast |
| 59 | + |
| 60 | + filename = filename.replace('file://', '') |
| 61 | + |
| 62 | + for (const inst of instrumentations) { |
| 63 | + const { astQuery, functionQuery = {}, module: { name, versionRange, filePath } } = inst |
| 64 | + const { kind } = functionQuery |
| 65 | + const operator = kind === 'Async' ? 'tracePromise' : kind === 'Callback' ? 'traceCallback' : 'traceSync' |
| 66 | + const transform = transforms[operator] |
| 67 | + |
| 68 | + if (disabled.has(name)) continue |
| 69 | + if (!filename.endsWith(`${name}/${filePath}`)) continue |
| 70 | + if (!transform) continue |
| 71 | + if (!satisfies(filename, filePath, versionRange)) continue |
| 72 | + |
| 73 | + ast ??= parse(content.toString(), { loc: true, ranges: true, module: format === 'module' }) |
| 74 | + |
| 75 | + const query = astQuery || fromFunctionQuery(functionQuery) |
| 76 | + const state = { ...inst, format, functionQuery, operator } |
| 77 | + |
| 78 | + traverse(ast, query, (...args) => transform(state, ...args)) |
| 79 | + } |
| 80 | + |
| 81 | + if (ast) { |
| 82 | + if (!enableSourceMaps) return generate(ast) |
| 83 | + |
| 84 | + // TODO: Can we use the same version of `source-map` that DI uses? |
| 85 | + SourceMapGenerator ??= require('@datadog/source-map').SourceMapGenerator |
| 86 | + |
| 87 | + const sourceMap = new SourceMapGenerator({ file: filename }) |
| 88 | + const code = generate(ast, { sourceMap }) |
| 89 | + const map = Buffer.from(sourceMap.toString()).toString('base64') |
| 90 | + |
| 91 | + return code + '\n' + `//# sourceMappingURL=data:application/json;base64,${map}` |
| 92 | + } |
| 93 | + } catch (e) { |
| 94 | + log.error(e) |
| 95 | + } |
| 96 | + |
| 97 | + return content |
| 98 | +} |
| 99 | + |
| 100 | +function disable (instrumentation) { |
| 101 | + disabled.add(instrumentation) |
| 102 | +} |
| 103 | + |
| 104 | +function satisfies (filename, filePath, versions) { |
| 105 | + const [basename] = filename.split(filePath) |
| 106 | + |
| 107 | + if (supported[basename] === undefined) { |
| 108 | + try { |
| 109 | + const pkg = JSON.parse(readFileSync( |
| 110 | + join(basename, 'package.json'), 'utf8' |
| 111 | + )) |
| 112 | + |
| 113 | + supported[basename] = semifies(pkg.version, versions) |
| 114 | + } catch { |
| 115 | + supported[basename] = false |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + return supported[basename] |
| 120 | +} |
| 121 | + |
| 122 | +// TODO: Support index |
| 123 | +function fromFunctionQuery (functionQuery) { |
| 124 | + const { methodName, functionName, expressionName, className } = functionQuery |
| 125 | + const queries = [] |
| 126 | + |
| 127 | + if (className) { |
| 128 | + queries.push( |
| 129 | + `[id.name="${className}"]`, |
| 130 | + `[id.name="${className}"] > ClassBody > [key.name="${methodName}"] > [async]`, |
| 131 | + `[id.name="${className}"] > ClassExpression > ClassBody > [key.name="${methodName}"] > [async]` |
| 132 | + ) |
| 133 | + } else if (methodName) { |
| 134 | + queries.push( |
| 135 | + `ClassBody > [key.name="${methodName}"] > [async]`, |
| 136 | + `Property[key.name="${methodName}"] > [async]` |
| 137 | + ) |
| 138 | + } |
| 139 | + |
| 140 | + if (functionName) { |
| 141 | + queries.push(`FunctionDeclaration[id.name="${functionName}"][async]`) |
| 142 | + } else if (expressionName) { |
| 143 | + queries.push( |
| 144 | + `FunctionExpression[id.name="${expressionName}"][async]`, |
| 145 | + `ArrowFunctionExpression[id.name="${expressionName}"][async]` |
| 146 | + ) |
| 147 | + } |
| 148 | + |
| 149 | + return queries.join(', ') |
| 150 | +} |
| 151 | + |
| 152 | +module.exports = { rewrite, disable } |
0 commit comments