UNPKG

3.73 kBPlain TextView Raw
1import {parse} from '@babel/parser';
2import {recursive as walk} from 'babel-walk';
3import * as t from '@babel/types';
4import detect from './globals';
5
6const parseOptions = {
7 allowReturnOutsideFunction: true,
8 allowImportExportEverywhere: true,
9};
10
11/**
12 * Mimic `with` as far as possible but at compile time
13 *
14 * @param obj The object part of a with expression
15 * @param src The body of the with expression
16 * @param exclude A list of variable names to explicitly exclude
17 */
18export default function addWith(
19 obj: string,
20 src: string,
21 exclude: string[] = [],
22) {
23 // tslint:disable-next-line: no-parameter-reassignment
24 obj = obj + '';
25 // tslint:disable-next-line: no-parameter-reassignment
26 src = src + '';
27
28 let ast;
29 try {
30 ast = parse(src, parseOptions);
31 } catch (e) {
32 throw Object.assign(
33 new Error('Error parsing body of the with expression'),
34 {
35 component: 'src',
36 babylonError: e,
37 },
38 );
39 }
40 let objAst;
41 try {
42 objAst = parse(obj, parseOptions);
43 } catch (e) {
44 throw Object.assign(
45 new Error('Error parsing object part of the with expression'),
46 {
47 component: 'obj',
48 babylonError: e,
49 },
50 );
51 }
52 const excludeSet = new Set([
53 'undefined',
54 'this',
55 ...exclude,
56 ...detect(objAst).map((g) => g.name),
57 ]);
58
59 const vars = new Set(
60 detect(ast)
61 .map((global) => global.name)
62 .filter((v) => !excludeSet.has(v)),
63 );
64
65 if (vars.size === 0) return src;
66
67 let declareLocal = '';
68 let local = 'locals_for_with';
69 let result = 'result_of_with';
70 if (t.isValidIdentifier(obj)) {
71 local = obj;
72 } else {
73 while (vars.has(local) || excludeSet.has(local)) {
74 local += '_';
75 }
76 declareLocal = `var ${local} = (${obj});`;
77 }
78 while (vars.has(result) || excludeSet.has(result)) {
79 result += '_';
80 }
81
82 const args = [
83 'this',
84 ...Array.from(vars).map(
85 (v) =>
86 `${JSON.stringify(v)} in ${local} ?
87 ${local}.${v} :
88 typeof ${v} !== 'undefined' ? ${v} : undefined`,
89 ),
90 ];
91
92 const unwrapped = unwrapReturns(ast, src, result);
93
94 return `;
95 ${declareLocal}
96 ${unwrapped.before}
97 (function (${Array.from(vars).join(', ')}) {
98 ${unwrapped.body}
99 }.call(${args.join(', ')}));
100 ${unwrapped.after};`;
101}
102
103interface UnwrapReturnsState {
104 hasReturn: boolean;
105 source(node: t.Node): string;
106 replace(node: t.Node, str: string): void;
107}
108const unwrapReturnsVisitors = walk<UnwrapReturnsState>({
109 Function(_node, _state, _c) {
110 // returns in these functions are not applicable
111 },
112
113 ReturnStatement(node, state) {
114 state.hasReturn = true;
115 let value = '';
116 if (node.argument) {
117 value = `value: (${state.source(node.argument)})`;
118 }
119 state.replace(node, `return {${value}};`);
120 },
121});
122
123/**
124 * Take a self calling function, and unwrap it such that return inside the function
125 * results in return outside the function
126 *
127 * @param src Some JavaScript code representing a self-calling function
128 * @param result A temporary variable to store the result in
129 */
130function unwrapReturns(ast: t.Node, src: string, result: string) {
131 const charArray = src.split('');
132
133 const state: UnwrapReturnsState = {
134 hasReturn: false,
135 source(node) {
136 return src.slice(node.start!, node.end!);
137 },
138 replace(node, str) {
139 charArray.fill('', node.start!, node.end!);
140 charArray[node.start!] = str;
141 },
142 };
143
144 unwrapReturnsVisitors(ast, state);
145
146 return {
147 before: state.hasReturn ? `var ${result} = ` : '',
148 body: charArray.join(''),
149 after: state.hasReturn ? `;if (${result}) return ${result}.value` : '',
150 };
151}
152
153module.exports = addWith;
154module.exports.default = addWith;