UNPKG

@vue-macros/jsx-directive

Version:

jsxDirective feature from Vue Macros.

325 lines (316 loc) 14.5 kB
import { HELPER_PREFIX, MagicStringAST, REGEX_SETUP_SFC, babelParse, generateTransform, getLang, importHelperFn, parseSFC, walkAST } from "@vue-macros/common"; //#region src/core/v-for.ts function resolveVFor(attribute, s, { lib }, vMemoAttribute) { if (attribute.value) { let item, index, objectIndex, list; if (attribute.value.type === "JSXExpressionContainer" && attribute.value.expression.type === "BinaryExpression") { if (attribute.value.expression.left.type === "SequenceExpression") { const expressions = attribute.value.expression.left.expressions; item = expressions[0] ? expressions[0] : ""; index = expressions[1] ? expressions[1] : ""; objectIndex = expressions[2] ? expressions[2] : ""; } else item = attribute.value.expression.left; list = attribute.value.expression.right; } if (item && list) { if (vMemoAttribute) index ??= `${HELPER_PREFIX}index`; const renderList = importHelperFn(s, 0, "renderList", void 0, lib.startsWith("vue") ? "vue" : "@vue-macros/jsx-directive/helpers"); const params = [ item, index, objectIndex ].flatMap((param, index$1) => param ? index$1 ? [", ", param] : param : []); return [ renderList, "(", list, ", (", ...params, ") => " ]; } } return []; } function transformVFor(nodes, s, options) { if (nodes.length === 0) return; nodes.forEach(({ node, attribute, parent, vIfAttribute, vMemoAttribute }) => { const hasScope = ["JSXElement", "JSXFragment"].includes(String(parent?.type)); s.replaceRange(node.start, node.start, hasScope ? vIfAttribute ? "" : "{" : "<>{", ...resolveVFor(attribute, s, options, vMemoAttribute)); s.prependLeft(node.end, `)${hasScope ? vIfAttribute ? "" : "}" : "}</>"}`); s.replaceRange(attribute.start - 1, attribute.end); const isTemplate = node.type === "JSXElement" && node.openingElement.name.type === "JSXIdentifier" && node.openingElement.name.name === "template"; if (isTemplate && node.closingElement) { s.overwriteNode(node.openingElement.name, ""); s.overwriteNode(node.closingElement.name, ""); } }); } //#endregion //#region src/core/v-html.ts function transformVHtml(nodes, s) { nodes.forEach(({ attribute }) => { s.overwriteNode(attribute.name, "innerHTML"); }); } //#endregion //#region src/core/v-if.ts function transformVIf(nodes, s, options) { const { prefix } = options; nodes.forEach(({ node, attribute, parent }, index) => { const hasScope = ["JSXElement", "JSXFragment"].includes(String(parent?.type)); if ([`${prefix}if`, `${prefix}else-if`].includes(String(attribute.name.name)) && attribute.value?.type === "JSXExpressionContainer") { s.replaceRange(node.start, node.start, hasScope ? "" : "<>{", attribute.name.name === `${prefix}if` && hasScope ? "{" : "", "(", attribute.value.expression, ") ? "); s.replaceRange(node.end, node.end, String(nodes[index + 1]?.attribute.name.name).startsWith(`${prefix}else`) ? " :" : ` : null${hasScope ? "}" : "}</>"}`); } else if (attribute.name.name === `${prefix}else`) s.appendRight(node.end, hasScope ? "}" : ""); s.replaceRange(attribute.start - 1, attribute.end); const isTemplate = node.type === "JSXElement" && node.openingElement.name.type === "JSXIdentifier" && node.openingElement.name.name === "template"; if (isTemplate && node.closingElement) { s.overwriteNode(node.openingElement.name, ""); s.overwriteNode(node.closingElement.name, ""); } }); } //#endregion //#region src/core/v-memo.ts function transformVMemo(nodes, s, { lib }) { if (nodes.length === 0) return; const withMemo = importHelperFn(s, 0, "withMemo", void 0, lib.startsWith("vue") ? "vue" : "@vue-macros/jsx-directive/helpers"); s.prependRight(0, `const ${HELPER_PREFIX}cache = [];`); nodes.forEach(({ node, attribute, parent, vForAttribute }, nodeIndex) => { const hasScope = ["JSXElement", "JSXFragment"].includes(String(parent?.type)); s.appendRight(node.start, `${hasScope ? "{" : ""}${withMemo}(${attribute.value ? s.slice(attribute.value.start + 1, attribute.value.end - 1) : `[]`}, () => `); let index = String(nodeIndex); let cache = `${HELPER_PREFIX}cache`; let vForIndex = `${HELPER_PREFIX}index`; if (vForAttribute?.value?.type === "JSXExpressionContainer") { if (vForAttribute.value.expression.type === "BinaryExpression" && vForAttribute.value.expression.left.type === "SequenceExpression" && vForAttribute.value.expression.left.expressions[1].type === "Identifier") vForIndex = vForAttribute.value.expression.left.expressions[1].name; cache += `[${index}]`; s.appendRight(0, `${cache} = [];`); index += ` + ${vForIndex} + 1`; } s.prependLeft(node.end, `, ${cache}, ${index})${hasScope ? "}" : ""}`); s.remove(attribute.start - 1, attribute.end); }); } //#endregion //#region src/core/v-model.ts const dynamicModelRE = /^\$(.*)\$(?:_(.*))?/; function transformVModel(attribute, s) { if (attribute.name.type === "JSXNamespacedName" && attribute.value?.type === "JSXExpressionContainer") { const matched = attribute.name.name.name.match(dynamicModelRE); if (!matched) return; let [, argument, modifiers] = matched; argument = argument.replaceAll("_", "."); modifiers = modifiers ? `, [${argument} + "Modifiers"]: { ${modifiers.split("_").map((key) => `${key}: true`).join(", ")} }` : ""; s.replaceRange(attribute.start, attribute.end, `{...{[${argument}]: `, attribute.value.expression, `, ["onUpdate:" + ${argument}]: $event => `, s.sliceNode(attribute.value.expression), ` = $event${modifiers}}}`); } } //#endregion //#region src/core/v-on.ts function transformVOn(nodes, s) { if (nodes.length > 0) s.prependRight(0, `const ${HELPER_PREFIX}transformVOn = (obj) => Object.entries(obj).reduce((res, [key, value]) => (res['on' + key[0].toUpperCase() + key.slice(1)] = value, res), {});`); nodes.forEach(({ attribute }) => { if (attribute.value?.type === "JSXExpressionContainer") s.replaceRange(attribute.start, attribute.end, `{...${HELPER_PREFIX}transformVOn(`, attribute.value.expression, `)}`); }); } function transformOnWithModifiers(nodes, s, { lib }) { nodes.forEach(({ attribute }) => { const attributeName = attribute.name.name.toString(); let [name, ...modifiers] = attributeName.split("_"); const withModifiersOrKeys = importHelperFn(s, 0, isKeyboardEvent(name) ? "withKeys" : "withModifiers", void 0, lib.startsWith("vue") ? "vue" : "@vue-macros/jsx-directive/helpers"); modifiers = modifiers.filter((modifier) => { if (modifier === "capture") { s.appendRight(attribute.name.end, modifier[0].toUpperCase() + modifier.slice(1)); return false; } else return true; }); const result = `, [${modifiers.map((modifier) => `'${modifier}'`)}])`; if (attribute.value?.type === "JSXExpressionContainer") { s.appendRight(attribute.value.expression.start, `${withModifiersOrKeys}(`); s.appendLeft(attribute.value.expression.end, result); } else s.appendRight(attribute.name.end, `={${withModifiersOrKeys}(() => {}${result}}`); s.replaceRange(attribute.name.start + name.length, attribute.name.end); }); } function isKeyboardEvent(value) { return [ "onKeyup", "onKeydown", "onKeypress" ].includes(value); } //#endregion //#region src/core/v-slot.ts function isSlotTemplate(child, { prefix }) { return child.type === "JSXElement" && child.openingElement.name.type === "JSXIdentifier" && child.openingElement.name.name === "template" && child.openingElement.attributes.some((attribute) => attribute.type === "JSXAttribute" && (attribute.name.type === "JSXNamespacedName" ? attribute.name.namespace : attribute.name).name === `${prefix}slot`); } function transformVSlot(nodeMap, s, options) { const { prefix, lib } = options; Array.from(nodeMap).forEach(([node, { attributeMap, vSlotAttribute }]) => { const result = [` v-slots={{`]; const attributes = Array.from(attributeMap); let isStable = lib === "vue"; const removeNodes = []; attributes.forEach(([attribute, { children, vIfAttribute, vForAttribute }], index) => { if (!attribute) return; if (vIfAttribute) { isStable = false; if (`${prefix}if` === vIfAttribute.name.name) result.push("..."); if ([`${prefix}if`, `${prefix}else-if`].includes(String(vIfAttribute.name.name)) && vIfAttribute.value?.type === "JSXExpressionContainer") result.push("(", vIfAttribute.value.expression, ") ? {"); else if (`${prefix}else` === vIfAttribute.name.name) result.push("{"); } if (vForAttribute) { isStable = false; result.push("...Object.fromEntries(", ...resolveVFor(vForAttribute, s, { ...options, lib: "vue" }), "(["); } let isDynamic = false; let attributeName = attribute.name.type === "JSXNamespacedName" ? attribute.name.name.name : "default"; attributeName = attributeName.replace(/\$(.*)\$/, (_, $1) => { isDynamic = true; isStable = false; return $1.replaceAll("_", "."); }); result.push(isDynamic ? `[${attributeName}]` : `'${attributeName}'`, vForAttribute ? ", " : ": ", "(", attribute.value && attribute.value.type === "JSXExpressionContainer" ? attribute.value.expression : "", ") => ", "<>", ...children.flatMap((child) => { removeNodes.push(child); return isSlotTemplate(child, options) ? child.children : child; }) || [], "</>,"); if (vForAttribute) result.push("]))),"); if (vIfAttribute) { if ([`${prefix}if`, `${prefix}else-if`].includes(String(vIfAttribute.name.name))) { const nextIndex = index + (attributes[index + 1]?.[0] ? 1 : 2); result.push("}", String(attributes[nextIndex]?.[1].vIfAttribute?.name.name).startsWith(`${prefix}else`) ? " : " : " : null,"); } else if (`${prefix}else` === vIfAttribute.name.name) result.push("},"); } }); if (isStable) result.push("$stable: true,"); if (attributeMap.has(null)) result.push(`default: () => <>`); else result.push("}}"); if (vSlotAttribute) s.replaceRange(vSlotAttribute.start, vSlotAttribute.end, ...result); else if (node?.type === "JSXElement") { s.replaceRange(node.openingElement.end - 1, node.openingElement.end, ...result); s.appendLeft(node.closingElement.start, attributeMap.has(null) ? `</>}}>` : ">"); } removeNodes.forEach((node$1) => s.replaceRange(node$1.start, node$1.end)); }); } //#endregion //#region src/core/index.ts const onWithModifiersRegex = /^on[A-Z]\S*_\S+/; function transformJsxDirective(code, id, options) { const lang = getLang(id); const programs = []; if (lang === "vue" || REGEX_SETUP_SFC.test(id)) { const { scriptSetup, getSetupAst, script, getScriptAst } = parseSFC(code, id); if (script) programs.push([getScriptAst(), script.loc.start.offset]); if (scriptSetup) programs.push([getSetupAst(), scriptSetup.loc.start.offset]); } else if (["jsx", "tsx"].includes(lang)) programs.push([babelParse(code, lang), 0]); else return; const s = new MagicStringAST(code); for (const [ast, offset] of programs) { s.offset = offset; transform(s, ast, options); } return generateTransform(s, id); } function transform(s, program, options) { const { prefix, version } = options; const vIfMap = /* @__PURE__ */ new Map(); const vForNodes = []; const vMemoNodes = []; const vHtmlNodes = []; const vSlotMap = /* @__PURE__ */ new Map(); const vOnNodes = []; const onWithModifiers = []; walkAST(program, { enter(node, parent) { if (node.type !== "JSXElement") return; const tagName = s.sliceNode(node.openingElement.name); let vIfAttribute; let vForAttribute; let vMemoAttribute; let vSlotAttribute; for (const attribute of node.openingElement.attributes) { if (attribute.type !== "JSXAttribute") continue; if ([ `${prefix}if`, `${prefix}else-if`, `${prefix}else` ].includes(String(attribute.name.name))) vIfAttribute = attribute; else if (attribute.name.name === `${prefix}for`) vForAttribute = attribute; else if ([`${prefix}memo`, `${prefix}once`].includes(String(attribute.name.name))) vMemoAttribute = attribute; else if (attribute.name.name === `${prefix}html`) vHtmlNodes.push({ node, attribute }); else if ((attribute.name.type === "JSXNamespacedName" ? attribute.name.namespace : attribute.name).name === `${prefix}slot`) vSlotAttribute = attribute; else if (attribute.name.name === `${prefix}on`) vOnNodes.push({ node, attribute }); else if (onWithModifiersRegex.test(String(attribute.name.name))) onWithModifiers.push({ node, attribute }); else if (attribute.name.type === "JSXNamespacedName" && attribute.name.namespace.name === `${prefix}model`) transformVModel(attribute, s); } if (!vSlotAttribute || tagName !== "template") { if (vIfAttribute) { vIfMap.get(parent) || vIfMap.set(parent, []); vIfMap.get(parent).push({ node, attribute: vIfAttribute, parent }); } if (vForAttribute) vForNodes.unshift({ node, attribute: vForAttribute, vIfAttribute, parent, vMemoAttribute }); } if (vMemoAttribute) vMemoNodes.push({ node, attribute: vMemoAttribute, parent: vForAttribute || vIfAttribute ? void 0 : parent, vForAttribute }); if (vSlotAttribute) { const slotNode = tagName === "template" ? parent : node; if (slotNode?.type !== "JSXElement") return; const attributeMap = vSlotMap.get(slotNode)?.attributeMap || vSlotMap.set(slotNode, { vSlotAttribute: tagName !== "template" ? vSlotAttribute : void 0, attributeMap: /* @__PURE__ */ new Map() }).get(slotNode).attributeMap; const children = attributeMap.get(vSlotAttribute)?.children || attributeMap.set(vSlotAttribute, { children: [], ...tagName === "template" ? { vIfAttribute, vForAttribute } : {} }).get(vSlotAttribute).children; if (slotNode === parent) { children.push(node); if (attributeMap.get(null)) return; for (const child of parent.children) { if (isSlotTemplate(child, options) || child.type === "JSXText" && !s.sliceNode(child).trim()) continue; const defaultNodes = attributeMap.get(null)?.children || attributeMap.set(null, { children: [] }).get(null).children; defaultNodes.push(child); } } else children.push(...node.children); } } }); transformVSlot(vSlotMap, s, options); vIfMap.forEach((nodes) => transformVIf(nodes, s, options)); transformVFor(vForNodes, s, options); if (!version || version >= 3.2) transformVMemo(vMemoNodes, s, options); transformVHtml(vHtmlNodes, s); transformVOn(vOnNodes, s); transformOnWithModifiers(onWithModifiers, s, options); } //#endregion export { transformJsxDirective };