@beqa/babel-plugin-transform-react-slots
Version:
The JSX to slot function transpilation plugin for babel
471 lines (462 loc) • 17.4 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
// src/index.ts
var import_core2 = require("@babel/core");
var import_helper_plugin_utils = require("@babel/helper-plugin-utils");
// src/utils.ts
var import_core = require("@babel/core");
// src/errors.ts
var import_code_frame = require("@babel/code-frame");
// src/rawCodeStore.ts
var _rawCode;
var rawCodeStore_default = {
get() {
return _rawCode;
},
set(rawCode) {
return _rawCode = rawCode;
}
};
// src/errors.ts
var UnsupportedSyntaxError = class extends Error {
codeFrame;
constructor(location) {
super();
this.codeFrame = location ? (0, import_code_frame.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.`
);
}
};
var VarDeclarationError = class extends UnsupportedSyntaxError {
kind;
constructor(kind, location) {
super(location);
this.kind = kind;
}
};
var UnsupportedMemberExpressionError = class extends UnsupportedSyntaxError {
};
var JSXNamespacedNameError = class extends UnsupportedSyntaxError {
};
// src/utils.ts
var ANY_PROPERTY = Symbol("AnyProperty");
function isIdentifierWithName(node, name) {
return import_core.types.isIdentifier(node) && (name === ANY_PROPERTY ? true : node.name === name);
}
function isJSXIdentifierWithName(node, name) {
return import_core.types.isJSXIdentifier(node) && (name === ANY_PROPERTY ? true : node.name === name);
}
function isStringLiteralWithValue(node, value) {
return import_core.types.isStringLiteral(node) && (value === ANY_PROPERTY ? true : node.value === value);
}
function skipTS(nodePath) {
if (nodePath === null) {
return null;
}
if (import_core.types.isTSAsExpression(nodePath.node) || import_core.types.isTSTypeAssertion(nodePath.node) || import_core.types.isTSSatisfiesExpression(nodePath.node) || import_core.types.isTSInstantiationExpression(nodePath.node)) {
return skipTS(nodePath.parentPath);
}
return nodePath;
}
function isUnbreaking(nodePath) {
if (import_core.types.isLogicalExpression(nodePath.node) || import_core.types.isIfStatement(nodePath.node) || import_core.types.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 (import_core.types.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 && import_core.types.isIdentifier(parent.node.property) || import_core.types.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 (import_core.types.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 && import_core.types.isObjectProperty(prop.node) && isIdentifierWithName(prop.node.key, pathToValue.at(-1))) {
restHasPath = false;
const name = pathToValue.pop();
if (import_core.types.isIdentifier(prop.node.value)) {
identifiers.set(
prop.get("value"),
pathToValue.slice()
);
pathToValue.push(name);
continue;
}
if (import_core.types.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 (import_core.types.isIdentifier(varDeclarator.node.id)) {
identifiers.set(
varDeclarator.get("id"),
pathToValue.slice()
);
} else if (import_core.types.isObjectPattern(varDeclarator.node.id)) {
getValuesFromObjectPattern(
varDeclarator.get("id"),
pathToValue,
identifiers
);
}
return identifiers;
}
function getJSXElement(path, pathToSlottable) {
let parent = path.parentPath;
while (import_core.types.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 (import_core.types.isJSXClosingElement(parent.node)) {
return null;
}
return parent.parentPath;
}
function jsxNameToCallee(el) {
if (import_core.types.isJSXIdentifier(el)) {
return import_core.types.identifier(el.name);
}
if (import_core.types.isJSXNamespacedName(el)) {
throw new JSXNamespacedNameError(el.loc);
}
return import_core.types.memberExpression(
jsxNameToCallee(el.object),
import_core.types.identifier(el.property.name),
false
);
}
// src/index.ts
var import_plugin_syntax_jsx = __toESM(require("@babel/plugin-syntax-jsx"));
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 import_core2.types.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 (import_core2.types.isImportNamespaceSpecifier(specifier)) {
namespaceImports.push(specifier);
} else if (import_core2.types.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 (import_core2.types.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 (import_core2.types.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 (import_core2.types.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 (import_core2.types.isCallExpression(parentPath.node) && path.length === 0 && skipTS(parentPath.get("callee"))?.node === nodePath.node) {
continue;
}
if (isUnbreaking(parentPath)) {
continue;
}
if (import_core2.types.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 (import_core2.types.isJSXSpreadAttribute(attr)) {
return import_core2.types.spreadElement(attr.argument);
}
return import_core2.types.objectProperty(
import_core2.types.isJSXNamespacedName(attr.name) ? import_core2.types.identifier(`${attr.name.namespace}:${attr.name.name}`) : import_core2.types.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.
import_core2.types.isJSXExpressionContainer(attr.value) ? import_core2.types.isJSXEmptyExpression(attr.value.expression) ? import_core2.types.identifier("undefined") : attr.value.expression : attr.value === null || attr.value === void 0 ? import_core2.types.booleanLiteral(true) : attr.value
);
});
const callExpression = import_core2.types.callExpression(
jsxNameToCallee(element.node.openingElement.name),
[
defaultContent.length ? import_core2.types.jsxElement(
import_core2.types.jsxOpeningElement(import_core2.types.jsxIdentifier(DEFAULT_CONTENT_WRAPPER), []),
import_core2.types.jsxClosingElement(import_core2.types.jsxIdentifier(DEFAULT_CONTENT_WRAPPER)),
defaultContent
) : import_core2.types.jsxElement(
import_core2.types.jsxOpeningElement(
import_core2.types.jsxIdentifier(DEFAULT_CONTENT_WRAPPER),
[],
true
),
null,
[],
true
),
props.length && import_core2.types.objectExpression(props)
].filter(Boolean)
);
element.replaceWith(
import_core2.types.isJSXElement(element.parent) || import_core2.types.isJSXFragment(element.parent) ? import_core2.types.inherits(import_core2.types.jSXExpressionContainer(callExpression), element.node) : import_core2.types.inherits(callExpression, element.node)
);
}
module.exports = (0, import_helper_plugin_utils.declare)((api) => {
api.assertVersion(7);
const jsxElementSet = /* @__PURE__ */ new Set();
return {
name: "transform-react-slots",
inherits: import_plugin_syntax_jsx.default,
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);
}
}
}
}
};
});