@vue-jsx-vapor/macros
Version:
Macros for Vue JSX Vapor
299 lines (292 loc) • 14.8 kB
JavaScript
import { resolveOptions } from "./options-BWRkHmm5.js";
import { REGEX_VUE_SFC, createFilter } from "@vue-macros/common";
import { allCodeFeatures, createPlugin, getText } from "ts-macro";
//#region src/volar/define-component.ts
function transformDefineComponent(node, parent, options) {
const { codes, ast, ts } = options;
const [comp, compOptions] = node.arguments;
const isVapor = node.expression.getText(ast) === "defineVaporComponent";
codes.replaceRange(comp.end, node.end - 1);
codes.replaceRange(node.getStart(ast), node.expression.end, ts.isExpressionStatement(parent) ? ";" : "", `(() => {
const __setup = `);
const result = (ts.isArrowFunction(comp) || ts.isFunctionExpression(comp)) && comp.typeParameters?.length ? ["\nreturn __setup"] : [
`
type __Setup = typeof __setup
type __Props = Parameters<__Setup>[0]
type __Slots = Parameters<__Setup>[1] extends { slots?: infer Slots } | undefined ? Slots : {}
type __Exposed = Parameters<__Setup>[1] extends { expose?: (exposed: infer Exposed) => any } | undefined ? Exposed : {}`,
"\n const __component = ",
isVapor ? `// @ts-ignore\n(defineVaporComponent,await import('vue-jsx-vapor')).` : "",
[node.expression.getText(ast), node.expression.getStart(ast)],
`({\n`,
`...{} as {
setup: (props: __Props) => __Exposed,
render: () => ReturnType<__Setup>
slots: ${isVapor ? "__Slots" : `import('vue').SlotsType<__Slots>`}
},`,
...compOptions ? ["...", [compOptions.getText(ast), compOptions.getStart(ast)]] : [],
`})
return {} as Omit<typeof __component, 'constructor'> & {
new (props?: __Props): InstanceType<typeof __component> & {${isVapor ? "\n/** @deprecated This is only a type when used in Vapor Instances. */\n$props: __Props" : ""}},
}`
];
codes.replaceRange(node.end, node.end, ...result, `\n})()`);
}
//#endregion
//#region src/volar/define-style.ts
function transformDefineStyle({ expression, isCssModules }, index, root, options) {
const { ts, codes, ast } = options;
if (isCssModules && expression?.arguments[0] && !expression.typeArguments && ts.isTemplateLiteral(expression.arguments[0])) codes.replaceRange(expression.arguments.pos - 1, expression.arguments.pos - 1, `<{`, ...parseCssClassNames(expression.arguments[0].getText(ast).slice(1, -1)).flatMap(({ text, offset }) => [
`\n`,
[
`'${text.slice(1)}'`,
`style_${index}`,
expression.arguments.pos + offset + 1,
{ navigation: true }
],
`: string`
]), "\n}>");
else if (root?.body) {
const returnNode = root.body.forEachChild((node) => ts.isReturnStatement(node) ? node : void 0);
if (returnNode) {
codes.replaceRange(expression.getStart(ast), expression.getEnd());
codes.replaceRange(returnNode.pos, returnNode.pos, ";", [expression.getText(ast), expression.getStart(ast)]);
}
}
addEmbeddedCode(expression, index, options);
}
const commentReg = /(?<=\/\*)[\s\S]*?(?=\*\/)|(?<=\/\/)[\s\S]*?(?=\n)/g;
const cssClassNameReg = /(?=(\.[a-z_][-\w]*)[\s.,+~>:#)[{])/gi;
const fragmentReg = /(?<=\{)[^{]*(?=(?<!\\);)/g;
function parseCssClassNames(css) {
for (const reg of [commentReg, fragmentReg]) css = css.replace(reg, (match) => " ".repeat(match.length));
const matches = css.matchAll(cssClassNameReg);
const result = [];
for (const match of matches) {
const matchText = match[1];
if (matchText) result.push({
offset: match.index,
text: matchText
});
}
return result;
}
function addEmbeddedCode(expression, index, options) {
const { ts, ast } = options;
const languageId = ts.isPropertyAccessExpression(expression.expression) && ts.isIdentifier(expression.expression.name) ? expression.expression.name.text : "css";
const style = expression.arguments[0];
const styleText = style.getText(ast).slice(1, -1).replaceAll(/\$\{.*\}/g, (str) => "_".repeat(str.length));
options.embeddedCodes.push({
id: `style_${index}`,
languageId,
snapshot: {
getText: (start, end) => styleText.slice(start, end),
getLength: () => styleText.length,
getChangeRange: () => void 0
},
mappings: [{
sourceOffsets: [style.getStart(ast) + 1],
generatedOffsets: [0],
lengths: [styleText.length],
data: allCodeFeatures
}],
embeddedCodes: []
});
}
//#endregion
//#region src/volar/transform.ts
function transformJsxMacros(rootMap, options) {
const { ts, codes, ast } = options;
let defineStyleIndex = 0;
for (const [root, macros] of rootMap) {
macros.defineStyle?.forEach((defaultStyle) => transformDefineStyle(defaultStyle, defineStyleIndex++, root, options));
if (!root?.body || Object.keys(macros).length === 1 && macros.defineStyle) continue;
const asyncModifier = root.modifiers?.find((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword);
if (asyncModifier && macros.defineComponent) codes.replaceRange(asyncModifier.pos, asyncModifier.end);
const result = "({}) as typeof __ctx.render & { __ctx?: { props: typeof __ctx.props } & typeof __ctx.context }";
const propsType = root.parameters[0]?.type ? root.parameters[0].type.getText(ast) : "{}";
codes.replaceRange(root.parameters.pos, root.parameters.pos, ts.isArrowFunction(root) && root.parameters.pos === root.pos ? "(" : "", `__props: typeof __ctx.props & ${propsType}, `, `__context?: typeof __ctx.context, `, `__ctx = {} as Awaited<ReturnType<typeof __fn>>, `, `__fn = (${asyncModifier ? "async" : ""}(`);
if (ts.isArrowFunction(root)) codes.replaceRange(root.end, root.end, `))${root.pos === root.parameters.pos ? ")" : ""} => `, result);
else {
codes.replaceRange(root.body.getStart(ast), root.body.getStart(ast), "=>");
codes.replaceRange(root.end, root.end, `)){ return `, result, "}");
}
root.body.forEachChild((node) => {
if (ts.isReturnStatement(node) && node.expression) {
const props = [...macros.defineModel ?? []];
const elements = root.parameters[0] && !root.parameters[0].type && ts.isObjectBindingPattern(root.parameters[0].name) ? root.parameters[0].name.elements : [];
for (const element of elements) if (ts.isIdentifier(element.name)) {
const isRequired = element.forEachChild(function isNonNullExpression(node$1) {
return ts.isNonNullExpression(node$1) || !!node$1.forEachChild(isNonNullExpression);
});
props.push(`${element.name.escapedText}${isRequired ? ":" : "?:"} typeof ${element.name.escapedText}`);
}
codes.replaceRange(node.getStart(ast), node.expression.getStart(ast), "const ", [
`__render`,
node.getStart(ast) - 1,
{ verification: true }
], macros.defineComponent ? macros.defineComponent?.expression.getText(ast) === "defineVaporComponent" ? "" : ": () => JSX.Element" : "", " = ");
codes.replaceRange(node.expression.end, node.expression.end, `
return {} as {
props: {${props.join(", ")}},
context: ${root.parameters[1]?.type ? `${root.parameters[1].type.getText(ast)} & ` : ""}{
slots: ${macros.defineSlots ?? "{}"},
expose: (exposed: ${macros.defineExpose ?? "Record<string, any>"}) => void,
attrs: Record<string, any>
},
render: typeof __render
}`);
}
});
}
}
//#endregion
//#region src/volar/global-types.ts
function getGlobalTypes(rootMap, options) {
let defineStyle = "";
if (options.defineSlots.alias) {
defineStyle = options.defineStyle.alias.map((alias) => `declare const ${alias}: { <T>(...args: __StyleArgs): T; scss: <T>(...args: __StyleArgs)=> T; sass: <T>(...args: __StyleArgs)=> T; stylus: <T>(...args: __StyleArgs)=> T; less: <T>(...args: __StyleArgs)=> T; postcss: <T>(...args: __StyleArgs)=> T };`).join("\n");
defineStyle += `\ntype __StyleArgs = [style: string, options?: { scoped?: boolean }];`;
}
if (!rootMap.size) return `\n${defineStyle}`;
const defineSlots = options.defineSlots.alias.flatMap((alias) => [`declare function ${alias}<T extends Record<string, any>>(): Partial<T>;`, `declare function ${alias}<T extends Record<string, any>>(slots: T): T;`]).join("\n");
const defineExpose = options.defineExpose.alias.map((alias) => `declare function ${alias}<Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed): Exposed;`).join("\n");
const defineModel = options.defineModel.alias.map((alias) => alias === "defineModel" ? "defineModel" : `defineModel: ${alias}`);
const defineComponent = options.defineComponent.alias.map((alias) => ["defineComponent", "defineVaporComponent"].includes(alias) ? "" : `defineComponent: ${alias}`);
const VueMacros = [...defineModel, ...defineComponent].filter(Boolean).join(",");
return `
${VueMacros ? `declare const { ${VueMacros} }: typeof import('vue');` : ""}
${defineSlots}
${defineExpose}
${defineStyle}
`;
}
//#endregion
//#region src/volar/index.ts
function getMacro(node, ts, options) {
if (!node) return;
if (ts.isVariableStatement(node)) return node.declarationList.forEachChild((decl) => getExpression(decl));
else return getExpression(node);
function getExpression(decl) {
if (ts.isVariableDeclaration(decl) && decl.initializer) {
const initializer = ts.isCallExpression(decl.initializer) && ts.isIdentifier(decl.initializer.expression) && decl.initializer.expression.escapedText === "$" && decl.initializer.arguments[0] ? decl.initializer.arguments[0] : decl.initializer;
const expression = getMacroExpression(initializer);
if (expression) return {
expression,
variableDeclaration: decl,
initializer: decl.initializer,
isRequired: ts.isNonNullExpression(initializer)
};
} else if (ts.isExpressionStatement(decl)) {
const expression = getMacroExpression(decl.expression);
if (expression) return {
expression,
initializer: decl.expression,
isRequired: ts.isNonNullExpression(decl.expression)
};
}
}
function getMacroExpression(node$1) {
if (ts.isNonNullExpression(node$1)) node$1 = node$1.expression;
if (!ts.isCallExpression(node$1)) return;
const expression = ts.isPropertyAccessExpression(node$1.expression) ? node$1.expression : node$1;
return ts.isIdentifier(expression.expression) && [
...options.defineModel.alias,
...options.defineSlots.alias,
...options.defineStyle.alias,
...options.defineExpose.alias,
...options.defineComponent.alias
].includes(expression.expression.escapedText) && node$1;
}
}
function getRootMap(options) {
const { ts, ast, codes } = options;
const rootMap = /* @__PURE__ */ new Map();
function walk(node, parents) {
const root = parents[1] && (ts.isArrowFunction(parents[1]) || ts.isFunctionExpression(parents[1]) || ts.isFunctionDeclaration(parents[1])) ? parents[1] : void 0;
if (root && parents[2] && ts.isCallExpression(parents[2]) && !parents[2].typeArguments && options.defineComponent.alias.includes(parents[2].expression.getText(ast))) {
if (!rootMap.has(root)) rootMap.set(root, {});
if (!rootMap.get(root).defineComponent) {
rootMap.get(root).defineComponent = parents[2];
transformDefineComponent(parents[2], parents[3], options);
}
}
const macro = getMacro(node, ts, options);
if (macro) {
const { expression, initializer, variableDeclaration } = macro;
let isRequired = macro.isRequired;
if (!rootMap.has(root)) rootMap.set(root, {});
const macroName = expression.expression.getText(ast);
if (macroName.startsWith("defineStyle")) {
(rootMap.get(root).defineStyle ??= []).push({
expression,
isCssModules: ts.isVariableStatement(node)
});
return;
}
if (root) {
if (options.defineModel.alias.includes(macroName)) {
const modelName = expression.arguments[0] && ts.isStringLiteralLike(expression.arguments[0]) ? expression.arguments[0].text : "modelValue";
const modelOptions = expression.arguments[0] && ts.isStringLiteralLike(expression.arguments[0]) ? expression.arguments[1] : expression.arguments[0];
if (modelOptions && ts.isObjectLiteralExpression(modelOptions)) {
let hasRequired = false;
for (const prop of modelOptions.properties) if (ts.isPropertyAssignment(prop) && prop.name.getText(ast) === "required") {
hasRequired = true;
isRequired = prop.initializer.kind === ts.SyntaxKind.TrueKeyword;
}
if (!hasRequired && isRequired) codes.replaceRange(modelOptions.end - 1, modelOptions.end - 1, `${!modelOptions.properties.hasTrailingComma && modelOptions.properties.length ? "," : ""} required: true`);
} else if (isRequired) codes.replaceRange(expression.arguments.end, expression.arguments.end, `${!expression.arguments.hasTrailingComma && expression.arguments.length ? "," : ""} { required: true }`);
const id = toValidAssetId(modelName, `__model`);
const typeString = `import('vue').UnwrapRef<typeof ${id}>`;
const defineModel = rootMap.get(root).defineModel ??= [];
defineModel.push(`${modelName.includes("-") ? `'${modelName}'` : modelName}${isRequired ? ":" : "?:"} ${typeString}`, `'onUpdate:${modelName}'?: ($event: ${typeString}) => any`);
if (expression.typeArguments?.[1]) defineModel.push(`${modelName}Modifiers?: Partial<Record<${getText(expression.typeArguments[1], ast, ts)}, boolean>>`);
codes.replaceRange(initializer.getStart(ast), initializer.getStart(ast), variableDeclaration ? `// @ts-ignore\n${id};\n` : "", `let ${id} = `);
} else if (options.defineSlots.alias.includes(macroName)) {
codes.replaceRange(expression.getStart(ast), expression.getStart(ast), variableDeclaration ? "// @ts-ignore\n__slots;\n" : "", `const __slots = `);
rootMap.get(root).defineSlots = `Partial<typeof __slots>`;
} else if (options.defineExpose.alias.includes(macroName)) {
codes.replaceRange(expression.getStart(ast), expression.getStart(ast), variableDeclaration ? "// @ts-ignore\n__exposed;\n" : "", `const __exposed = `);
rootMap.get(root).defineExpose = `typeof __exposed`;
}
}
}
node.forEachChild((child) => {
parents.unshift(node);
walk(child, parents);
parents.shift();
});
}
ast.forEachChild((node) => walk(node, []));
return rootMap;
}
function toValidAssetId(name, type) {
return `_${type}_${name.replaceAll(/\W/g, (searchValue, replaceValue) => {
return searchValue === "-" ? "_" : name.charCodeAt(replaceValue).toString();
})}`;
}
//#endregion
//#region src/volar.ts
const plugin = createPlugin(({ ts }, userOptions = {}) => {
const resolvedOptions = resolveOptions(userOptions);
resolvedOptions.include.push(REGEX_VUE_SFC);
const filter = createFilter(resolvedOptions);
return {
name: "@vue-jsx-vapor/macros",
resolveVirtualCode(virtualCode) {
const { filePath, codes } = virtualCode;
if (!filter(filePath)) return;
const options = {
ts,
...virtualCode,
...resolvedOptions
};
const rootMap = getRootMap(options);
if (rootMap.size) transformJsxMacros(rootMap, options);
codes.push(getGlobalTypes(rootMap, options));
}
};
});
var volar_default = plugin;
//#endregion
export { volar_default as default, plugin as "module.exports" };