forked from DevExpress/testcafe-hammerhead
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.ts
190 lines (149 loc) · 6.42 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
// -------------------------------------------------------------
// WARNING: this file is used by both the client and the server.
// Do not use any browser or node-specific API!
// -------------------------------------------------------------
import { Program, Node } from 'estree';
import transform, { CodeChange } from './transform';
import INSTRUCTION from './instruction';
import { add as addHeader, remove as removeHeader } from './header';
import { parse } from 'acorn-hammerhead';
import { generate, Syntax } from 'esotope-hammerhead';
import reEscape from '../../utils/regexp-escape';
import getBOM from '../../utils/get-bom';
const HTML_COMMENT_RE = /(^|\n)\s*<!--[^\n]*(\n|$)/g;
const OBJECT_RE = /^\s*\{.*\}\s*$/;
const TRAILING_SEMICOLON_RE = /;\s*$/;
const OBJECT_WRAPPER_RE = /^\s*\((.*)\);\s*$/;
const SOURCEMAP_RE = /(?:\/\/[@#][ \t]+sourceMappingURL=([^\s'"]+?)[ \t]*$)/gm;
const PROCESSED_SCRIPT_RE = new RegExp([
reEscape(INSTRUCTION.getLocation),
reEscape(INSTRUCTION.setLocation),
reEscape(INSTRUCTION.getProperty),
reEscape(INSTRUCTION.setProperty),
reEscape(INSTRUCTION.callMethod),
reEscape(INSTRUCTION.processScript),
reEscape(INSTRUCTION.processHtml),
reEscape(INSTRUCTION.getPostMessage),
reEscape(INSTRUCTION.getProxyUrl)
].join('|'));
const PARSING_OPTIONS = {
allowReturnOutsideFunction: true,
allowImportExportEverywhere: true,
ecmaVersion: 13
};
// Code pre/post-processing
function removeHtmlComments (code: string): string {
// NOTE: The JS parser removes the line that follows'<!--'. (T226589)
do
code = code.replace(HTML_COMMENT_RE, '\n');
while (HTML_COMMENT_RE.test(code));
return code;
}
function preprocess (code: string): { bom: string | null; preprocessed: string } {
const bom = getBOM(code);
let preprocessed = bom ? code.substring(bom.length) : code;
preprocessed = removeHeader(preprocessed);
preprocessed = removeSourceMap(preprocessed);
return { bom, preprocessed };
}
function removeSourceMap (code: string): string {
return code.replace(SOURCEMAP_RE, '');
}
function postprocess (processed: string, withHeader: boolean, bom: string | null, strictMode: boolean, swScopeHeaderValue?: string): string {
// NOTE: If the 'use strict' directive is not in the beginning of the file, it is ignored.
// As we insert our header in the beginning of the script, we must put a new 'use strict'
// before the header, otherwise it will be ignored.
if (withHeader)
processed = addHeader(processed, strictMode, swScopeHeaderValue);
return bom ? bom + processed : processed;
}
// Parse/generate code
function removeTrailingSemicolon (processed: string, src: string): string {
return TRAILING_SEMICOLON_RE.test(src) ? processed : processed.replace(TRAILING_SEMICOLON_RE, '');
}
function getAst (src: string, isObject: boolean): Program | null {
// NOTE: In case of objects (e.g.eval('{ 1: 2}')) without wrapping
// object will be parsed as label. To avoid this we parenthesize src
src = isObject ? `(${src})` : src;
try {
return parse(src, PARSING_OPTIONS);
}
catch (err) {
return null;
}
}
function getCode (ast: Node, src: string): string {
const code = generate(ast, {
format: {
quotes: 'double',
escapeless: true,
compact: true
}
});
return src ? removeTrailingSemicolon(code, src) : code;
}
// Analyze code
function analyze (code: string): { ast: Program | null; isObject: boolean } {
let isObject = OBJECT_RE.test(code);
let ast = getAst(code, isObject);
// NOTE: `{ const a = 'foo'; }` edge case
if (!ast && isObject) {
ast = getAst(code, false);
isObject = false;
}
return { ast, isObject };
}
function isArrayDataScript (ast: Program): boolean {
const firstChild = ast.body[0];
return ast.body.length === 1 &&
firstChild.type === Syntax.ExpressionStatement &&
firstChild.expression.type === Syntax.ArrayExpression;
}
function isStrictMode (ast: Program): boolean {
if (ast.body.length) {
const firstChild = ast.body[0];
if (firstChild.type === Syntax.ExpressionStatement && firstChild.expression.type === Syntax.Literal)
return firstChild.expression.value === 'use strict';
}
return false;
}
function applyChanges (script: string, changes: CodeChange[], isObject: boolean): string {
const indexOffset = isObject ? -1 : 0;
const chunks = [] as string[];
let index = 0;
if (!changes.length)
return script;
changes.sort((a, b) => (a.start - b.start) || (a.end - b.end) ||
((a.node.type === Syntax.VariableDeclaration ? 0 : 1) - (b.node.type === Syntax.VariableDeclaration ? 0 : 1)));
for (const change of changes) {
const changeStart = change.start + indexOffset;
const changeEnd = change.end + indexOffset;
const parentheses = change.node.type === Syntax.SequenceExpression &&
change.parentType !== Syntax.ExpressionStatement &&
change.parentType !== Syntax.SequenceExpression;
chunks.push(script.substring(index, changeStart));
chunks.push(parentheses ? '(' : ' ');
chunks.push(getCode(change.node, script.substring(changeStart, changeEnd)));
chunks.push(parentheses ? ')' : ' ');
index += changeEnd - index;
}
chunks.push(script.substring(index));
return chunks.join('');
}
export function isScriptProcessed (code: string): boolean {
return PROCESSED_SCRIPT_RE.test(code);
}
export function processScript (src: string, withHeader = false, wrapLastExprWithProcessHtml = false, resolver?: Function, swScopeHeaderValue?: string): string {
const { bom, preprocessed } = preprocess(src);
const withoutHtmlComments = removeHtmlComments(preprocessed);
const { ast, isObject } = analyze(withoutHtmlComments);
if (!ast)
return src;
withHeader = withHeader && !isObject && !isArrayDataScript(ast);
const changes = transform(ast, wrapLastExprWithProcessHtml, resolver);
let processed = changes.length ? applyChanges(withoutHtmlComments, changes, isObject) : preprocessed;
processed = postprocess(processed, withHeader, bom, isStrictMode(ast), swScopeHeaderValue);
if (isObject)
processed = processed.replace(OBJECT_WRAPPER_RE, '$1');
return processed;
}