@contrast/rewriter 1.3.1 → 1.4.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.
Files changed (2) hide show
  1. package/lib/index.js +66 -200
  2. package/package.json +5 -7
package/lib/index.js CHANGED
@@ -15,223 +15,89 @@
15
15
 
16
16
  'use strict';
17
17
 
18
+ const { transformSync } = require('@swc/core');
18
19
  const Module = require('module');
19
- const { default: traverse } = require('@babel/traverse');
20
- const parser = require('@babel/parser');
21
- const { default: generate } = require('@babel/generator');
22
- const t = require('@babel/types');
23
- const { expression, statement } = require('@babel/template');
24
20
 
21
+ const rewriterPath = require.resolve('@contrast/agent-swc-plugin');
22
+ const unwriterPath = require.resolve('@contrast/agent-swc-plugin-unwrite');
25
23
 
26
- /**
27
- * factory
28
- */
29
- module.exports = function(core) {
30
- const rewriter = new Rewriter(core);
31
- return core.rewriter = rewriter;
32
- };
33
-
34
-
35
- /**
36
- * Babel will add a trailing semicolon in some cases. This will remove it if it
37
- * wasn't there to begin with.
38
- * @param {string} rewritten rewritten content
39
- * @param {string} orig original content before rewriting
40
- * @returns {string}
41
- */
42
- function removeAddedSemicolons(orig, rewritten) {
43
- if (
44
- rewritten.charCodeAt(rewritten.length - 1) == 59 &&
45
- orig.charCodeAt(orig.length - 1) != 59
46
- ) {
47
- rewritten = rewritten.substr(0, rewritten.length - 1);
48
- }
49
- return rewritten;
50
- }
51
-
52
- class Rewriter {
53
- constructor(deps) {
54
- const self = this;
55
- this.logger = deps.logger;
56
- this.visitors = [];
57
- this.installedModes = [];
58
- this.tokens = [];
59
- this.injections = [];
60
- this.methodLookups = {};
61
- this.rewriteTransforms = {
62
- enter(...args) {
63
- for (const v of self.visitors) {
64
- v(...args);
65
- }
66
- },
67
- Program: function Program(path, state) {
68
- if (state.wrap) {
69
- let [prefix, suffix] = Module.wrapper;
70
- prefix = prefix.trim();
71
- suffix = suffix.trim().replace(/;$/, '.apply(this, arguments);');
72
-
73
- path.node.body = [
74
- statement(`${prefix} %%body%% ${suffix}`)({
75
- body: path.node.body
76
- })
77
- ];
78
- }
79
-
80
- if (state.inject) {
81
- path.unshiftContainer('body', self.injections);
82
- }
83
- },
84
- CallExpression(path) {
85
- if (path.node.callee.name === 'eval') {
86
- path.node.arguments = [
87
- t.callExpression(expression('global.ContrastMethods.eval')(), path.node.arguments)
88
- ];
89
- }
90
- },
91
- BinaryExpression: function BinaryExpression(path) {
92
- const method = self.methodLookups[path.node.operator];
93
- if (method) {
94
- path.replaceWith(
95
- t.callExpression(
96
- expression('ContrastMethods.%%method%%')({ method }), [
97
- path.node.left,
98
- path.node.right
99
- ]
100
- )
101
- );
102
- }
103
- }
104
- };
105
- this.unwriteTransforms = {
106
- CallExpression(path) {
107
- const obj = path.node.callee.object;
108
- if (obj && obj.property && obj.property.name === 'ContrastMethods') {
109
- path.replaceWith(path.node.arguments[0]);
110
- }
111
- }
112
- };
113
- this.install = function(mode) {
114
- self.installedModes.push(mode);
115
- !self.methodLookups.eval && (self.methodLookups = {
116
- eval: 'eval'
117
- });
24
+ const prefix = Module.wrapper[0];
25
+ const suffix = Module.wrapper[1].replace(/;$/, '.apply(this, arguments);');
118
26
 
119
- !(self.injections.length === 2) && self.injections.push(
120
- statement(
121
- 'const %%id%% = global.%%id%% || (() => { throw new SyntaxError(%%errMessage%%); })();'
122
- )({
123
- id: 'ContrastMethods',
124
- errMessage: t.stringLiteral(
125
- 'ContrastMethods undefined during compilation'
126
- )
127
- }),
128
- statement(
129
- 'var %%name%% = global.ContrastMethods.%%id%% || %%name%%;'
130
- )({ name: 'Function', id: 'Function' }),
131
- );
27
+ /** @typedef {'assess' | 'protect'} Mode */
132
28
 
133
- if (self.installedModes.includes('protect')) {
134
- // Protect doesn't have anything specific
135
- // for rewriting that should not be rewritten
136
- // in Assess (at least for now)
137
- }
138
- if (self.installedModes.includes('assess')) {
139
- Object.assign(self.methodLookups, {
140
- '+': 'plus',
141
- '===': 'tripleEqual',
142
- '!==': 'notTripleEqual',
143
- '==': 'doubleEqual',
144
- '!=': 'notDoubleEqual'
145
- });
146
- Object.assign(self.rewriteTransforms, {
147
- AssignmentExpression(path) {
148
- if (path.node.operator !== '+=') return;
149
- path.replaceWith(
150
- t.assignmentExpression(
151
- '=',
152
- path.node.left,
153
- t.callExpression(expression('global.ContrastMethods.plus')(), [path.node.left, path.node.right])
154
- )
155
- );
156
- }
157
- });
158
- }
29
+ const rewriter = {
30
+ /** @type {Set<Mode>} */
31
+ modes: new Set(),
159
32
 
160
- self.tokens = Object.keys(self.methodLookups);
161
- };
162
- }
33
+ /**
34
+ * Sets the rewriter to 'assess' or 'protect' mode, enabling different
35
+ * transforms.
36
+ * @param {Mode} mode
37
+ */
38
+ install(mode) {
39
+ this.modes.add(mode);
40
+ },
163
41
 
164
42
  /**
165
43
  * @param {string} content the source code
166
44
  * @param {object} opts
167
45
  * @param {string} opts.filename e.g. 'index.js'
168
- * @param {boolean} opts.inject whether to inject contrast methods
169
- * @param {string} opts.sourceType script or module
170
- * @param {boolean} opts.wrap whether to wrap code in module wrap IIFE
171
- * @returns {object}
46
+ * @param {boolean} opts.isModule if true, file is parsed as an ES module instead of a CJS script
47
+ * @param {boolean} opts.inject if true, injects ContrastMethods on the global object
48
+ * @param {boolean} opts.wrap if true, wraps the content with a modified module wrapper IIFE
49
+ * @returns {import("@swc/core").Output}
172
50
  */
173
- rewrite(content, opts = {
174
- inject: false,
175
- wrap: false,
176
- sourceType: 'script',
177
- }) {
178
- opts.filename = opts.filename || 'no filename';
179
- opts.sourceType = opts.sourceType || 'script';
180
-
181
- const state = {
182
- orig: String(content),
183
- deps: [],
184
- filename: opts.filename,
185
- ...opts
186
- };
187
-
188
- if (this.tokens.every((token) => state.orig.indexOf(token) === -1)) {
189
- return { code: state.orig };
51
+ rewrite(content, opts = {}) {
52
+ if (opts.wrap) {
53
+ content = `${prefix}${content}${suffix}`;
190
54
  }
191
55
 
192
- const ast = parser.parse(state.orig, {
193
- plugins: [
194
- 'classPrivateMethods',
195
- 'classPrivateProperties',
196
- 'classProperties'
197
- ],
198
- ranges: true,
199
- sourceType: state.sourceType,
200
- sourceFilename: state.filename,
201
- tokens: true
202
- });
203
-
204
- traverse(ast, this.rewriteTransforms, null, state);
205
- // TODO: Look into how effective this is
206
- traverse.cache.clear();
207
-
208
- const result = generate(
209
- ast,
210
- {
211
- jsonCompatibleStrings: true,
212
- sourceMaps: true,
213
- sourceFileName: state.filename
56
+ return transformSync(content, {
57
+ filename: opts.filename,
58
+ isModule: opts.isModule,
59
+ jsc: {
60
+ target: 'es2019', // should work for node >14
61
+ experimental: {
62
+ plugins: [
63
+ [
64
+ rewriterPath,
65
+ {
66
+ assess: this.modes.has('assess'),
67
+ inject: this.modes.has('assess') && opts.inject,
68
+ },
69
+ ],
70
+ ],
71
+ },
214
72
  },
215
- state.orig
216
- );
217
-
218
- result.code = removeAddedSemicolons(content, result.code);
219
- result.deps = state.deps;
220
-
221
- return result;
222
- }
73
+ sourceMaps: true,
74
+ });
75
+ },
223
76
 
224
77
  /**
225
- * @param {string} code
226
- * @param {object} opts
78
+ * @param {string} content
227
79
  * @returns {string}
228
80
  */
229
- unwrite(code, opts) {
230
- const ast = parser.parse(code);
231
- traverse(ast, this.unwriteTransforms);
232
- const unwritten = generate(ast, { jsonCompatibleStrings: true }).code;
233
- return removeAddedSemicolons(code, unwritten);
234
- }
235
- }
81
+ unwrite(content) {
82
+ return transformSync(content, {
83
+ jsc: {
84
+ target: 'es2019', // should work for node >14
85
+ experimental: {
86
+ plugins: [[unwriterPath, {}]],
87
+ },
88
+ },
89
+ }).code;
90
+ },
91
+ };
236
92
 
237
- module.exports.Rewriter = Rewriter;
93
+ /** @typedef {{}} Core */
94
+ /** @typedef {typeof rewriter} Rewriter */
95
+
96
+ /**
97
+ * @param {Core} core
98
+ * @returns {Rewriter}
99
+ */
100
+ module.exports = function init(core) {
101
+ core.rewriter = rewriter;
102
+ return rewriter;
103
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/rewriter",
3
- "version": "1.3.1",
3
+ "version": "1.4.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)",
@@ -17,12 +17,10 @@
17
17
  "test": "../scripts/test.sh"
18
18
  },
19
19
  "dependencies": {
20
- "@babel/generator": "^7.17.9",
21
- "@babel/parser": "^7.17.9",
22
- "@babel/template": "^7.16.7",
23
- "@babel/traverse": "^7.17.9",
24
- "@babel/types": "^7.16.7",
20
+ "@contrast/agent-swc-plugin": "^1.1.0",
21
+ "@contrast/agent-swc-plugin-unwrite": "^1.1.0",
25
22
  "@contrast/synchronous-source-maps": "^1.1.3",
23
+ "@swc/core": "1.3.39",
26
24
  "multi-stage-sourcemap": "^0.3.1"
27
25
  }
28
- }
26
+ }