-
Notifications
You must be signed in to change notification settings - Fork 207
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ZK-5802: _listenFlex, _unlistenFlex declared on static member _ are u…
…sed directly, cannot be overriden
- Loading branch information
1 parent
d4d75a0
commit e161832
Showing
2 changed files
with
310 additions
and
56 deletions.
There are no files selected for viewing
365 changes: 309 additions & 56 deletions
365
babel-plugin-expose-private-functions-and-variables.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,87 +1,340 @@ | ||
// eslint-disable-next-line no-undef | ||
module.exports = function ({types: t}) { | ||
|
||
function createNestedMemberExpression(identifiers) { | ||
if (identifiers.length === 1) { | ||
return t.identifier(identifiers[0]); | ||
/** | ||
* @internal | ||
* Convert directory string array to MemberExpression. | ||
* @param dir - directory string[] | ||
* @returns MemberExpression | ||
*/ | ||
function _createNestedMemberExpression(dir) { | ||
// example: | ||
// input -> ['window', 'zk', 'widget_', '_listenFlex'] | ||
// output -> window.zk.widget_._listenFlex | ||
if (dir.length === 1) { | ||
return t.identifier(dir[0]); | ||
} else { | ||
const [head, ...tail] = identifiers; | ||
const tail = dir[dir.length - 1], | ||
rest = dir.slice(0, -1); | ||
return t.memberExpression( | ||
createNestedMemberExpression(tail), | ||
t.identifier(head) | ||
_createNestedMemberExpression(rest), | ||
t.identifier(tail) | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* @internal | ||
* Create function AssignmentExpression from a FunctionDeclaration node. | ||
* @param dir - directory string[] | ||
* @param node - FunctionDeclaration node | ||
* @returns ExpressionStatement | ||
*/ | ||
function _createFunctionAssignmentExpression(dir, node) { | ||
// example: | ||
// input -> ['window', 'zk', 'widget_'], node = function _listenFlex(args) {...} | ||
// output -> window.zk.widget_._listenFlex = function(args) {...} | ||
return t.expressionStatement( | ||
t.assignmentExpression( | ||
'=', | ||
_createNestedMemberExpression([...dir, node.id.name]), | ||
t.functionExpression( | ||
undefined, | ||
node.params, | ||
node.body, | ||
node.generator || false, | ||
node.async || false | ||
) | ||
) | ||
); | ||
} | ||
|
||
/** | ||
* @internal | ||
* Create check-exist if statement for a directory. | ||
* @param dir - directory string[] | ||
* @returns IfStatement | ||
*/ | ||
function _createCheckExistIfStatement(dir) { | ||
// example: | ||
// input -> ['window', 'zk', 'widget_'] | ||
// output -> if (!window.zk) window.zk = {}; if (!window.zk.widget_) window.zk.widget_ = {}; | ||
const nestedExpression = _createNestedMemberExpression(dir); | ||
return t.ifStatement( | ||
t.unaryExpression('!', nestedExpression), | ||
t.expressionStatement( | ||
t.assignmentExpression( | ||
'=', | ||
nestedExpression, | ||
t.objectExpression([]) | ||
) | ||
) | ||
); | ||
} | ||
|
||
/** | ||
* @internal | ||
* Create export private variable statement. | ||
* @param dir - directory string[] | ||
* @param varName - variable name | ||
* @returns ExpressionStatement | ||
*/ | ||
function _createExportPrivateVariableStatement(dir, varName) { | ||
// example: | ||
// input -> ['window', 'zk', 'widget_'], varName = '_listenFlex' | ||
// output -> window.zk.widget_._listenFlex = _listenFlex | ||
return t.expressionStatement( | ||
t.assignmentExpression( | ||
'=', | ||
_createNestedMemberExpression([...dir, varName]), | ||
t.identifier(varName) | ||
) | ||
); | ||
} | ||
|
||
/** | ||
* @internal | ||
* Check if a node is an exported function. | ||
* @param node - node to check | ||
* @returns boolean | ||
*/ | ||
function _isExportedFunction(node) { | ||
// example: | ||
// input -> exports.x = x; | ||
// output -> true | ||
return t.isExpressionStatement(node) && | ||
t.isAssignmentExpression(node.expression) && | ||
t.isMemberExpression(node.expression.left) && | ||
t.isIdentifier(node.expression.left.object) && | ||
node.expression.left.object.name === 'exports' && | ||
t.isIdentifier(node.expression.left.property) && | ||
t.isIdentifier(node.expression.right); | ||
} | ||
|
||
return { | ||
visitor: { | ||
Program: { | ||
exit(path) { | ||
let dir = this.file.opts.filename.replace(/-/g, '_').split('/'); | ||
const jsLoc = dir.findIndex(x => x === 'js'), | ||
file = dir[dir.length - 1], | ||
exports = {}; | ||
exit(rootPath) { | ||
let _dir = this.file.opts.filename.replace(/-/g, '_').split('/'), | ||
_jsLoc = _dir.findIndex(x => x === 'js'), | ||
_file = _dir[_dir.length - 1], | ||
_privateVars = new Set(), | ||
_privateFuncs = new Set(), | ||
_exportedFuncs = new Set(); | ||
|
||
// pass if [not in js folder] or [not ts file] or [is global.d.ts] or [is index.ts] | ||
if (jsLoc === -1 || !file.endsWith('ts') || file === 'global.d.ts' || file === 'index.ts') return; | ||
if (_jsLoc === -1 || !_file.endsWith('ts') || _file === 'global.d.ts' || _file === 'index.ts') return; | ||
|
||
// simplify whole dir | ||
dir = dir.slice(jsLoc + 1); | ||
dir[dir.length - 1] = file.replace('.ts', ''); | ||
|
||
// sort order for follow-up | ||
dir.unshift('window'); | ||
dir.push('_'); | ||
dir.reverse(); | ||
// preprocess directory to ['window', '${PACKAGE_PATH}_'] | ||
// e.g. js/zk/widget.ts -> ['window', 'zk', 'widget_'] | ||
_dir = ['window', ..._dir.slice(_jsLoc + 1, -1), _file.replace('.ts', '') + '_']; | ||
|
||
// visit all nodes | ||
path.node.body.forEach((node, index) => { | ||
rootPath.node.body.forEach((node) => { | ||
// collect private variables | ||
if (t.isVariableDeclaration(node)) { | ||
node.declarations.forEach((declaration) => { | ||
if (t.isIdentifier(declaration.id)) { | ||
exports[declaration.id.name] = declaration.id.name; | ||
} | ||
if (t.isIdentifier(declaration.id)) | ||
_privateVars.add(declaration.id.name); | ||
}); | ||
} else if (t.isFunctionDeclaration(node) && t.isIdentifier(node.id)) { | ||
exports[node.id.name] = node.id.name; | ||
_privateFuncs.add(node.id.name); | ||
} else if (_isExportedFunction(node)) { | ||
_exportedFuncs.add(node.expression.right.name); | ||
} | ||
}); | ||
_exportedFuncs.forEach(f => _privateFuncs.delete(f)); | ||
|
||
// replace private function declarations with `window.${PACKAGE_PATH}_._func` | ||
rootPath.node.body.forEach((node, index) => { | ||
if (t.isFunctionDeclaration(node) | ||
&& t.isIdentifier(node.id) | ||
&& _privateFuncs.has(node.id.name)) | ||
rootPath.get('body')[index].replaceWith(_createFunctionAssignmentExpression(_dir, node)); | ||
}); | ||
|
||
// insert check-exist if statements in the start of the file | ||
for (let i = _dir.length; i >= 2; i--) | ||
rootPath.unshiftContainer('body', _createCheckExistIfStatement(_dir.slice(0, i))); | ||
|
||
// append check-exist if statements in the end of the file | ||
for (let i = 2; i <= _dir.length; i++) | ||
rootPath.pushContainer('body', _createCheckExistIfStatement(_dir.slice(0, i))); | ||
|
||
// export private variable to `window.${PACKAGE_PATH}_._var = _var` | ||
_privateVars.forEach(v => { | ||
rootPath.pushContainer('body', _createExportPrivateVariableStatement(_dir, v)); | ||
}); | ||
|
||
/** | ||
* ArrayExpression | ||
* properties: elements | ||
* example: [x, y, z...] | ||
* elements: x, y, z... | ||
*/ | ||
function arrExp(path) { | ||
path.get('elements').forEach((element) => { | ||
dfs(element); | ||
|
||
if (t.isIdentifier(element.node) && _privateFuncs.has(element.node.name)) | ||
element.replaceWith(_createNestedMemberExpression([..._dir, element.node.name])); | ||
}); | ||
} | ||
|
||
/** | ||
* AssignmentExpression | ||
* properties: left, right | ||
* example: x = y | ||
* left: x, right: y | ||
*/ | ||
function assExp(path) { | ||
dfs(path.get('left')); | ||
dfs(path.get('right')); | ||
|
||
const { left, right } = path.node; | ||
// case: FUNC = x -> window.${PACKAGE_PATH}_.FUNC = x | ||
if (t.isIdentifier(left) && _privateFuncs.has(left.name)) | ||
path.node.left = _createNestedMemberExpression([..._dir, left.name]); | ||
// case: x = FUNC -> x = window.${PACKAGE_PATH}_.FUNC | ||
if (t.isIdentifier(right) && _privateFuncs.has(right.name)) | ||
path.node.right = _createNestedMemberExpression([..._dir, right.name]); | ||
} | ||
|
||
/** | ||
* CallExpression | ||
* properties: callee, arguments | ||
* example: x(y...) | ||
* callee: x, arguments: y... | ||
*/ | ||
function callExp(path) { | ||
dfs(path.get('callee')); | ||
|
||
const { callee } = path.node; | ||
// case: FUNC() -> window.${PACKAGE_PATH}_.FUNC() | ||
if (t.isIdentifier(callee) && _privateFuncs.has(callee.name)) { | ||
path.node.callee = _createNestedMemberExpression([..._dir, callee.name]); | ||
} | ||
|
||
const args = path.get('arguments'); | ||
args.forEach(arg => { | ||
dfs(arg); | ||
// case: xxx(FUNC...) -> xxx(window.${PACKAGE_PATH}_.FUNC...) | ||
if (t.isIdentifier(arg.node) && _privateFuncs.has(arg.node.name)) | ||
arg.replaceWith(_createNestedMemberExpression([..._dir, arg.node.name])); | ||
}); | ||
} | ||
|
||
/** | ||
* ConditionalExpression | ||
* properties: test, consequent, alternate | ||
* example: x ? y : z | ||
* test: x, consequent: y, alternate: z | ||
*/ | ||
function condExp(path) { | ||
dfs(path.get('test')); | ||
dfs(path.get('consequent')); | ||
dfs(path.get('alternate')); | ||
|
||
const { test, consequent, alternate } = path.node; | ||
// case: FUNC ? x : y -> window.${PACKAGE_PATH}_.FUNC ? x : y | ||
if (t.isIdentifier(test) && _privateFuncs.has(test.name)) | ||
path.node.test = _createNestedMemberExpression([..._dir, test.name]); | ||
// case: x ? FUNC : x -> x ? window.${PACKAGE_PATH}_.FUNC : x | ||
if (t.isIdentifier(consequent) && _privateFuncs.has(consequent.name)) | ||
path.node.consequent = _createNestedMemberExpression([..._dir, consequent.name]); | ||
// case: x ? x : FUNC -> x ? x : window.${PACKAGE_PATH}_.FUNC | ||
if (t.isIdentifier(alternate) && _privateFuncs.has(alternate.name)) | ||
path.node.alternate = _createNestedMemberExpression([..._dir, alternate.name]); | ||
} | ||
|
||
/** | ||
* LogicalExpression | ||
* properties: left, right | ||
* example: x && y, x || y, x ?? y | ||
* left: x, right: y | ||
*/ | ||
function logicExp(path) { | ||
dfs(path.get('left')); | ||
dfs(path.get('right')); | ||
|
||
// add check-exist if statements | ||
for (let i = dir.length - 2; i > 0; i--) { | ||
const nestedExpression = createNestedMemberExpression(dir.slice(i)); | ||
path.pushContainer('body', | ||
t.ifStatement( | ||
t.unaryExpression('!', nestedExpression), | ||
t.expressionStatement( | ||
t.assignmentExpression( | ||
'=', | ||
nestedExpression, | ||
t.objectExpression([]) | ||
) | ||
) | ||
) | ||
); | ||
const { left, right } = path.node; | ||
// case: FUNC && x -> window.${PACKAGE_PATH}_.FUNC && x | ||
if (t.isIdentifier(left) && _privateFuncs.has(left.name)) | ||
path.node.left = _createNestedMemberExpression([..._dir, left.name]); | ||
// case: x && FUNC -> x && window.${PACKAGE_PATH}_.FUNC | ||
if (t.isIdentifier(right) && _privateFuncs.has(right.name)) | ||
path.node.right = _createNestedMemberExpression([..._dir, right.name]); | ||
} | ||
|
||
// export all global variables and functions | ||
path.pushContainer('body', | ||
// window.x.x.x._ = {...} | ||
t.expressionStatement( | ||
t.assignmentExpression( | ||
'=', | ||
createNestedMemberExpression(dir), | ||
t.objectExpression( | ||
Object.entries(exports).map(([k, v]) => { | ||
return t.objectProperty(t.identifier(k), t.identifier(v)); | ||
}) | ||
) | ||
) | ||
) | ||
); | ||
/** | ||
* MemberExpression | ||
* properties: object, property | ||
* example: x.y.z | ||
* object: x.y, property: z | ||
*/ | ||
function memExp(path) { | ||
dfs(path.get('object')); | ||
// [NOTE] property cannot replace with window.${PACKAGE_PATH}_.FUNC, so ignore | ||
|
||
const object = path.node.object; | ||
// case: FUNC.x -> window.${PACKAGE_PATH}_.FUNC.x | ||
if (t.isIdentifier(object) && _privateFuncs.has(object.name)) | ||
path.node.object = _createNestedMemberExpression([..._dir, object.name]); | ||
} | ||
|
||
/** | ||
* ObjectExpression | ||
* properties: properties | ||
* TODO: split properties into 3 types (ObjectMethod | ObjectProperty | SpreadElement) of functions to handle | ||
*/ | ||
function objExp(path) { | ||
path.node.properties.forEach((property) => { | ||
// TODO: dfs | ||
const {value} = property; | ||
// TODO: key ? | ||
// case: x = { x: FUNC } -> x = { x: window.${PACKAGE_PATH}_.FUNC } | ||
// case: x.x = FUNC -> x.x = window.${PACKAGE_PATH}_.FUNC | ||
if (t.isIdentifier(value) && _privateFuncs.has(value.name)) | ||
property.value = _createNestedMemberExpression([..._dir, value.name]); | ||
}); | ||
} | ||
|
||
/** | ||
* VariableDeclaration | ||
* properties: declarations | ||
*/ | ||
function varDecl(path) { | ||
// WARNING: cannot use `path.get('declarations')` to iterate here, will get wrong result | ||
path.node.declarations.forEach((declaration) => { | ||
// [NOTE] declaration.get('init') and declaration.init in dfs are dead | ||
|
||
const { init } = declaration; | ||
if (t.isIdentifier(init) && _privateFuncs.has(init.name)) | ||
declaration.init = _createNestedMemberExpression([..._dir, init.name]); | ||
}); | ||
} | ||
|
||
/** | ||
* Traverse all nodes in current file | ||
*/ | ||
function dfs(path) { | ||
path.traverse({ | ||
ArrayExpression(p) { arrExp(p); }, | ||
AssignmentExpression(p) {assExp(p); }, | ||
CallExpression(p) { callExp(p); }, | ||
ConditionalExpression(p) { condExp(p); }, | ||
LogicalExpression(p) { logicExp(p); }, | ||
MemberExpression(p) { memExp(p); }, | ||
ObjectExpression(p) { objExp(p); }, | ||
VariableDeclaration(p) { varDecl(p); } | ||
}); | ||
} | ||
|
||
// replace private function calls to window.${PACKAGE_PATH}_.FUNC | ||
dfs(rootPath); | ||
} | ||
} | ||
} | ||
}; | ||
}; | ||
}; | ||
|
Oops, something went wrong.