Skip to content

Commit

Permalink
ZK-5802: _listenFlex, _unlistenFlex declared on static member _ are u…
Browse files Browse the repository at this point in the history
…sed directly, cannot be overriden
  • Loading branch information
JamsonChan committed Oct 15, 2024
1 parent d4d75a0 commit e161832
Show file tree
Hide file tree
Showing 2 changed files with 310 additions and 56 deletions.
365 changes: 309 additions & 56 deletions babel-plugin-expose-private-functions-and-variables.js
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);
}
}
}
};
};
};

Loading

0 comments on commit e161832

Please sign in to comment.