Skip to content

Commit

Permalink
chore(babel): code style & jsdocs
Browse files Browse the repository at this point in the history
  • Loading branch information
lukeed committed Feb 18, 2021
1 parent 9a8f195 commit 08c6d8f
Showing 1 changed file with 60 additions and 58 deletions.
118 changes: 60 additions & 58 deletions babel-plugin-optimize-obj-str/index.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
// @ts-check

/**
* @typedef {import('@babel/core').Node} Node
* @typedef {import('@babel/core').NodePath<*>} NodePath
* @typedef {import('@babel/core').PluginItem} PluginItem
* @typedef {import('@babel/core').types.ObjectExpression['properties']} ObjectProperties
*/

/**
* @param {import('@babel/core')} babel
* @param {object} [options]
* @param {boolean} [options.strict]
* @returns {import('@babel/core').PluginItem}
* @returns {PluginItem}
*/
module.exports = function (babel, options={}) {
const { types: t } = babel;
const { ast } = babel.template.expression;

/**
* Fails on strict mode when encountering an unoptimizable case.
* @param {NodePath} path
* @param {string} message
*/
function throwUnoptimizable(path, state, message) {
if (options.strict) throw path.buildCodeFrameError(message);
function unoptimizable(path, message) {
if (options.strict) throw path.buildCodeFrameError(`${path.node.callee.name}() ` + message);
}

/**
* Decontextualizes a node for comparison with a different node, irrespective
* of its location in source or surrounding comments.
* @param {Node} node
*/
function decontextualize(node) {
const clean = { ...node };
Expand All @@ -39,51 +52,51 @@ module.exports = function (babel, options={}) {
/**
* Converts identifier property keys into string literals as mapped by spec,
* as how {a: x} is the same as {'a': x}.
* @NOTE Ignores `SpreadElement` intentionally; see `dedupe`
* @param {ObjectProperties[number]} prop
*/
function propKey(prop) {
return t.isIdentifier(prop.key) && !prop.computed
? t.stringLiteral(prop.key.name)
: prop.key;
return t.isSpreadElement(prop) ? void 0 :
t.isIdentifier(prop.key) && !prop.computed
? t.stringLiteral(prop.key.name)
: prop.key;
}

/**
* Removes properties with duplicate keys, honoring the lastly defined value.
* @param {NodePath} path
* @param {ObjectProperties} properties
* @returns {ObjectProperties|void}
*/
function dedupe(path, state, properties) {
const deduped = Object.create(null);
for (const prop of properties) {
if (!prop.key) {
const { name } = path.node.callee;
const { type, argument } = prop;
throwUnoptimizable(
path,
state,
`${name}() must only contain keyed props, found [${type}] ${
(argument && argument.name) || '(unknown)'
}`
);
return;
function dedupe(path, properties) {
const cache = Object.create(null);
for (let prop of properties) {
if ('key' in prop) {
cache[JSON.stringify(decontextualize(propKey(prop)))] = prop;
} else {
let { type, argument } = prop;
return unoptimizable(path, `must only contain keyed props, found [${type}] ${
(argument && argument.name) || '(unknown)'
}`);
}
const key = JSON.stringify(decontextualize(propKey(prop)));
deduped[key] = prop;
}
return Object.values(deduped);
return Object.values(cache);
}

/**
* Replaces a path with a simpler constant value if possible.
* @param {NodePath} path
*/
function maybeEvaluate(path) {
function tryEval(path) {
const { confident, value } = path.evaluate();
if (confident) {
path.replaceWith(ast(JSON.stringify(value)));
}
if (confident) path.replaceWith(ast(JSON.stringify(value)));
}

/**
* Generates expression to concatenate strings.
* @param {ObjectProperties} properties
*/
function concatExpr(properties) {
function expr(properties) {
return properties.reduce((previous, prop) => {
const condition = prop.value;
const part = propKey(prop);
Expand All @@ -96,42 +109,31 @@ module.exports = function (babel, options={}) {
return {
name: 'optimize-obj-str',
visitor: {
CallExpression(path, state) {
CallExpression(path) {
const callee = path.get('callee');
if (!callee.referencesImport('obj-str', 'default')) {
return;
}
if (!callee.referencesImport('obj-str', 'default')) return;

const objectExpression = path.node.arguments[0];
if (
path.node.arguments.length !== 1 ||
!t.isObjectExpression(objectExpression)
) {
throwUnoptimizable(
path,
state,
`${path.node.callee.name}() argument should be a single Object Expression initializer.`
);
return;
const argument = path.node.arguments[0];
if (path.node.arguments.length !== 1 || !t.isObjectExpression(argument)) {
return unoptimizable(path, 'argument should be a single Object Expression initializer.');
}

const { properties } = objectExpression;
const usableProperties = dedupe(path, state, properties);
if (!usableProperties) {
return;
}
const properties = dedupe(path, argument.properties);

const expression = concatExpr(usableProperties);
path.replaceWith(expression);
if (properties) {
path.replaceWith(
expr(properties)
);

path.traverse({
BinaryExpression: maybeEvaluate,
ConditionalExpression: maybeEvaluate,
LogicalExpression: maybeEvaluate,
});
path.traverse({
BinaryExpression: tryEval,
ConditionalExpression: tryEval,
LogicalExpression: tryEval,
});

maybeEvaluate(path);
},
},
tryEval(path);
}
}
}
};
};

0 comments on commit 08c6d8f

Please sign in to comment.