132 lines
3.5 KiB
JavaScript
132 lines
3.5 KiB
JavaScript
import { fileURLToPath } from "node:url";
|
|
import { parse } from "@babel/parser";
|
|
import { traverseFast as traverse } from "@babel/types";
|
|
import babelGenerator from "@babel/generator";
|
|
import { outdent } from "outdent";
|
|
import { SOURCE_DIR } from "../../utils/index.mjs";
|
|
|
|
const generate = babelGenerator.default;
|
|
const atHelperPath = fileURLToPath(new URL("../shims/at.js", import.meta.url));
|
|
|
|
/* Doesn't work for dependencies, optional call, computed property, and spread arguments */
|
|
|
|
/**
|
|
* `Object.hasOwn(foo, "bar")` -> `Object.prototype.hasOwnProperty.call(foo, "bar")`
|
|
*
|
|
* @param {import("@babel/types").Node} node
|
|
* @returns {boolean}
|
|
*/
|
|
function transformObjectHasOwnCall(node) {
|
|
if (
|
|
!(
|
|
node.type === "CallExpression" &&
|
|
!node.optional &&
|
|
node.arguments.length === 2 &&
|
|
node.arguments.every(({ type }) => type !== "SpreadElement") &&
|
|
node.callee.type === "MemberExpression" &&
|
|
!node.callee.optional &&
|
|
!node.callee.computed &&
|
|
node.callee.object.type === "Identifier" &&
|
|
node.callee.object.name === "Object" &&
|
|
node.callee.property.type === "Identifier" &&
|
|
node.callee.property.name === "hasOwn"
|
|
)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
node.callee = {
|
|
type: "MemberExpression",
|
|
object: {
|
|
type: "MemberExpression",
|
|
object: {
|
|
type: "MemberExpression",
|
|
object: { type: "Identifier", name: "Object" },
|
|
property: { type: "Identifier", name: "prototype" },
|
|
},
|
|
property: { type: "Identifier", name: "hasOwnProperty" },
|
|
},
|
|
property: { type: "Identifier", name: "call" },
|
|
};
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* `foo.at(index)` -> `__at(false, foo, index)`
|
|
* `foo?.at(index)` -> `__at(true, foo, index)`
|
|
*
|
|
* @param {import("@babel/types").Node} node
|
|
* @returns {boolean}
|
|
*/
|
|
function transformRelativeIndexingCall(node) {
|
|
if (
|
|
!(
|
|
(node.type === "CallExpression" ||
|
|
node.type === "OptionalCallExpression") &&
|
|
!node.optional &&
|
|
node.arguments.length === 1 &&
|
|
node.arguments.every(({ type }) => type !== "SpreadElement") &&
|
|
(node.callee.type === "MemberExpression" ||
|
|
node.callee.type === "OptionalMemberExpression") &&
|
|
!node.callee.computed &&
|
|
node.callee.object.type !== "ThisExpression" &&
|
|
node.callee.property.type === "Identifier" &&
|
|
node.callee.property.name === "at"
|
|
)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
node.arguments.unshift(
|
|
{
|
|
type: "BooleanLiteral",
|
|
value: node.callee.type === "OptionalMemberExpression",
|
|
},
|
|
node.callee.object
|
|
);
|
|
node.callee = { type: "Identifier", name: "__at" };
|
|
|
|
return true;
|
|
}
|
|
|
|
function transform(original, file) {
|
|
if (
|
|
file === atHelperPath ||
|
|
!file.startsWith(SOURCE_DIR) ||
|
|
(!original.includes(".at(") && !original.includes("Object.hasOwn("))
|
|
) {
|
|
return original;
|
|
}
|
|
|
|
let changed = false;
|
|
let shouldInjectRelativeIndexingHelper = false;
|
|
const ast = parse(original, { sourceType: "module" });
|
|
traverse(ast, (node) => {
|
|
const hasObjectHasOwnCall = transformObjectHasOwnCall(node);
|
|
changed ||= hasObjectHasOwnCall;
|
|
|
|
const hasRelativeIndexingCall = transformRelativeIndexingCall(node);
|
|
shouldInjectRelativeIndexingHelper ||= hasRelativeIndexingCall;
|
|
changed ||= hasRelativeIndexingCall;
|
|
});
|
|
|
|
if (!changed) {
|
|
return original;
|
|
}
|
|
|
|
let { code } = generate(ast);
|
|
|
|
if (shouldInjectRelativeIndexingHelper) {
|
|
code = outdent`
|
|
import __at from ${JSON.stringify(atHelperPath)};
|
|
|
|
${code}
|
|
`;
|
|
}
|
|
|
|
return code;
|
|
}
|
|
|
|
export default transform;
|