@contrast/rewriter 1.4.1 → 1.5.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/LICENSE +1 -1
- package/README.md +2 -29
- package/lib/cache.js +263 -0
- package/lib/index.js +164 -70
- package/lib/source-maps.js +7 -8
- package/package.json +6 -8
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -2,32 +2,5 @@
|
|
|
2
2
|
|
|
3
3
|
Rewrite javascript code with custom rewrite transforms.
|
|
4
4
|
|
|
5
|
-
For example, Assess will register transforms for `+` -> `
|
|
6
|
-
via instrumentation of `
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
#### Example Service Usage
|
|
10
|
-
|
|
11
|
-
```typescript
|
|
12
|
-
const { Rewriter } = require('.');
|
|
13
|
-
|
|
14
|
-
const rewriter = new Rewriter({ logger });
|
|
15
|
-
|
|
16
|
-
rewriter.addTransforms({
|
|
17
|
-
BinaryExpression(path) {
|
|
18
|
-
const method = methodLookups[path.node.operator];
|
|
19
|
-
if (method) {
|
|
20
|
-
path.replaceWith(
|
|
21
|
-
t.callExpression(
|
|
22
|
-
expression('ContrastMethods.%%method%%')({ method }), [
|
|
23
|
-
path.node.left,
|
|
24
|
-
path.node.right
|
|
25
|
-
]
|
|
26
|
-
)
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const result = rewriter.rewrite('function add(x, y) { return x + y; }');
|
|
33
|
-
```
|
|
5
|
+
For example, Assess will register transforms for `+` -> `ContrastMethods.add()`
|
|
6
|
+
so that it can perform propagation via instrumentation of `ContrastMethods.add()`.
|
package/lib/cache.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
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
|
+
// @ts-check
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('node:fs');
|
|
19
|
+
const fsPromises = require('node:fs/promises');
|
|
20
|
+
const os = require('node:os');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns the modification time of a file as a number.
|
|
25
|
+
* @param {string} filename
|
|
26
|
+
* @returns {Promise<number>}
|
|
27
|
+
*/
|
|
28
|
+
const mtime = async (filename) => +(await fsPromises.stat(filename)).mtime;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns the modification time of a file as a number.
|
|
32
|
+
* @param {string} filename
|
|
33
|
+
* @returns {number}
|
|
34
|
+
*/
|
|
35
|
+
const mtimeSync = (filename) => +fs.statSync(filename).mtime;
|
|
36
|
+
|
|
37
|
+
module.exports.Cache = class Cache {
|
|
38
|
+
/**
|
|
39
|
+
* @param {import('.').Core} core
|
|
40
|
+
*/
|
|
41
|
+
constructor(core) {
|
|
42
|
+
this.config = core.config;
|
|
43
|
+
this.logger = core.logger.child({ name: 'contrast:rewriter:cache' });
|
|
44
|
+
/** @type {Set<import('.').Mode>} */
|
|
45
|
+
this.modes = new Set();
|
|
46
|
+
this.appDirRegex = new RegExp(`^${core.appInfo.app_dir.replace(/\\/g, '\\\\')}`);
|
|
47
|
+
this.cacheDir = path.join(
|
|
48
|
+
core.config.agent.node.rewrite.cache.path,
|
|
49
|
+
core.appInfo.name.replace('/', '_'),
|
|
50
|
+
core.agentVersion,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Sets the rewriter to 'assess' or 'protect' mode, enabling different
|
|
56
|
+
* transforms.
|
|
57
|
+
* @param {import('.').Mode} mode
|
|
58
|
+
*/
|
|
59
|
+
install(mode) {
|
|
60
|
+
this.modes.add(mode);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Returns the filename of a cached rewrite result. Paths within the `app_dir`
|
|
65
|
+
* directory are nested under the `_` directory to prevent potential
|
|
66
|
+
* collisions with absolute paths.
|
|
67
|
+
* /path/to/app/node_modules/mod/index.js
|
|
68
|
+
* -> '.contrast/app_name/5.1.2/assess/_/node_modules/mod/index.js
|
|
69
|
+
* /somewhere/else/index.js
|
|
70
|
+
* -> .contrast/app_name/5.1.2/assess/somewhere/else/index.js
|
|
71
|
+
* @param {string} filename
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
getCachedFilename(filename) {
|
|
75
|
+
filename = filename.replace(this.appDirRegex, '_');
|
|
76
|
+
|
|
77
|
+
if (os.platform() === 'win32') {
|
|
78
|
+
filename = filename.replace(/^([A-Za-z]):/, '$1_');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return path.join(
|
|
82
|
+
this.cacheDir,
|
|
83
|
+
this.modes.has('assess') ? 'assess' : 'protect',
|
|
84
|
+
filename,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Looks up and returns the string content of a previously rewritten file
|
|
90
|
+
* asynchronously.
|
|
91
|
+
* @param {string} filename
|
|
92
|
+
* @returns {Promise<string | undefined>}
|
|
93
|
+
*/
|
|
94
|
+
async read(filename) {
|
|
95
|
+
const filenameCached = this.getCachedFilename(filename);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const [time, timeCached] = await Promise.all([mtime(filename), mtime(filenameCached)]);
|
|
99
|
+
if (time > timeCached) {
|
|
100
|
+
this.logger.trace(
|
|
101
|
+
{
|
|
102
|
+
filename,
|
|
103
|
+
filenameCached,
|
|
104
|
+
mtime: time,
|
|
105
|
+
mtimeCached: timeCached,
|
|
106
|
+
},
|
|
107
|
+
'Cache stale, falling back to compiling.'
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.logger.trace(
|
|
114
|
+
{
|
|
115
|
+
filename,
|
|
116
|
+
filenameCached,
|
|
117
|
+
mtime: time,
|
|
118
|
+
mtimeCached: timeCached,
|
|
119
|
+
},
|
|
120
|
+
'Cache current.'
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return fsPromises.readFile(filenameCached, 'utf8');
|
|
124
|
+
} catch (err) {
|
|
125
|
+
// @ts-expect-error ts treats errors poorly.
|
|
126
|
+
if (err.code !== 'ENOENT') {
|
|
127
|
+
this.logger.error(
|
|
128
|
+
{ err, filename, filenameCached },
|
|
129
|
+
'An unexpected error occurred, falling back to compiling.'
|
|
130
|
+
);
|
|
131
|
+
} else {
|
|
132
|
+
this.logger.trace(
|
|
133
|
+
{ filename, filenameCached },
|
|
134
|
+
'Cache miss, falling back to compiling.',
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Looks up and returns the source map for a previously rewritten file.
|
|
144
|
+
* @param {string} filename
|
|
145
|
+
* @returns {Promise<string | undefined>}
|
|
146
|
+
*/
|
|
147
|
+
async readMap(filename) {
|
|
148
|
+
const filenameCached = this.getCachedFilename(filename);
|
|
149
|
+
const sourceMap = `${filenameCached}.map`;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
return fsPromises.readFile(sourceMap, 'utf8');
|
|
153
|
+
} catch (err) {
|
|
154
|
+
// @ts-expect-error ts treats errors poorly.
|
|
155
|
+
if (err.code !== 'ENOENT') {
|
|
156
|
+
this.logger.warn(
|
|
157
|
+
{ err, filename, filenameCached, sourceMap },
|
|
158
|
+
'An unexpected error occurred finding source map.'
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Looks up and returns the string content of a previously rewritten file
|
|
167
|
+
* synchronously. Used when we need to block on cache lookups.
|
|
168
|
+
* @param {string} filename
|
|
169
|
+
* @returns {string | undefined}
|
|
170
|
+
*/
|
|
171
|
+
readSync(filename) {
|
|
172
|
+
const filenameCached = this.getCachedFilename(filename);
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const time = mtimeSync(filename);
|
|
176
|
+
const timeCached = mtimeSync(filenameCached);
|
|
177
|
+
if (time > timeCached) {
|
|
178
|
+
this.logger.trace(
|
|
179
|
+
{
|
|
180
|
+
filename,
|
|
181
|
+
filenameCached,
|
|
182
|
+
mtime: time,
|
|
183
|
+
mtimeCached: timeCached,
|
|
184
|
+
},
|
|
185
|
+
'Cache stale, falling back to compiling.'
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.logger.trace(
|
|
192
|
+
{
|
|
193
|
+
filename,
|
|
194
|
+
filenameCached,
|
|
195
|
+
mtime: time,
|
|
196
|
+
mtimeCached: timeCached,
|
|
197
|
+
},
|
|
198
|
+
'Cache current.'
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return fs.readFileSync(filenameCached, 'utf8');
|
|
202
|
+
} catch (err) {
|
|
203
|
+
// @ts-expect-error ts treats errors poorly.
|
|
204
|
+
if (err.code !== 'ENOENT') {
|
|
205
|
+
this.logger.error(
|
|
206
|
+
{ err, filename, filenameCached },
|
|
207
|
+
'An unexpected error occurred, falling back to compiling.'
|
|
208
|
+
);
|
|
209
|
+
} else {
|
|
210
|
+
this.logger.trace(
|
|
211
|
+
{ filename, filenameCached },
|
|
212
|
+
'Cache miss, falling back to compiling.',
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Writes a rewritten file to the cache directory. Runs asynchronously so that
|
|
222
|
+
* disk I/O doesn't impact startup times, regardless of whether we're in a CJS
|
|
223
|
+
* or ESM environment.
|
|
224
|
+
* @param {string} filename
|
|
225
|
+
* @param {import('@swc/core').Output} result
|
|
226
|
+
*/
|
|
227
|
+
async write(filename, result) {
|
|
228
|
+
const filenameCached = this.getCachedFilename(filename);
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
await fsPromises.mkdir(path.dirname(filenameCached), { recursive: true });
|
|
232
|
+
|
|
233
|
+
const writePromises = [
|
|
234
|
+
fsPromises.writeFile(filenameCached, result.code, 'utf8')
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
if (result.map) {
|
|
238
|
+
writePromises.push(
|
|
239
|
+
fsPromises.writeFile(`${filenameCached}.map`, result.map, 'utf8')
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
await Promise.all(writePromises);
|
|
244
|
+
|
|
245
|
+
this.logger.trace(
|
|
246
|
+
{
|
|
247
|
+
filename,
|
|
248
|
+
filenameCached,
|
|
249
|
+
},
|
|
250
|
+
'Cache entry created.'
|
|
251
|
+
);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
this.logger.warn(
|
|
254
|
+
{
|
|
255
|
+
err,
|
|
256
|
+
filename,
|
|
257
|
+
filenameCached,
|
|
258
|
+
},
|
|
259
|
+
'Unable to cache rewrite results.'
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright:
|
|
2
|
+
* Copyright: 2024 Contrast Security, Inc
|
|
3
3
|
* Contact: support@contrastsecurity.com
|
|
4
4
|
* License: Commercial
|
|
5
5
|
|
|
@@ -13,25 +13,100 @@
|
|
|
13
13
|
* way not consistent with the End User License Agreement.
|
|
14
14
|
*/
|
|
15
15
|
// @ts-check
|
|
16
|
-
|
|
17
16
|
'use strict';
|
|
18
17
|
|
|
19
|
-
const { transformSync } = require('@swc/core');
|
|
20
|
-
const Module = require('module');
|
|
18
|
+
const { transform, transformSync } = require('@swc/core');
|
|
19
|
+
const Module = require('node:module');
|
|
20
|
+
const { Cache } = require('./cache');
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} Core
|
|
24
|
+
* @prop {import('@contrast/common').AppInfo} appInfo
|
|
25
|
+
* @prop {string} agentVersion
|
|
26
|
+
* @prop {import('@contrast/config').Config} config
|
|
27
|
+
* @prop {import('@contrast/logger').Logger} logger
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {'assess' | 'protect'} Mode
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {Object} RewriteOpts
|
|
34
|
+
* @prop {string=} filename e.g. 'index.js'
|
|
35
|
+
* @prop {boolean=} isModule if true, file is parsed as an ES module instead of a CJS script
|
|
36
|
+
* @prop {boolean=} inject if true, injects ContrastMethods on the global object
|
|
37
|
+
* @prop {boolean=} wrap if true, wraps the content with a modified module wrapper IIFE
|
|
38
|
+
* @prop {boolean=} trim if true, removes added characters from the end of the generated code
|
|
39
|
+
*/
|
|
24
40
|
|
|
25
41
|
// @ts-expect-error `wrapper` is missing from @types/node
|
|
26
42
|
const prefix = Module.wrapper[0];
|
|
27
43
|
// @ts-expect-error `wrapper` is missing from @types/node
|
|
28
44
|
const suffix = Module.wrapper[1].replace(/;$/, '.apply(this, arguments);');
|
|
29
45
|
|
|
30
|
-
|
|
46
|
+
const rewriterPath = require.resolve('@contrast/agent-swc-plugin');
|
|
47
|
+
const unwriterPath = require.resolve('@contrast/agent-swc-plugin-unwrite');
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Wraps the source content as necessary to support rewriting.
|
|
51
|
+
* Wrapping must occur before rewriting since the underlying rewriter cannot
|
|
52
|
+
* parse certain valid statements such as `return` statements in a CJS script.
|
|
53
|
+
* @param {string} content
|
|
54
|
+
* @returns {string}
|
|
55
|
+
*/
|
|
56
|
+
const wrap = (content) => {
|
|
57
|
+
let shebang = '';
|
|
58
|
+
|
|
59
|
+
// The shebang will be commented out since it cannot be present in a
|
|
60
|
+
// function body. swc doesn't include the commented shebang in the generated
|
|
61
|
+
// code despite including comments otherwise.
|
|
62
|
+
if (content.charAt(0) === '#') {
|
|
63
|
+
shebang = content.substring(0, content.indexOf('\n') + 1);
|
|
64
|
+
content = `//${content}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
content = `${shebang}${prefix}${content}${suffix}`;
|
|
68
|
+
|
|
69
|
+
return content;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Trims extraneous characters that may have been added by the rewriter.
|
|
74
|
+
* Handles newline or semicolon insertion, removing the added characters if they
|
|
75
|
+
* were not present in the original source content.
|
|
76
|
+
* @param {string} content
|
|
77
|
+
* @param {import('@swc/core').Output} result
|
|
78
|
+
* @returns {import('@swc/core').Output}
|
|
79
|
+
*/
|
|
80
|
+
const trim = (content, result) => {
|
|
81
|
+
let carriageReturn = 0;
|
|
82
|
+
// swc always adds a newline, so we only need to check the input
|
|
83
|
+
if (!content.endsWith('\n')) {
|
|
84
|
+
result.code = result.code.substring(0, result.code.length - 1);
|
|
85
|
+
} else if (content.endsWith('\r\n')) {
|
|
86
|
+
// if EOL is \r\n, then we need to account for that when we check the
|
|
87
|
+
// negative index of the last semicolon below
|
|
88
|
+
carriageReturn = 1;
|
|
89
|
+
}
|
|
90
|
+
const resultSemicolonIdx = result.code.lastIndexOf(';');
|
|
91
|
+
const contentSemicolonIdx = content.lastIndexOf(';');
|
|
92
|
+
if (contentSemicolonIdx === -1 || resultSemicolonIdx - result.code.length !== contentSemicolonIdx - content.length + carriageReturn) {
|
|
93
|
+
result.code = result.code.substring(0, resultSemicolonIdx) + result.code.substring(resultSemicolonIdx + 1, result.code.length);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result;
|
|
97
|
+
};
|
|
31
98
|
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
|
|
99
|
+
class Rewriter {
|
|
100
|
+
/**
|
|
101
|
+
* @param {Core} core
|
|
102
|
+
*/
|
|
103
|
+
constructor(core) {
|
|
104
|
+
this.core = core;
|
|
105
|
+
this.logger = core.logger.child({ name: 'contrast:rewriter' });
|
|
106
|
+
/** @type {Set<Mode>} */
|
|
107
|
+
this.modes = new Set();
|
|
108
|
+
this.cache = new Cache(core);
|
|
109
|
+
}
|
|
35
110
|
|
|
36
111
|
/**
|
|
37
112
|
* Sets the rewriter to 'assess' or 'protect' mode, enabling different
|
|
@@ -39,94 +114,113 @@ const rewriter = {
|
|
|
39
114
|
* @param {Mode} mode
|
|
40
115
|
*/
|
|
41
116
|
install(mode) {
|
|
117
|
+
this.logger.trace('installing rewriter mode: %s', mode);
|
|
42
118
|
this.modes.add(mode);
|
|
43
|
-
|
|
119
|
+
this.cache.install(mode);
|
|
120
|
+
}
|
|
44
121
|
|
|
45
122
|
/**
|
|
46
|
-
* @param {
|
|
47
|
-
* @
|
|
48
|
-
* @param {string=} opts.filename e.g. 'index.js'
|
|
49
|
-
* @param {boolean=} opts.isModule if true, file is parsed as an ES module instead of a CJS script
|
|
50
|
-
* @param {boolean=} opts.inject if true, injects ContrastMethods on the global object
|
|
51
|
-
* @param {boolean=} opts.wrap if true, wraps the content with a modified module wrapper IIFE
|
|
52
|
-
* @returns {import("@swc/core").Output}
|
|
123
|
+
* @param {RewriteOpts} opts
|
|
124
|
+
* @returns {import('@swc/core').Options}
|
|
53
125
|
*/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (content.charAt(0) === '#') {
|
|
58
|
-
shebang = content.substring(0, content.indexOf('\n') + 1);
|
|
59
|
-
// see the test output: swc doesn't include the commented shebang in the generated code despite including comments otherwise
|
|
60
|
-
content = `//${content}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (opts.wrap) {
|
|
64
|
-
content = `${shebang}${prefix}${content}${suffix}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const result = transformSync(content, {
|
|
126
|
+
rewriteConfig(opts) {
|
|
127
|
+
return {
|
|
68
128
|
filename: opts.filename,
|
|
69
129
|
isModule: opts.isModule,
|
|
130
|
+
env: {
|
|
131
|
+
targets: {
|
|
132
|
+
node: process.versions.node
|
|
133
|
+
}
|
|
134
|
+
},
|
|
70
135
|
jsc: {
|
|
71
|
-
target: 'es2019', // should work for node >14
|
|
72
136
|
experimental: {
|
|
73
|
-
plugins: [
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
assess: this.modes.has('assess'),
|
|
78
|
-
inject: this.modes.has('assess') && opts.inject,
|
|
79
|
-
},
|
|
80
|
-
],
|
|
81
|
-
],
|
|
137
|
+
plugins: [[rewriterPath, {
|
|
138
|
+
assess: this.modes.has('assess'),
|
|
139
|
+
inject: opts.inject,
|
|
140
|
+
}]],
|
|
82
141
|
},
|
|
83
142
|
},
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
143
|
+
// if we're trimming the output we're not rewriting an entire file, which
|
|
144
|
+
// means source maps are not relevant.
|
|
145
|
+
sourceMaps: !opts.trim && this.core.config.agent.node.source_maps.enable,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Rewrites the provided source code string asynchronously to be consumed by
|
|
151
|
+
* ESM hooks.
|
|
152
|
+
* @param {string} content
|
|
153
|
+
* @param {RewriteOpts=} opts
|
|
154
|
+
* @returns {Promise<import('@swc/core').Output>}
|
|
155
|
+
*/
|
|
156
|
+
async rewrite(content, opts = {}) {
|
|
157
|
+
this.logger.trace({ opts }, 'rewriting %s', opts.filename);
|
|
158
|
+
|
|
159
|
+
if (opts.wrap) {
|
|
160
|
+
content = wrap(content);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let result = await transform(content, this.rewriteConfig(opts));
|
|
164
|
+
|
|
165
|
+
if (opts.trim) {
|
|
166
|
+
result = trim(content, result);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Rewrites the provided source code string synchronously to be consumed by
|
|
174
|
+
* CJS hooks.
|
|
175
|
+
* @param {string} content
|
|
176
|
+
* @param {RewriteOpts=} opts
|
|
177
|
+
* @returns {import('@swc/core').Output}
|
|
178
|
+
*/
|
|
179
|
+
rewriteSync(content, opts = {}) {
|
|
180
|
+
this.logger.trace({ opts }, 'rewriting %s', opts.filename);
|
|
181
|
+
|
|
182
|
+
if (opts.wrap) {
|
|
183
|
+
content = wrap(content);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let result = transformSync(content, this.rewriteConfig(opts));
|
|
187
|
+
|
|
188
|
+
if (opts.trim) {
|
|
189
|
+
result = trim(content, result);
|
|
102
190
|
}
|
|
103
191
|
|
|
104
192
|
return result;
|
|
105
|
-
}
|
|
193
|
+
}
|
|
106
194
|
|
|
107
195
|
/**
|
|
196
|
+
* Removes contrast-related rewritten code from provided source code string.
|
|
108
197
|
* @param {string} content
|
|
109
198
|
* @returns {string}
|
|
110
199
|
*/
|
|
111
|
-
|
|
200
|
+
unwriteSync(content) {
|
|
112
201
|
return transformSync(content, {
|
|
202
|
+
env: {
|
|
203
|
+
targets: {
|
|
204
|
+
node: process.versions.node
|
|
205
|
+
}
|
|
206
|
+
},
|
|
113
207
|
jsc: {
|
|
114
|
-
target: 'es2019', // should work for node >14
|
|
115
208
|
experimental: {
|
|
116
209
|
plugins: [[unwriterPath, {}]],
|
|
117
210
|
},
|
|
118
211
|
},
|
|
212
|
+
sourceMaps: false,
|
|
119
213
|
}).code;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** @typedef {typeof rewriter} Rewriter */
|
|
214
|
+
}
|
|
215
|
+
}
|
|
124
216
|
|
|
125
217
|
/**
|
|
126
|
-
* @param {{ rewriter
|
|
218
|
+
* @param {Core & { rewriter?: Rewriter; }} core
|
|
127
219
|
* @returns {Rewriter}
|
|
128
220
|
*/
|
|
129
221
|
module.exports = function init(core) {
|
|
130
|
-
core.rewriter =
|
|
131
|
-
return rewriter;
|
|
222
|
+
core.rewriter = new Rewriter(core);
|
|
223
|
+
return core.rewriter;
|
|
132
224
|
};
|
|
225
|
+
|
|
226
|
+
module.exports.Rewriter = Rewriter;
|
package/lib/source-maps.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Copyright:
|
|
2
|
+
* Copyright: 2024 Contrast Security, Inc
|
|
3
3
|
* Contact: support@contrastsecurity.com
|
|
4
4
|
* License: Commercial
|
|
5
5
|
|
|
@@ -19,27 +19,26 @@ const { readFileSync, existsSync } = require('fs');
|
|
|
19
19
|
const { transfer } = require('multi-stage-sourcemap');
|
|
20
20
|
const { SourceMapConsumer } = require('@contrast/synchronous-source-maps');
|
|
21
21
|
|
|
22
|
-
module.exports = function(deps) {
|
|
22
|
+
module.exports = function (deps) {
|
|
23
23
|
const sourceMaps = deps.rewriter.sourceMaps = {};
|
|
24
24
|
const consumerCache = sourceMaps.consumerCache = {};
|
|
25
25
|
|
|
26
|
-
sourceMaps.cacheConsumerMap = function(filename, map) {
|
|
26
|
+
sourceMaps.cacheConsumerMap = function (filename, map) {
|
|
27
27
|
consumerCache[filename] = new SourceMapConsumer(map);
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
*/
|
|
32
|
-
sourceMaps.chain = function(filename, map) {
|
|
32
|
+
sourceMaps.chain = function (filename, map) {
|
|
33
33
|
let ret;
|
|
34
34
|
|
|
35
35
|
if (existsSync(`${filename}.map`)) {
|
|
36
36
|
try {
|
|
37
37
|
const existingMap = JSON.parse(readFileSync(`${filename}.map`, 'utf8'));
|
|
38
38
|
ret = transfer({ fromSourceMap: map, toSourceMap: existingMap });
|
|
39
|
-
deps.logger.trace(
|
|
40
|
-
} catch (
|
|
41
|
-
deps.logger.debug(
|
|
42
|
-
deps.logger.debug(`${e}`);
|
|
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);
|
|
43
42
|
}
|
|
44
43
|
}
|
|
45
44
|
|
package/package.json
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/rewriter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
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)",
|
|
7
|
-
"files": [
|
|
8
|
-
"lib/"
|
|
9
|
-
],
|
|
10
7
|
"main": "lib/index.js",
|
|
11
|
-
"types": "
|
|
8
|
+
"types": "types/index.d.ts",
|
|
12
9
|
"engines": {
|
|
13
10
|
"npm": ">=6.13.7 <7 || >= 8.3.1",
|
|
14
11
|
"node": ">= 14.15.0"
|
|
@@ -17,10 +14,11 @@
|
|
|
17
14
|
"test": "../scripts/test.sh"
|
|
18
15
|
},
|
|
19
16
|
"dependencies": {
|
|
20
|
-
"@contrast/agent-swc-plugin": "^1.
|
|
21
|
-
"@contrast/agent-swc-plugin-unwrite": "^1.
|
|
17
|
+
"@contrast/agent-swc-plugin": "^1.3.0",
|
|
18
|
+
"@contrast/agent-swc-plugin-unwrite": "^1.3.0",
|
|
19
|
+
"@contrast/common": "^1.16.0",
|
|
22
20
|
"@contrast/synchronous-source-maps": "^1.1.3",
|
|
23
21
|
"@swc/core": "1.3.39",
|
|
24
22
|
"multi-stage-sourcemap": "^0.3.1"
|
|
25
23
|
}
|
|
26
|
-
}
|
|
24
|
+
}
|