@contrast/rewriter 1.8.0 → 1.8.2
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/lib/cache.js +57 -4
- package/lib/index.js +80 -44
- package/package.json +3 -3
- package/lib/source-maps.js +0 -49
package/lib/cache.js
CHANGED
|
@@ -44,7 +44,7 @@ module.exports.Cache = class Cache {
|
|
|
44
44
|
this.logger = core.logger.child({ name: 'contrast:rewriter:cache' });
|
|
45
45
|
/** @type {Set<import('.').Mode>} */
|
|
46
46
|
this.modes = new Set();
|
|
47
|
-
this.appDirRegex = new RegExp(`^${core.appInfo.app_dir.
|
|
47
|
+
this.appDirRegex = new RegExp(`^${core.appInfo.app_dir.replaceAll('\\', '\\\\')}`);
|
|
48
48
|
this.cacheDir = path.join(
|
|
49
49
|
core.config.agent.node.rewrite.cache.path,
|
|
50
50
|
core.appInfo.name.replace('/', '_'),
|
|
@@ -219,11 +219,64 @@ module.exports.Cache = class Cache {
|
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
/**
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
222
|
+
* Synchronously writes a rewritten file to the cache directory. This is
|
|
223
|
+
* intended for use by require instrumentation because require is a sync
|
|
224
|
+
* operation.
|
|
225
|
+
*
|
|
226
|
+
* Incorrectly using the .write() method for require can result in the
|
|
227
|
+
* "unexpected end-of-file" error or rewriting the same file multiple
|
|
228
|
+
* times because it's required again before the write operation has
|
|
229
|
+
* completed.
|
|
230
|
+
*
|
|
225
231
|
* @param {string} filename
|
|
226
232
|
* @param {import('@swc/core').Output} result
|
|
233
|
+
* @returns {void}
|
|
234
|
+
*/
|
|
235
|
+
writeSync(filename, result) {
|
|
236
|
+
const filenameCached = this.getCachedFilename(filename);
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
fs.mkdirSync(path.dirname(filenameCached), { recursive: true });
|
|
240
|
+
|
|
241
|
+
fs.writeFileSync(filenameCached, result.code, 'utf8');
|
|
242
|
+
|
|
243
|
+
if (result.map) {
|
|
244
|
+
fs.writeFileSync(`${filenameCached}.map`, result.map, 'utf8');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
this.logger.trace(
|
|
248
|
+
{
|
|
249
|
+
filename,
|
|
250
|
+
filenameCached,
|
|
251
|
+
},
|
|
252
|
+
'Cache entry created.'
|
|
253
|
+
);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
this.logger.warn(
|
|
256
|
+
{
|
|
257
|
+
err,
|
|
258
|
+
filename,
|
|
259
|
+
filenameCached,
|
|
260
|
+
},
|
|
261
|
+
'Unable to cache rewrite results.'
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Asynchronously writes a rewritten file to the cache directory. This is
|
|
268
|
+
* intended for use by import instrumentation because import is an async
|
|
269
|
+
* operation.
|
|
270
|
+
*
|
|
271
|
+
* The caller should await this method to ensure that the cache is written
|
|
272
|
+
* before proceeding. If the caller doesn't wait, it's possible that the
|
|
273
|
+
* code will attempt to read a half-written file and get an "unexpected
|
|
274
|
+
* end-of-file" error or that the same file will be rewritten because it's
|
|
275
|
+
* required again before the file appears in the file system.
|
|
276
|
+
*
|
|
277
|
+
* @param {string} filename
|
|
278
|
+
* @param {import('@swc/core').Output} result
|
|
279
|
+
* @returns {Promise<void>}
|
|
227
280
|
*/
|
|
228
281
|
async write(filename, result) {
|
|
229
282
|
const filenameCached = this.getCachedFilename(filename);
|
package/lib/index.js
CHANGED
|
@@ -15,8 +15,11 @@
|
|
|
15
15
|
// @ts-check
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const { transform, transformSync } = require('@swc/core');
|
|
19
18
|
const Module = require('node:module');
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const fsp = fs.promises;
|
|
21
|
+
const { transfer } = require('multi-stage-sourcemap');
|
|
22
|
+
const { transform, transformSync } = require('@swc/core');
|
|
20
23
|
const { Cache } = require('./cache');
|
|
21
24
|
|
|
22
25
|
/**
|
|
@@ -34,7 +37,6 @@ const { Cache } = require('./cache');
|
|
|
34
37
|
* @prop {boolean=} isModule if true, file is parsed as an ES module instead of a CJS script
|
|
35
38
|
* @prop {boolean=} inject if true, injects ContrastMethods on the global object
|
|
36
39
|
* @prop {boolean=} wrap if true, wraps the content with a modified module wrapper IIFE
|
|
37
|
-
* @prop {boolean=} trim if true, removes added characters from the end of the generated code
|
|
38
40
|
*/
|
|
39
41
|
|
|
40
42
|
// @ts-expect-error `wrapper` is missing from @types/node
|
|
@@ -68,33 +70,6 @@ const wrap = (content) => {
|
|
|
68
70
|
return content;
|
|
69
71
|
};
|
|
70
72
|
|
|
71
|
-
/**
|
|
72
|
-
* Trims extraneous characters that may have been added by the rewriter.
|
|
73
|
-
* Handles newline or semicolon insertion, removing the added characters if they
|
|
74
|
-
* were not present in the original source content.
|
|
75
|
-
* @param {string} content
|
|
76
|
-
* @param {import('@swc/core').Output} result
|
|
77
|
-
* @returns {import('@swc/core').Output}
|
|
78
|
-
*/
|
|
79
|
-
const trim = (content, result) => {
|
|
80
|
-
let carriageReturn = 0;
|
|
81
|
-
// swc always adds a newline, so we only need to check the input
|
|
82
|
-
if (!content.endsWith('\n')) {
|
|
83
|
-
result.code = result.code.substring(0, result.code.length - 1);
|
|
84
|
-
} else if (content.endsWith('\r\n')) {
|
|
85
|
-
// if EOL is \r\n, then we need to account for that when we check the
|
|
86
|
-
// negative index of the last semicolon below
|
|
87
|
-
carriageReturn = 1;
|
|
88
|
-
}
|
|
89
|
-
const resultSemicolonIdx = result.code.lastIndexOf(';');
|
|
90
|
-
const contentSemicolonIdx = content.lastIndexOf(';');
|
|
91
|
-
if (contentSemicolonIdx === -1 || resultSemicolonIdx - result.code.length !== contentSemicolonIdx - content.length + carriageReturn) {
|
|
92
|
-
result.code = result.code.substring(0, resultSemicolonIdx) + result.code.substring(resultSemicolonIdx + 1, result.code.length);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return result;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
73
|
class Rewriter {
|
|
99
74
|
/**
|
|
100
75
|
* @param {Core} core
|
|
@@ -139,18 +114,17 @@ class Rewriter {
|
|
|
139
114
|
}]],
|
|
140
115
|
},
|
|
141
116
|
},
|
|
142
|
-
|
|
143
|
-
// means source maps are not relevant.
|
|
144
|
-
sourceMaps: !opts.trim && this.core.config.agent.node.source_maps.enable,
|
|
117
|
+
sourceMaps: this.core.config.agent.node.source_maps.enable,
|
|
145
118
|
};
|
|
146
119
|
}
|
|
147
120
|
|
|
148
121
|
/**
|
|
149
|
-
* Rewrites the provided source code string asynchronously
|
|
150
|
-
*
|
|
122
|
+
* Rewrites the provided source code string asynchronously. this is used in an ESM
|
|
123
|
+
* context. CJS cannot use this because `require` is synchronous.
|
|
124
|
+
*
|
|
151
125
|
* @param {string} content
|
|
152
126
|
* @param {RewriteOpts=} opts
|
|
153
|
-
* @returns {Promise<import('@swc/core').Output>}
|
|
127
|
+
* @returns {Promise<import('@swc/core').Output>} with possibly modified source map.
|
|
154
128
|
*/
|
|
155
129
|
async rewrite(content, opts = {}) {
|
|
156
130
|
this.logger.trace({ opts }, 'rewriting %s', opts.filename);
|
|
@@ -159,21 +133,23 @@ class Rewriter {
|
|
|
159
133
|
content = wrap(content);
|
|
160
134
|
}
|
|
161
135
|
|
|
162
|
-
|
|
136
|
+
const result = await transform(content, this.rewriteConfig(opts));
|
|
163
137
|
|
|
164
|
-
if (
|
|
165
|
-
result =
|
|
138
|
+
if (result.map) {
|
|
139
|
+
result.map = await this.ifSourceMapExistsChainIt(`${opts.filename}.map`, result.map);
|
|
166
140
|
}
|
|
167
141
|
|
|
168
142
|
return result;
|
|
169
143
|
}
|
|
170
144
|
|
|
171
145
|
/**
|
|
172
|
-
* Rewrites the provided source code string synchronously
|
|
173
|
-
*
|
|
146
|
+
* Rewrites the provided source code string synchronously. this is used in a CJS
|
|
147
|
+
* context. while ESM could use this, performance is better when using the async
|
|
148
|
+
* version.
|
|
149
|
+
*
|
|
174
150
|
* @param {string} content
|
|
175
151
|
* @param {RewriteOpts=} opts
|
|
176
|
-
* @returns {import('@swc/core').Output}
|
|
152
|
+
* @returns {import('@swc/core').Output} with possibly modified source map.
|
|
177
153
|
*/
|
|
178
154
|
rewriteSync(content, opts = {}) {
|
|
179
155
|
this.logger.trace({ opts }, 'rewriting %s', opts.filename);
|
|
@@ -182,10 +158,10 @@ class Rewriter {
|
|
|
182
158
|
content = wrap(content);
|
|
183
159
|
}
|
|
184
160
|
|
|
185
|
-
|
|
161
|
+
const result = transformSync(content, this.rewriteConfig(opts));
|
|
186
162
|
|
|
187
|
-
if (
|
|
188
|
-
result =
|
|
163
|
+
if (result.map) {
|
|
164
|
+
result.map = this.ifSourceMapExistsChainItSync(`${opts.filename}.map`, result.map);
|
|
189
165
|
}
|
|
190
166
|
|
|
191
167
|
return result;
|
|
@@ -211,6 +187,66 @@ class Rewriter {
|
|
|
211
187
|
sourceMaps: false,
|
|
212
188
|
}).code;
|
|
213
189
|
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* If there is a .map file in the same directory as the code being rewritten
|
|
194
|
+
* then chain the two maps together. This is an async function because there
|
|
195
|
+
* is no reason to wait for the source map to be finalized at startup. node-mono
|
|
196
|
+
* writes the map file asynchronously but performs two synchronous IO reads
|
|
197
|
+
* before calling transfer. This code performs a single async read before
|
|
198
|
+
* calling transfer.
|
|
199
|
+
*
|
|
200
|
+
* Question: should this log or just defer to the caller?
|
|
201
|
+
*
|
|
202
|
+
* @param {string} possibleMapPath the absolute path to a possibly pre-existing source map.
|
|
203
|
+
* @param {string} contrastMap the source map generated by the agent
|
|
204
|
+
* @returns {Promise<string>} promise to the final sourceMap object or, if an error,
|
|
205
|
+
* the input contrast source-map.
|
|
206
|
+
*/
|
|
207
|
+
// @ts-ignore
|
|
208
|
+
async ifSourceMapExistsChainIt(possibleMapPath, contrastMap) {
|
|
209
|
+
try {
|
|
210
|
+
const data = await fsp.readFile(possibleMapPath, 'utf8');
|
|
211
|
+
const existingMap = JSON.parse(data);
|
|
212
|
+
contrastMap = transfer({ fromSourceMap: contrastMap, toSourceMap: existingMap });
|
|
213
|
+
this.logger.trace({ existingMap: possibleMapPath }, 'merged source-map');
|
|
214
|
+
} catch (err) {
|
|
215
|
+
// if the map file isn't found, it's not an error, otherwise log it.
|
|
216
|
+
// @ts-ignore
|
|
217
|
+
if (err.code !== 'ENOENT') {
|
|
218
|
+
this.logger.debug({ existingMap: possibleMapPath, err }, 'failed to read');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// return the merged map or the original contrast map
|
|
223
|
+
return contrastMap;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @param {string} possibleMapPath the absolute path to a possibly pre-existing source map.
|
|
228
|
+
* @param {string} contrastMap the source map generated by the agent
|
|
229
|
+
* @returns {string} the final sourceMap object or, if an error,
|
|
230
|
+
* the input contrast source-map.
|
|
231
|
+
*/
|
|
232
|
+
// @ts-ignore
|
|
233
|
+
ifSourceMapExistsChainItSync(possibleMapPath, contrastMap) {
|
|
234
|
+
try {
|
|
235
|
+
const data = fs.readFileSync(possibleMapPath, 'utf8');
|
|
236
|
+
const existingMap = JSON.parse(data);
|
|
237
|
+
contrastMap = transfer({ fromSourceMap: contrastMap, toSourceMap: existingMap });
|
|
238
|
+
this.logger.trace({ existingMap: possibleMapPath }, 'merged source-map');
|
|
239
|
+
} catch (err) {
|
|
240
|
+
// if the map file isn't found, it's not an error, otherwise log it.
|
|
241
|
+
// @ts-ignore
|
|
242
|
+
if (err.code !== 'ENOENT') {
|
|
243
|
+
this.logger.debug({ existingMap: possibleMapPath, err }, 'failed to read');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// return the merged map or the original contrast map
|
|
248
|
+
return contrastMap;
|
|
249
|
+
}
|
|
214
250
|
}
|
|
215
251
|
|
|
216
252
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/rewriter",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.2",
|
|
4
4
|
"description": "A transpilation tool mainly used for instrumentation",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"types": "types/index.d.ts",
|
|
9
9
|
"engines": {
|
|
10
10
|
"npm": ">=6.13.7 <7 || >= 8.3.1",
|
|
11
|
-
"node": ">=
|
|
11
|
+
"node": ">= 16.9.1"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
14
|
"test": "../scripts/test.sh"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@contrast/agent-swc-plugin": "1.5.0",
|
|
18
18
|
"@contrast/agent-swc-plugin-unwrite": "1.5.0",
|
|
19
|
-
"@contrast/common": "1.21.
|
|
19
|
+
"@contrast/common": "1.21.2",
|
|
20
20
|
"@contrast/synchronous-source-maps": "^1.1.3",
|
|
21
21
|
"@swc/core": "1.3.39",
|
|
22
22
|
"multi-stage-sourcemap": "^0.3.1"
|
package/lib/source-maps.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright: 2024 Contrast Security, Inc
|
|
3
|
-
* Contact: support@contrastsecurity.com
|
|
4
|
-
* License: Commercial
|
|
5
|
-
|
|
6
|
-
* NOTICE: This Software and the patented inventions embodied within may only be
|
|
7
|
-
* used as part of Contrast Security’s commercial offerings. Even though it is
|
|
8
|
-
* made available through public repositories, use of this Software is subject to
|
|
9
|
-
* the applicable End User Licensing Agreement found at
|
|
10
|
-
* https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
|
|
11
|
-
* between Contrast Security and the End User. The Software may not be reverse
|
|
12
|
-
* engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
|
-
* way not consistent with the End User License Agreement.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
'use strict';
|
|
17
|
-
|
|
18
|
-
const { readFileSync, existsSync } = require('fs');
|
|
19
|
-
const { transfer } = require('multi-stage-sourcemap');
|
|
20
|
-
const { SourceMapConsumer } = require('@contrast/synchronous-source-maps');
|
|
21
|
-
|
|
22
|
-
module.exports = function (deps) {
|
|
23
|
-
const sourceMaps = deps.rewriter.sourceMaps = {};
|
|
24
|
-
const consumerCache = sourceMaps.consumerCache = {};
|
|
25
|
-
|
|
26
|
-
sourceMaps.cacheConsumerMap = function (filename, map) {
|
|
27
|
-
consumerCache[filename] = new SourceMapConsumer(map);
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
*/
|
|
32
|
-
sourceMaps.chain = function (filename, map) {
|
|
33
|
-
let ret;
|
|
34
|
-
|
|
35
|
-
if (existsSync(`${filename}.map`)) {
|
|
36
|
-
try {
|
|
37
|
-
const existingMap = JSON.parse(readFileSync(`${filename}.map`, 'utf8'));
|
|
38
|
-
ret = transfer({ fromSourceMap: map, toSourceMap: existingMap });
|
|
39
|
-
deps.logger.trace('Merged sourcemap from %s.map', filename);
|
|
40
|
-
} catch (err) {
|
|
41
|
-
deps.logger.debug({ err }, 'Unable to read %s.map.js', filename);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return ret;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
return sourceMaps;
|
|
49
|
-
};
|