@beqa/babel-plugin-transform-react-slots
Version:
The JSX to slot function transpilation plugin for babel
424 lines (381 loc) • 13.1 kB
text/typescript
import babel from "@babel/core";
import {
ANY_PROPERTY,
getJSXElement,
goThroughMemberExpression,
getValuesFromVarDeclarator,
} from "../src/utils";
describe("goThroughMemberExpression", () => {
test("returns the first member expression when directly accessing the value; `pathToValue` becomes empty", () => {
const ast = babel.parse(`let x = a.b.c`)!;
babel.traverse(ast, {
VariableDeclarator(path) {
let pathToA: any;
path.traverse({
Identifier(path) {
if (path.node.name === "a") {
pathToA = path;
}
},
});
const pathToValue = ["c", "b"];
const parent = goThroughMemberExpression(pathToA, pathToValue) as any;
expect(parent.node.property.name).toBe("c");
expect(pathToValue).toEqual([]);
},
});
});
test("`pathToValue` holds remaining property names when the expression is accessing an ancestor of the value", () => {
const ast = babel.parse(`let x = a.b.c`)!;
babel.traverse(ast, {
VariableDeclarator(path) {
let pathToA: any;
path.traverse({
Identifier(path) {
if (path.node.name === "a") {
pathToA = path;
}
},
});
const pathToValue = ["d", "c", "b"];
goThroughMemberExpression(pathToA, pathToValue);
expect(pathToValue).toEqual(["d"]);
},
});
});
test("returns null when the expression goes deeper than specified; `pathToValue` becomes empty", () => {
const ast = babel.parse(`let x = a.b.c.d`)!;
babel.traverse(ast, {
VariableDeclarator(path) {
let pathToA: any;
path.traverse({
Identifier(path) {
if (path.node.name === "a") {
pathToA = path;
}
},
});
const pathToValue = ["c", "b"];
const parent = goThroughMemberExpression(pathToA, pathToValue);
expect(parent).toBeNull();
expect(pathToValue).toEqual([]);
},
});
});
test("returns null when path is different than specified. The array holds remaining property names from the moment the paths diverged", () => {
const ast = babel.parse(`let x = a.b.x.y`)!;
babel.traverse(ast, {
VariableDeclarator(path) {
let pathToA: any;
path.traverse({
Identifier(path) {
if (path.node.name === "a") {
pathToA = path;
}
},
});
const pathToValue = ["d", "c", "b"];
const parent = goThroughMemberExpression(pathToA, pathToValue);
expect(parent).toBeNull();
expect(pathToValue).toEqual(["d", "c"]);
},
});
});
test("accepts call expression nodes as arguments", () => {
const ast = babel.parse(`let x = a().b.c`)!;
babel.traverse(ast, {
CallExpression(path) {
const pathToValue = ["c", "b"];
const parent = goThroughMemberExpression(path, pathToValue) as any;
expect(babel.types.isVariableDeclarator(parent?.parent)).toBe(true);
expect(pathToValue).toEqual([]);
},
});
});
test("accepts string literal property paths", () => {
const ast = babel.parse(`let x = a["b"].c`)!;
babel.traverse(ast, {
VariableDeclarator(path) {
let pathToA: any;
path.traverse({
Identifier(path) {
if (path.node.name === "a") {
pathToA = path;
}
},
});
const pathToValue = ["c", "b"];
const parent = goThroughMemberExpression(pathToA, pathToValue);
expect(babel.types.isVariableDeclarator(parent?.parent)).toBe(true);
expect(pathToValue).toEqual([]);
},
});
});
test("can match ANY_PROPERTY wildcard", () => {
const ast = babel.parse(`let x = a.b.c.d`)!;
babel.traverse(ast, {
VariableDeclarator(path) {
let pathToA: any;
path.traverse({
Identifier(path) {
if (path.node.name === "a") {
pathToA = path;
}
},
});
const pathToValue = ["e", ANY_PROPERTY, "c", ANY_PROPERTY];
const parent = goThroughMemberExpression(pathToA, pathToValue);
expect(babel.types.isVariableDeclarator(parent?.parent)).toBe(true);
expect(pathToValue).toEqual(["e"]);
},
});
});
});
describe("getValueFromVarDeclarator", () => {
test("returns the identifier from the object pattern that holds the value", () => {
const ast = babel.parse(`let { a: {b}, c } = literally.anything`)!;
babel.traverse(ast, {
VariableDeclarator(path) {
const pathToValue = ["b", "a"];
for (const [identifier, newPathToValue] of getValuesFromVarDeclarator(
path,
pathToValue,
)) {
expect(identifier.node.name).toBe("b");
expect(newPathToValue).toEqual([]);
}
},
});
});
test("returns the identifier from the object pattern that's the ancestor of the value when pathToValue is longer", () => {
const ast = babel.parse(`let { a, ...c } = [literally.anything]`)!;
babel.traverse(ast, {
VariableDeclarator(path) {
const pathToValue = ["b", "a"];
for (const [identifier, newPathToValue] of getValuesFromVarDeclarator(
path,
pathToValue,
)) {
expect(identifier.node.name).toBe("a");
expect(newPathToValue).toEqual(["b"]);
}
},
});
});
test("returns the rest operator identifier when destructured properties don't match the path", () => {
const ast = babel.parse(
`let { a, b: {c, ...rest2}, ...rest1 } = [literallyAnything]`,
)!;
babel.traverse(ast, {
VariableDeclarator(path) {
const pathToValue = ["e", "d", "b"];
for (const [identifier, newPathToValue] of getValuesFromVarDeclarator(
path,
pathToValue,
)) {
expect(identifier.node.name).toBe("rest2");
expect(newPathToValue).toEqual(["e", "d"]);
}
},
});
});
test("returns the identifier with the new name", () => {
const ast = babel.parse(`let { a, b: c } = ['literally.anything']`)!;
babel.traverse(ast, {
VariableDeclarator(path) {
const pathToValue = ["d", "b"];
for (const [identifier, newPathToValue] of getValuesFromVarDeclarator(
path,
pathToValue,
)) {
expect(identifier.node.name).toBe("c");
expect(newPathToValue).toEqual(["d"]);
}
},
});
});
test("returns empty map when the paths have diverged", () => {
const ast = babel.parse(
`let { a: { b: { d } } } = ['literally.anything']`,
)!;
babel.traverse(ast, {
VariableDeclarator(path) {
const pathToValue = ["c", "b", "a"];
const identifiers = getValuesFromVarDeclarator(path, pathToValue);
expect(identifiers).toEqual(new Map());
},
});
});
test("returns multiple identifiers when ANY_PROPERTY is used and can match multiple values", () => {
const ast = babel.parse(
`let { a: {b, c, d: e, ...rest} } = ['literally.anything']`,
)!;
babel.traverse(ast, {
VariableDeclarator(path) {
const pathToValue = [ANY_PROPERTY, "a"];
const identifiers = getValuesFromVarDeclarator(path, pathToValue);
let iterator = identifiers.entries();
let currentEntry = iterator.next().value;
expect(currentEntry[0].node.name).toBe("b");
expect(currentEntry[1]).toEqual([]);
currentEntry = iterator.next().value;
expect(currentEntry[0].node.name).toBe("c");
expect(currentEntry[1]).toEqual([]);
currentEntry = iterator.next().value;
expect(currentEntry[0].node.name).toBe("e");
expect(currentEntry[1]).toEqual([]);
currentEntry = iterator.next().value;
expect(currentEntry[0].node.name).toBe("rest");
expect(currentEntry[1]).toEqual([ANY_PROPERTY]);
expect(iterator.next().done).toBe(true);
},
});
});
test("returns multiple identifiers when object pattern declares the same value with different names", () => {
const ast = babel.parse(`let { a, a:b, a: {c} } = ['literally.anything']`)!;
babel.traverse(ast, {
VariableDeclarator(path) {
const pathToValue = ["c", "a"];
const identifiers = getValuesFromVarDeclarator(path, pathToValue);
let iterator = identifiers.entries();
let currentEntry = iterator.next().value;
expect(currentEntry[0].node.name).toBe("a");
expect(currentEntry[1]).toEqual(["c"]);
currentEntry = iterator.next().value;
expect(currentEntry[0].node.name).toBe("b");
expect(currentEntry[1]).toEqual(["c"]);
currentEntry = iterator.next().value;
expect(currentEntry[0].node.name).toBe("c");
expect(currentEntry[1]).toEqual([]);
expect(iterator.next().done).toBe(true);
},
});
});
});
describe("getJSXElement", () => {
test("returns JSXElement when pathToSlottable exactly matches jsx element name", () => {
const ast = babel.parse(
`
<a.b.c>children</a.b.c>;
<d.e />;
<f></f>;
`,
{ plugins: ["@babel/syntax-jsx"] },
)!;
babel.traverse(ast, {
JSXOpeningElement(path) {
path.traverse({
JSXIdentifier(path) {
switch (path.node.name) {
case "a": {
let pathToSlottable = ["c", "b"];
let el = getJSXElement(path, pathToSlottable);
expect(babel.types.isJSXElement(el!.node)).toBe(true);
expect(pathToSlottable).toEqual([]);
break;
}
case "d": {
let pathToSlottable = [ANY_PROPERTY];
let el = getJSXElement(path, pathToSlottable);
expect(babel.types.isJSXElement(el!.node)).toBe(true);
expect(pathToSlottable).toEqual([]);
break;
}
case "f": {
let pathToSlottable: any = [];
let el = getJSXElement(path, pathToSlottable);
expect(babel.types.isJSXElement(el!.node)).toBe(true);
expect(pathToSlottable).toEqual([]);
break;
}
}
},
});
},
});
});
test("returns null when performing a match on jSXClosingElement", () => {
const ast = babel.parse(`<a.b.c>children</a.b.c>;`, {
plugins: ["@babel/syntax-jsx"],
})!;
babel.traverse(ast, {
JSXClosingElement(path) {
path.traverse({
JSXIdentifier(path) {
if (path.node.name === "a") {
let pathToSlottable = ["c", "b"];
let el = getJSXElement(path, pathToSlottable);
expect(el).toBeNull();
expect(pathToSlottable).toEqual([]);
}
},
});
},
});
});
test("returns null when JSXMemberExpression goes deeper than specified in pathToSlottable", () => {
const ast = babel.parse(`<a.b.c.d>children</a.b.c.d>;`, {
plugins: ["@babel/syntax-jsx"],
})!;
babel.traverse(ast, {
JSXOpeningElement(path) {
path.traverse({
JSXIdentifier(path) {
if (path.node.name === "a") {
let pathToSlottable = ["c", "b"];
let el = getJSXElement(path, pathToSlottable);
expect(el).toBeNull();
expect(pathToSlottable).toEqual([]);
}
},
});
},
});
});
test("returns null when JSXMemberExpression doesn't match pathToSlottable", () => {
const ast = babel.parse(`<a.b.c>children</a.b.c>;`, {
plugins: ["@babel/syntax-jsx"],
})!;
babel.traverse(ast, {
JSXOpeningElement(path) {
path.traverse({
JSXIdentifier(path) {
if (path.node.name === "a") {
let pathToSlottable = ["d", "e", "f"];
let el = getJSXElement(path, pathToSlottable);
expect(el).toBeNull();
expect(pathToSlottable).toEqual(["d", "e", "f"]);
}
},
});
},
});
});
test("returns null when JSXMemberExpression ends earlier than specified in pathToSlottable", () => {
const ast = babel.parse(`<a.b>children</a.b>;`, {
plugins: ["@babel/syntax-jsx"],
})!;
babel.traverse(ast, {
JSXOpeningElement(path) {
path.traverse({
JSXIdentifier(path) {
if (path.node.name === "a") {
let pathToSlottable = ["c", "b"];
let el = getJSXElement(path, pathToSlottable);
expect(el).toBeNull();
expect(pathToSlottable).toEqual(["c"]);
}
},
});
},
});
});
});
// test.only("test", () => {
// const code = babel.transformSync(
// `import * as A from "@beqa/react-slots"; x(A.useSlot);`,
// {
// plugins: [plugin],
// }
// );
// });