babel-plugin-transform-private-to-hash
Version:
Transform ES private fields/methods to hashed properties.
198 lines (197 loc) • 11.2 kB
JavaScript
;
const helper_plugin_utils_1 = require("@babel/helper-plugin-utils");
const slash = require("slash");
const murmurhash = require("murmurhash");
module.exports = helper_plugin_utils_1.declare((api, options = {}) => {
api.assertVersion(7);
const t = api.types;
const { salt = '', enumerable = false, hashLength = 8 } = options;
return {
name: 'transform-private-to-hash',
pre(file) {
this.classCounter = 0;
this.classHashStore = new WeakMap();
},
visitor: {
Class(classPath, state) {
const filePath = state.filename || '';
const cwd = process.cwd();
const relativePath = slash(filePath.startsWith(cwd) ? filePath.slice(cwd.length) : filePath);
const classIndex = state.classCounter++;
const privateHashes = new Map();
// 处理不可枚举的情况
const staticFields = new Map();
const instanceFields = new Map();
// 收集所有私有属性和方法的哈希
classPath.node.body.body.forEach(prop => {
if (t.isClassPrivateProperty(prop) || t.isClassPrivateMethod(prop)) {
const privateName = prop.key.id.name;
const hashInput = `${salt}_${relativePath}_${classIndex}_${privateName}`;
const fullHash = murmurhash.v3(hashInput).toString(16);
const hash = fullHash.slice(0, hashLength);
privateHashes.set(privateName, `__${hash}`);
}
});
const bodyBodyPaths = classPath.get('body.body');
bodyBodyPaths.forEach(propPath => {
let prop = propPath.node;
if (t.isClassPrivateProperty(prop) || t.isClassPrivateMethod(prop)) {
const privateName = prop.key.id.name;
const hashInput = `${salt}_${relativePath}_${classIndex}_${privateName}`;
const fullHash = murmurhash.v3(hashInput).toString(16);
const hash = fullHash.slice(0, hashLength);
privateHashes.set(privateName, `__${hash}`);
if (t.isClassPrivateProperty(prop)) {
if (prop.static) {
staticFields.set(prop.key.id.name, prop.value);
}
else {
instanceFields.set(prop.key.id.name, prop.value);
}
propPath.remove();
}
}
});
state.classHashStore.set(classPath.node, privateHashes);
// 处理实例字段
if (instanceFields.size > 0) {
let constructorPath = bodyBodyPaths.find(p => p.isClassMethod() && p.node.kind === 'constructor');
if (!constructorPath) {
let constructorNode = t.classMethod('constructor', t.identifier('constructor'), [], t.blockStatement(classPath.node.superClass ?
[t.expressionStatement(t.callExpression(t.super(), [t.spreadElement(t.identifier('arguments'))]))] :
[]));
const bodyPath = classPath.get('body');
constructorPath = bodyPath.unshiftContainer('body', constructorNode)[0];
}
let statements = [];
instanceFields.forEach((express, name) => {
let statement = t.expressionStatement(enumerable ?
t.assignmentExpression("=", t.memberExpression(t.thisExpression(), t.identifier(privateHashes.get(name))), express) :
t.callExpression(t.memberExpression(t.identifier('Object'), t.identifier('defineProperty')), [
t.thisExpression(),
t.stringLiteral(privateHashes.get(name)),
t.objectExpression([
t.objectProperty(t.identifier('value'), express),
t.objectProperty(t.identifier('enumerable'), t.booleanLiteral(false)),
t.objectProperty(t.identifier('configurable'), t.booleanLiteral(true)),
t.objectProperty(t.identifier('writable'), t.booleanLiteral(true)),
])
]));
statements.push(statement);
});
let superPath = constructorPath.get('body.body').find(p => {
if (p.isExpressionStatement()) {
let callee = p.get('expression.callee');
if (callee && callee.isSuper()) {
return true;
}
}
return false;
});
if (superPath) {
superPath.insertAfter(statements);
}
else {
constructorPath.get('body').unshiftContainer('body', statements);
}
}
// 处理静态字段
if (staticFields.size > 0) {
const staticBlockBody = [];
staticFields.forEach((express, name) => {
if (enumerable) {
staticBlockBody.push(t.expressionStatement(t.assignmentExpression("=", t.memberExpression(t.thisExpression(), t.identifier(privateHashes.get(name))), express)));
}
else {
staticBlockBody.push(t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('Object'), t.identifier('defineProperty')), [
t.thisExpression(),
t.stringLiteral(privateHashes.get(name)),
t.objectExpression([
t.objectProperty(t.identifier('value'), express),
t.objectProperty(t.identifier('enumerable'), t.booleanLiteral(false)),
t.objectProperty(t.identifier('configurable'), t.booleanLiteral(true)),
t.objectProperty(t.identifier('writable'), t.booleanLiteral(true)),
])
])));
}
});
classPath.get('body').pushContainer('body', t.staticBlock(staticBlockBody));
}
classPath.traverse({
Class(classPath) {
classPath.skip();
},
ClassPrivateProperty(propertyPath) {
const classPath = propertyPath.findParent(p => p.isClass());
const privateNode = propertyPath.node;
const hashes = state.classHashStore.get(classPath.node);
if (hashes) {
const privateName = privateNode.key.id.name;
const hash = hashes.get(privateName);
if (hash) {
if (enumerable) {
propertyPath.replaceWith(t.classProperty(t.identifier(hash), privateNode.value, null, null, false, privateNode.static));
}
else {
propertyPath.remove();
}
}
}
},
ClassPrivateMethod(methodPath) {
const classPath = methodPath.findParent(p => p.isClass());
const privateNode = methodPath.node;
const hashes = state.classHashStore.get(classPath.node);
if (hashes) {
const privateName = privateNode.key.id.name;
const hash = hashes.get(privateName);
if (hash) {
methodPath.replaceWith(t.classMethod(privateNode.kind, t.identifier(hash), privateNode.params, privateNode.body, privateNode.computed, privateNode.static, privateNode.generator, privateNode.async));
}
}
},
MemberExpression(memberExpPath) {
const propertyPath = memberExpPath.get('property');
const propertyNode = propertyPath.node;
if (!t.isPrivateName(propertyNode))
return;
memberExpPath.findParent(parentPath => {
if (parentPath.isClass) {
const hashes = state.classHashStore.get(parentPath.node);
if (hashes) {
const privateName = propertyNode.id.name;
const hash = hashes.get(privateName);
if (hash) {
propertyPath.replaceWith(t.identifier(hash));
return true;
}
}
}
return false;
});
},
BinaryExpression(binaryPath) {
const leftPath = binaryPath.get('left');
const leftNode = leftPath.node;
if (!t.isPrivateName(leftNode))
return;
binaryPath.findParent(parentPath => {
if (parentPath.isClass()) {
const hashes = state.classHashStore.get(parentPath.node);
if (hashes) {
const privateName = leftNode.id.name;
const hash = hashes.get(privateName);
if (hash) {
leftPath.replaceWith(t.stringLiteral(hash));
return true;
}
}
}
return false;
});
}
});
},
}
};
});