import {parse} from '@babel/parser'; import {recursive as walk} from 'babel-walk'; import * as t from '@babel/types'; import detect from './globals'; const parseOptions = { allowReturnOutsideFunction: true, allowImportExportEverywhere: true, }; /** * Mimic `with` as far as possible but at compile time * * @param obj The object part of a with expression * @param src The body of the with expression * @param exclude A list of variable names to explicitly exclude */ export default function addWith( obj: string, src: string, exclude: string[] = [], ) { // tslint:disable-next-line: no-parameter-reassignment obj = obj + ''; // tslint:disable-next-line: no-parameter-reassignment src = src + ''; let ast; try { ast = parse(src, parseOptions); } catch (e) { throw Object.assign( new Error('Error parsing body of the with expression'), { component: 'src', babylonError: e, }, ); } let objAst; try { objAst = parse(obj, parseOptions); } catch (e) { throw Object.assign( new Error('Error parsing object part of the with expression'), { component: 'obj', babylonError: e, }, ); } const excludeSet = new Set([ 'undefined', 'this', ...exclude, ...detect(objAst).map((g) => g.name), ]); const vars = new Set( detect(ast) .map((global) => global.name) .filter((v) => !excludeSet.has(v)), ); if (vars.size === 0) return src; let declareLocal = ''; let local = 'locals_for_with'; let result = 'result_of_with'; if (t.isValidIdentifier(obj)) { local = obj; } else { while (vars.has(local) || excludeSet.has(local)) { local += '_'; } declareLocal = `var ${local} = (${obj});`; } while (vars.has(result) || excludeSet.has(result)) { result += '_'; } const args = [ 'this', ...Array.from(vars).map( (v) => `${JSON.stringify(v)} in ${local} ? ${local}.${v} : typeof ${v} !== 'undefined' ? ${v} : undefined`, ), ]; const unwrapped = unwrapReturns(ast, src, result); return `; ${declareLocal} ${unwrapped.before} (function (${Array.from(vars).join(', ')}) { ${unwrapped.body} }.call(${args.join(', ')})); ${unwrapped.after};`; } interface UnwrapReturnsState { hasReturn: boolean; source(node: t.Node): string; replace(node: t.Node, str: string): void; } const unwrapReturnsVisitors = walk({ Function(_node, _state, _c) { // returns in these functions are not applicable }, ReturnStatement(node, state) { state.hasReturn = true; let value = ''; if (node.argument) { value = `value: (${state.source(node.argument)})`; } state.replace(node, `return {${value}};`); }, }); /** * Take a self calling function, and unwrap it such that return inside the function * results in return outside the function * * @param src Some JavaScript code representing a self-calling function * @param result A temporary variable to store the result in */ function unwrapReturns(ast: t.Node, src: string, result: string) { const charArray = src.split(''); const state: UnwrapReturnsState = { hasReturn: false, source(node) { return src.slice(node.start!, node.end!); }, replace(node, str) { charArray.fill('', node.start!, node.end!); charArray[node.start!] = str; }, }; unwrapReturnsVisitors(ast, state); return { before: state.hasReturn ? `var ${result} = ` : '', body: charArray.join(''), after: state.hasReturn ? `;if (${result}) return ${result}.value` : '', }; } module.exports = addWith; module.exports.default = addWith;