UNPKG

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

Version:

The JSX to slot function transpilation plugin for babel

471 lines (462 loc) 17.4 kB
"use strict"; 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); } } } } }; });