@contrast/rewriter 1.8.1 → 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 CHANGED
@@ -219,11 +219,64 @@ module.exports.Cache = class Cache {
219
219
  }
220
220
 
221
221
  /**
222
- * Writes a rewritten file to the cache directory. Runs asynchronously so that
223
- * disk I/O doesn't impact startup times, regardless of whether we're in a CJS
224
- * or ESM environment.
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
- // if we're trimming the output we're not rewriting an entire file, which
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 to be consumed by
150
- * ESM hooks.
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
- let result = await transform(content, this.rewriteConfig(opts));
136
+ const result = await transform(content, this.rewriteConfig(opts));
163
137
 
164
- if (opts.trim) {
165
- result = trim(content, 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 to be consumed by
173
- * CJS hooks.
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
- let result = transformSync(content, this.rewriteConfig(opts));
161
+ const result = transformSync(content, this.rewriteConfig(opts));
186
162
 
187
- if (opts.trim) {
188
- result = trim(content, 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.1",
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)",
@@ -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.1",
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"
@@ -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
- };