@beqa/babel-plugin-transform-react-slots
Version:
The JSX to slot function transpilation plugin for babel
476 lines (470 loc) • 17.2 kB
JavaScript
var __getOwnPropNames = Object.getOwnPropertyNames;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
// src/rawCodeStore.ts
var _rawCode, rawCodeStore_default;
var init_rawCodeStore = __esm({
"src/rawCodeStore.ts"() {
"use strict";
rawCodeStore_default = {
get() {
return _rawCode;
},
set(rawCode) {
return _rawCode = rawCode;
}
};
}
});
// src/errors.ts
import { codeFrameColumns } from "@babel/code-frame";
var UnsupportedSyntaxError, VarDeclarationError, UnsupportedMemberExpressionError, JSXNamespacedNameError;
var init_errors = __esm({
"src/errors.ts"() {
"use strict";
init_rawCodeStore();
UnsupportedSyntaxError = class extends Error {
codeFrame;
constructor(location) {
super();
this.codeFrame = location ? codeFrameColumns(rawCodeStore_default.get(), location) : "";
}
throw(message) {
throw new Error(
`${message}
${this.codeFrame}. This restriction ensures slottable values remain unaltered until used as JSX elements. To prevent compilation for a specific file, add the comment \`disable-transform-react-slots\` at the file's top and use an alternative call signature for the slots.`
);
}
};
VarDeclarationError = class extends UnsupportedSyntaxError {
kind;
constructor(kind, location) {
super(location);
this.kind = kind;
}
};
UnsupportedMemberExpressionError = class extends UnsupportedSyntaxError {
};
JSXNamespacedNameError = class extends UnsupportedSyntaxError {
};
}
});
// src/utils.ts
import { types as t } from "@babel/core";
function isIdentifierWithName(node, name) {
return t.isIdentifier(node) && (name === ANY_PROPERTY ? true : node.name === name);
}
function isJSXIdentifierWithName(node, name) {
return t.isJSXIdentifier(node) && (name === ANY_PROPERTY ? true : node.name === name);
}
function isStringLiteralWithValue(node, value) {
return t.isStringLiteral(node) && (value === ANY_PROPERTY ? true : node.value === value);
}
function skipTS(nodePath) {
if (nodePath === null) {
return null;
}
if (t.isTSAsExpression(nodePath.node) || t.isTSTypeAssertion(nodePath.node) || t.isTSSatisfiesExpression(nodePath.node) || t.isTSInstantiationExpression(nodePath.node)) {
return skipTS(nodePath.parentPath);
}
return nodePath;
}
function isUnbreaking(nodePath) {
if (t.isLogicalExpression(nodePath.node) || t.isIfStatement(nodePath.node) || t.isExpressionStatement(nodePath.node)) {
return true;
}
return false;
}
function goThroughMemberExpression(identifier, pathToValue) {
let parent = skipTS(identifier.parentPath);
let child = skipTS(identifier);
if (parent === null) {
return null;
}
while (t.isMemberExpression(parent.node)) {
const name = pathToValue.at(-1);
if (name === void 0) {
return null;
}
if (!parent.node.computed && isIdentifierWithName(parent.node.property, name) || parent.node.computed && isStringLiteralWithValue(parent.node.property, name)) {
pathToValue.pop();
child = parent;
parent = skipTS(parent.parentPath);
} else if (!parent.node.computed && t.isIdentifier(parent.node.property) || t.isStringLiteral(parent.node.property)) {
return null;
} else {
throw new UnsupportedMemberExpressionError(parent.node.loc);
}
}
return child;
}
function getValuesFromObjectPattern(objectPattern, pathToValue, identifiers) {
let restHasPath = true;
for (let prop of objectPattern.get("properties")) {
if (t.isRestElement(prop.node)) {
if (restHasPath || pathToValue.at(-1) === ANY_PROPERTY) {
const identifier = prop.get("argument");
identifiers.set(identifier, pathToValue.slice());
}
break;
}
if (pathToValue.length > 0 && t.isObjectProperty(prop.node) && isIdentifierWithName(prop.node.key, pathToValue.at(-1))) {
restHasPath = false;
const name = pathToValue.pop();
if (t.isIdentifier(prop.node.value)) {
identifiers.set(
prop.get("value"),
pathToValue.slice()
);
pathToValue.push(name);
continue;
}
if (t.isObjectPattern(prop.node.value)) {
getValuesFromObjectPattern(
prop.get("value"),
pathToValue,
identifiers
);
pathToValue.push(name);
}
}
}
}
function getValuesFromVarDeclarator(varDeclarator, pathToValue) {
const identifiers = /* @__PURE__ */ new Map();
const varDeclaration = varDeclarator.parent;
if (!["const", "let"].includes(varDeclaration.kind)) {
throw new VarDeclarationError(varDeclaration.kind, varDeclaration.loc);
}
if (t.isIdentifier(varDeclarator.node.id)) {
identifiers.set(
varDeclarator.get("id"),
pathToValue.slice()
);
} else if (t.isObjectPattern(varDeclarator.node.id)) {
getValuesFromObjectPattern(
varDeclarator.get("id"),
pathToValue,
identifiers
);
}
return identifiers;
}
function getJSXElement(path, pathToSlottable) {
let parent = path.parentPath;
while (t.isJSXMemberExpression(parent.node)) {
const name = pathToSlottable.at(-1);
if (name === void 0) {
return null;
}
if (!isJSXIdentifierWithName(parent.node.property, name)) {
return null;
}
pathToSlottable.pop();
parent = parent.parentPath;
}
if (pathToSlottable.length > 0) {
return null;
}
if (t.isJSXClosingElement(parent.node)) {
return null;
}
return parent.parentPath;
}
function jsxNameToCallee(el) {
if (t.isJSXIdentifier(el)) {
return t.identifier(el.name);
}
if (t.isJSXNamespacedName(el)) {
throw new JSXNamespacedNameError(el.loc);
}
return t.memberExpression(
jsxNameToCallee(el.object),
t.identifier(el.property.name),
false
);
}
var ANY_PROPERTY;
var init_utils = __esm({
"src/utils.ts"() {
"use strict";
init_errors();
ANY_PROPERTY = Symbol("AnyProperty");
}
});
// src/index.ts
import { types as t2 } from "@babel/core";
import { declare } from "@babel/helper-plugin-utils";
import syntaxJSX from "@babel/plugin-syntax-jsx";
var require_src = __commonJS({
"src/index.ts"(exports, module) {
init_utils();
init_rawCodeStore();
init_errors();
var LIB_SOURCE = "@beqa/react-slots";
var IMPORTED_NODE = "useSlot";
var SLOT_OBJECT_NAME = "slot";
var DISABLED_REGEX = /^\s*@disable-transform-react-slots\W/;
var DEFAULT_CONTENT_WRAPPER = "default-content-wrapper";
function isDisabled(comments, program) {
if (!comments) {
return false;
}
const programStart = program.directives.length ? program.directives[0]?.start || 0 : program.body.length ? program.body[0]?.start || 0 : 0;
let currentIndex = 0;
while (comments[currentIndex] && comments[currentIndex].start !== void 0 && comments[currentIndex].start <= programStart) {
if (comments[currentIndex]?.type === "CommentLine" && DISABLED_REGEX.test(comments[currentIndex].value)) {
return true;
}
++currentIndex;
}
if (comments[currentIndex] && comments[currentIndex]?.start === void 0) {
throw new Error(
"Could not read comments for @beqa/react-slots while looking for a `@disable-transform-react-slots` pragma. Please open a new issue on our Github repo."
);
}
return false;
}
function findImports(path) {
const importNodes = path.node.body.filter((node) => {
return t2.isImportDeclaration(node) && node.source.value === LIB_SOURCE && node.importKind !== "type" && node.importKind !== "typeof";
});
let imports = [];
let namespaceImports = [];
for (let node of importNodes) {
for (let specifier of node.specifiers) {
if (t2.isImportNamespaceSpecifier(specifier)) {
namespaceImports.push(specifier);
} else if (t2.isImportSpecifier(specifier) && specifier.imported.name === IMPORTED_NODE && specifier.importKind !== "typeof" && specifier.importKind !== "type") {
imports.push(specifier);
}
}
}
return [imports, namespaceImports];
}
function findCallExpressions(referencePaths, pathToUseSlot = [], callExpressions) {
for (const ref of referencePaths) {
const path = pathToUseSlot.slice();
let nodePath;
try {
nodePath = goThroughMemberExpression(ref, path);
} catch (err) {
if (err instanceof UnsupportedMemberExpressionError) {
err.throw(
`Unsupported syntax: Member expression accessing a \`useSlot\` value can only use dot notation or bracket notation (iff the value is a string literal). Allowed syntax is: \`ReactSlots.useSlot\` or \`ReactSlots["useSlot"]\`.`
);
}
throw err;
}
if (nodePath === null) {
continue;
}
let parentPath = skipTS(nodePath.parentPath);
if (isUnbreaking(parentPath)) {
continue;
}
if (t2.isCallExpression(parentPath.node) && skipTS(parentPath.get("callee"))?.node === nodePath.node) {
if (path.length !== 0) {
new UnsupportedSyntaxError(parentPath.node.loc).throw(
"Unsupported syntax: Object that holds the nested `useSlot` value was used as a function. Did you mean to do `" + rawCodeStore_default.get().slice(
parentPath.node.callee.start || 0,
parentPath.node.callee.end || 0
) + "." + path.reverse().join(".") + "()`?"
);
}
callExpressions.push(parentPath);
} else if (t2.isVariableDeclarator(parentPath.node)) {
try {
const newIdentifiers = getValuesFromVarDeclarator(
parentPath,
path
);
if (newIdentifiers.size) {
for (const [newIdentifier, newPath] of newIdentifiers) {
findCallExpressions(
newIdentifier.scope.bindings[newIdentifier.node.name].referencePaths,
newPath,
callExpressions
);
}
}
} catch (err) {
if (err instanceof VarDeclarationError) {
err.throw(
"Unsupported syntax: You must only use `let` or `const` variable declarations with `useSlot`, instead encountered " + err.kind + "."
);
}
throw err;
}
} else {
new UnsupportedSyntaxError(parentPath.node.loc).throw(
"Unsupported syntax: `useSlot` or an object holding a nested `useSlot` value used inside " + parentPath.node.type + "."
);
}
}
}
function findJSXElements(referencePaths, pathToSlottable, jsxElements) {
for (let ref of referencePaths) {
const path = pathToSlottable.slice();
if (t2.isJSXIdentifier(ref.node)) {
const jsxElement = getJSXElement(ref, path);
if (jsxElement) {
jsxElements.push(jsxElement);
}
} else {
let nodePath;
try {
nodePath = goThroughMemberExpression(ref, path);
} catch (err) {
if (err instanceof UnsupportedMemberExpressionError) {
err.throw(
'Unsupported syntax: Member expression accessing a slottable node can only use dot notation or bracket notation (iff the value is a string literal). eg: `useSlot().slot.default` or `useSlot()["slot"].default`.'
);
}
}
if (nodePath === null || nodePath === void 0) {
continue;
}
let parentPath = skipTS(nodePath.parentPath);
if (t2.isCallExpression(parentPath.node) && path.length === 0 && skipTS(parentPath.get("callee"))?.node === nodePath.node) {
continue;
}
if (isUnbreaking(parentPath)) {
continue;
}
if (t2.isVariableDeclarator(parentPath.node)) {
try {
const newIdentifiers = getValuesFromVarDeclarator(
parentPath,
path
);
if (newIdentifiers.size) {
for (const [newIdentifier, newPath] of newIdentifiers) {
const { scope, node } = newIdentifier;
findJSXElements(
scope.bindings[node.name].referencePaths,
newPath,
jsxElements
);
}
}
} catch (err) {
if (err instanceof VarDeclarationError) {
err.throw(
"Unsupported syntax: You must only use `let` or `const` variable declarations for slottable elements, instead encountered " + err.kind
);
}
throw err;
}
} else {
new UnsupportedSyntaxError(parentPath.node.loc).throw(
"Unsupported syntax: A slottable element or an object holding a nested slottable element used inside " + parentPath.node.type
);
}
}
}
}
function transformJSXElements(element) {
const defaultContent = element.node.children;
const props = element.node.openingElement.attributes.map((attr) => {
if (t2.isJSXSpreadAttribute(attr)) {
return t2.spreadElement(attr.argument);
}
return t2.objectProperty(
t2.isJSXNamespacedName(attr.name) ? t2.identifier(`${attr.name.namespace}:${attr.name.name}`) : t2.identifier(attr.name.name),
// I don't think attr.value can ever be JSXEmptyExpression or
// attr.value.expression can be undefined
// but it's there in ts declarations so we are handling it.
t2.isJSXExpressionContainer(attr.value) ? t2.isJSXEmptyExpression(attr.value.expression) ? t2.identifier("undefined") : attr.value.expression : attr.value === null || attr.value === void 0 ? t2.booleanLiteral(true) : attr.value
);
});
const callExpression = t2.callExpression(
jsxNameToCallee(element.node.openingElement.name),
[
defaultContent.length ? t2.jsxElement(
t2.jsxOpeningElement(t2.jsxIdentifier(DEFAULT_CONTENT_WRAPPER), []),
t2.jsxClosingElement(t2.jsxIdentifier(DEFAULT_CONTENT_WRAPPER)),
defaultContent
) : t2.jsxElement(
t2.jsxOpeningElement(
t2.jsxIdentifier(DEFAULT_CONTENT_WRAPPER),
[],
true
),
null,
[],
true
),
props.length && t2.objectExpression(props)
].filter(Boolean)
);
element.replaceWith(
t2.isJSXElement(element.parent) || t2.isJSXFragment(element.parent) ? t2.inherits(t2.jSXExpressionContainer(callExpression), element.node) : t2.inherits(callExpression, element.node)
);
}
module.exports = declare((api) => {
api.assertVersion(7);
const jsxElementSet = /* @__PURE__ */ new Set();
return {
name: "transform-react-slots",
inherits: syntaxJSX,
pre(file) {
rawCodeStore_default.set(file.code);
jsxElementSet.clear();
},
post() {
rawCodeStore_default.set("");
},
visitor: {
Program: {
enter(path, state) {
if (isDisabled(state.file.ast.comments, path.node)) {
return;
}
const [imports, namespaceImports] = findImports(path);
if (!imports.length && !namespaceImports.length) {
return;
}
const callExpressions = [];
for (const importNode of imports) {
findCallExpressions(
path.scope.bindings[importNode.local.name].referencePaths,
[],
callExpressions
);
}
for (const importNode of namespaceImports) {
findCallExpressions(
path.scope.bindings[importNode.local.name].referencePaths,
[IMPORTED_NODE],
callExpressions
);
}
const jsxElements = [];
findJSXElements(
callExpressions,
[ANY_PROPERTY, SLOT_OBJECT_NAME],
jsxElements
);
jsxElements.forEach((path2) => {
jsxElementSet.add(path2.node);
});
}
},
JSXElement: {
enter(path) {
if (jsxElementSet.has(path.node)) {
transformJSXElements(path);
}
}
}
}
};
});
}
});
export default require_src();