UNPKG

@vue-jsx-vapor/macros

Version:
299 lines (292 loc) 14.8 kB
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" };