@vue-macros/jsx-directive
Version:
jsxDirective feature from Vue Macros.
491 lines (478 loc) • 17.7 kB
JavaScript
// src/core/index.ts
import {
babelParse,
generateTransform,
getLang,
MagicStringAST,
parseSFC,
REGEX_SETUP_SFC,
walkAST
} from "@vue-macros/common";
// src/core/helper/index.ts
import { VIRTUAL_ID_PREFIX } from "@vue-macros/common";
// src/core/helper/with-defaults.ts?raw
var with_defaults_default = "function resolveDefaultProps(paths){const result={};for(const path of Object.keys(paths)){const segments=path.split(/[?.[\\]]/).filter(Boolean);let current=result;for(let i=0;i<segments.length;i++){const segment=segments[i];if(i===segments.length-1){current[segment]=paths[path]}else{if(!current[segment]){current[segment]=Number.isNaN(Number(segments[i+1]))?{}:[]}current=current[segment]}}}return result}export function createPropsDefaultProxy(props,defaults){const defaultProps=resolveDefaultProps(defaults);const result={};for(const key of[...new Set([...Object.keys(props),...Object.keys(defaultProps)])]){Object.defineProperty(result,key,{enumerable:true,get:()=>props[key]===void 0?defaultProps[key]:props[key]})}return result}\n";
// src/core/helper/index.ts
var helperPrefix = `${VIRTUAL_ID_PREFIX}/jsx-directive`;
var withDefaultsHelperId = `${helperPrefix}/with-defaults`;
// src/core/v-for.ts
import {
HELPER_PREFIX,
importHelperFn
} from "@vue-macros/common";
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] ? s.sliceNode(expressions[0]) : "";
index = expressions[1] ? s.sliceNode(expressions[1]) : "";
objectIndex = expressions[2] ? s.sliceNode(expressions[2]) : "";
} else {
item = s.sliceNode(attribute.value.expression.left);
}
list = s.sliceNode(attribute.value.expression.right);
}
if (item && list) {
if (vMemoAttribute) {
index ??= `${HELPER_PREFIX}index`;
}
const params = `(${item}${index ? `, ${index}` : ""}${objectIndex ? `, ${objectIndex}` : ""})`;
const renderList = importHelperFn(
s,
0,
"renderList",
void 0,
lib.startsWith("vue") ? "vue" : "@vue-macros/jsx-directive/helpers"
);
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.prependRight(
node.end,
`)${hasScope ? vIfAttribute ? "" : "}" : "}</>"}`
);
s.appendLeft(
node.start,
`${hasScope ? vIfAttribute ? "" : "{" : "<>{"}${resolveVFor(attribute, s, options, vMemoAttribute)}`
);
s.remove(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, "");
}
});
}
// src/core/v-html.ts
function transformVHtml(nodes, s) {
nodes.forEach(({ attribute }) => {
s.overwriteNode(attribute.name, "innerHTML");
});
}
// src/core/v-if.ts
function transformVIf(nodes, s, { prefix }) {
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))) {
if (attribute.value)
s.appendLeft(
node.start,
`${hasScope ? "" : "<>{"}${attribute.name.name === `${prefix}if` && hasScope ? "{" : ""}(${s.slice(
attribute.value.start + 1,
attribute.value.end - 1
)}) ? `
);
s.appendRight(
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 ? "}" : "");
}
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, "");
}
s.remove(attribute.start - 1, attribute.end);
});
}
// src/core/v-memo.ts
import {
HELPER_PREFIX as HELPER_PREFIX2,
importHelperFn as importHelperFn2
} from "@vue-macros/common";
function transformVMemo(nodes, s, { lib }) {
if (nodes.length === 0) return;
const withMemo = importHelperFn2(
s,
0,
"withMemo",
void 0,
lib.startsWith("vue") ? "vue" : "@vue-macros/jsx-directive/helpers"
);
s.prependRight(0, `const ${HELPER_PREFIX2}cache = [];`);
nodes.forEach(({ node, attribute, parent, vForAttribute }, nodeIndex) => {
const hasScope = ["JSXElement", "JSXFragment"].includes(
String(parent?.type)
);
s.appendLeft(
node.start,
`${hasScope ? "{" : ""}${withMemo}(${attribute.value ? s.slice(attribute.value.start + 1, attribute.value.end - 1) : `[]`}, () => `
);
let index = String(nodeIndex);
let cache = `${HELPER_PREFIX2}cache`;
let vForIndex = `${HELPER_PREFIX2}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.prependRight(node.end, `, ${cache}, ${index})${hasScope ? "}" : ""}`);
s.remove(attribute.start - 1, attribute.end);
});
}
// src/core/v-model.ts
var 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("_", ".");
const value = s.sliceNode(attribute.value.expression);
modifiers = modifiers ? `, [${argument} + "Modifiers"]: { ${modifiers.split("_").map((key) => `${key}: true`).join(", ")} }` : "";
s.overwriteNode(
attribute,
`{...{[${argument}]: ${value}, ["onUpdate:" + ${argument}]: $event => ${value} = $event${modifiers}}}`
);
}
}
// src/core/v-on.ts
import {
HELPER_PREFIX as HELPER_PREFIX3,
importHelperFn as importHelperFn3
} from "@vue-macros/common";
function transformVOn(nodes, s) {
if (nodes.length > 0)
s.prependRight(
0,
`const ${HELPER_PREFIX3}transformVOn = (obj) => Object.entries(obj).reduce((res, [key, value]) => (res['on' + key[0].toUpperCase() + key.slice(1)] = value, res), {});`
);
nodes.forEach(({ attribute }) => {
s.overwriteNode(
attribute,
`{...${HELPER_PREFIX3}transformVOn(${s.slice(
attribute.value.start + 1,
attribute.value.end - 1
)})}`
);
});
}
function transformOnWithModifiers(nodes, s, { lib }) {
nodes.forEach(({ attribute }) => {
const attributeName = attribute.name.name.toString();
let [name, ...modifiers] = attributeName.split("_");
const withModifiersOrKeys = importHelperFn3(
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.remove(attribute.name.start + name.length, attribute.name.end);
});
}
function isKeyboardEvent(value) {
return ["onKeyup", "onKeydown", "onKeypress"].includes(value);
}
// src/core/v-slot.ts
import { importHelperFn as importHelperFn4 } from "@vue-macros/common";
function transformVSlot(nodeMap, s, options) {
const { prefix, lib } = options;
Array.from(nodeMap).reverse().forEach(([node, { attributeMap, vSlotAttribute }]) => {
const result = [` v-slots={{`];
const attributes = Array.from(attributeMap);
attributes.forEach(
([attribute, { children, vIfAttribute, vForAttribute }], index) => {
if (!attribute) return;
if (vIfAttribute) {
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(`(${s.sliceNode(vIfAttribute.value.expression)}) ? {`);
} else if (`${prefix}else` === vIfAttribute.name.name) {
result.push("{");
}
}
if (vForAttribute) {
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;
return $1.replaceAll("_", ".");
});
result.push(
isDynamic ? `[${importHelperFn4(
s,
0,
"unref",
void 0,
lib.startsWith("vue") ? "vue" : "@vue-macros/jsx-directive/helpers"
)}(${attributeName})]` : `'${attributeName}'`,
vForAttribute ? ", " : ": "
);
const slotFn = [
"(",
attribute.value && attribute.value.type === "JSXExpressionContainer" ? s.sliceNode(attribute.value.expression) : "",
") => ",
"<>",
children.map((child) => {
const str = s.sliceNode(
child.type === "JSXElement" && s.sliceNode(child.openingElement.name) === "template" ? child.children : child
);
s.removeNode(child);
return str;
}).join("") || " ",
"</>"
].join("");
result.push(slotFn, ",");
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 (attributeMap.has(null)) {
result.push(`default: () => <>`);
} else {
result.push("}}");
}
if (vSlotAttribute) {
s.overwriteNode(vSlotAttribute, result.join(""));
} else if (node?.type === "JSXElement") {
s.overwrite(
node.openingElement.end - 1,
node.openingElement.end,
result.join("")
);
s.appendLeft(
node.closingElement.start,
attributeMap.has(null) ? `</>}}>` : ">"
);
}
});
}
// src/core/index.ts
var withDefaultsHelperCode = with_defaults_default;
var 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 (child.type === "JSXElement" && s.sliceNode(child.openingElement.name) === "template" || 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);
}
}
}
});
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);
transformVSlot(vSlotMap, s, options);
}
export {
with_defaults_default,
helperPrefix,
withDefaultsHelperId,
withDefaultsHelperCode,
transformJsxDirective
};