UNPKG

@beqa/babel-plugin-transform-react-slots

Version:

The JSX to slot function transpilation plugin for babel

476 lines (470 loc) 17.2 kB
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();