UNPKG

@vue.ts/complex-types

Version:
223 lines (215 loc) 9.91 kB
import { ensureLanguage, getLanguage } from "@vue.ts/language"; import { createUnplugin } from "unplugin"; import { createFilter } from "unplugin-utils"; import MagicString from "magic-string"; import ts from "typescript"; import { createOptionsResolver, normalizePath } from "@vue.ts/shared"; import { isReservedProp } from "@vue/shared"; //#region src/core/utils.ts const defaultOptions = { root: process.cwd(), include: ["**/*.vue"], exclude: ["node_modules/**"], tsconfigPath: "tsconfig.json", defineEmits: true, defineProps: true }; const resolveOptions = createOptionsResolver(defaultOptions); const quotesReg = /"/g; const escapeQuotes = (s) => s.replace(quotesReg, "\\\""); //#endregion //#region src/core/printer.ts var Printer = class { isPropertyBlacklisted = () => false; constructor(checker) { this.checker = checker; } typeToString(type) { return this.checker.typeToString(type, void 0, ts.TypeFormatFlags.NoTruncation); } printUnionOrIntersection(type, separator, inner) { return [...new Set(type.types.map((t) => this.printType(t, inner)).filter(Boolean))].join(separator); } printConditionType(type, inner) { const { trueType, falseType } = type.root.node; const trueTypeNode = this.checker.getTypeAtLocation(trueType); const falseTypeNode = this.checker.getTypeAtLocation(falseType); return `${this.printType(trueTypeNode, inner)} | ${this.printType(falseTypeNode, inner)}`; } printPrimitiveType(type) { if (type.flags & ts.TypeFlags.BooleanLiteral) return "boolean"; else if (type.flags & ts.TypeFlags.String || type.isStringLiteral()) return "string"; else if (type.flags & ts.TypeFlags.Number || type.isNumberLiteral()) return "number"; else if (type.flags & ts.TypeFlags.BigInt) return "bigint"; else if (type.flags & ts.TypeFlags.Any) return "any"; else if (type.flags & ts.TypeFlags.Unknown) return "unknown"; else if (type.flags & ts.TypeFlags.Null) return "null"; return ""; } printType(type, inner = false) { if (type.isUnion()) return this.printUnionOrIntersection(type, " | ", inner); else if (type.isIntersection()) return this.printUnionOrIntersection(type, " & ", inner); if (this.checker.isArrayType(type)) return "Array"; else if (type.flags & ts.TypeFlags.Object) { const decl = type.getSymbol()?.declarations?.[0]; if (decl && ts.isFunctionTypeNode(decl)) return "Function"; if (inner) return "object"; const properties = type.getProperties(); const props = {}; for (const prop of properties) { const propType = this.checker.getTypeOfSymbol(prop); const name = prop.getName(); if (this.isPropertyBlacklisted(name)) continue; props[name] = { value: this.printType(propType, true), isOptional: !!(prop.flags & ts.SymbolFlags.Optional) }; } const parts = []; for (const [propName, { isOptional, value }] of Object.entries(props)) { const questionMark = isOptional ? "?" : ""; parts.push(`"${escapeQuotes(propName)}"${questionMark}: ${value},`); } return Object.keys(props).length > 0 ? `{\n${parts.join("\n")}\n}` : ""; } else if (type.isLiteral() || type.flags & ts.TypeFlags.BooleanLiteral || type.flags & ts.TypeFlags.String || type.flags & ts.TypeFlags.Number || type.flags & ts.TypeFlags.BigInt || type.flags & ts.TypeFlags.Any || type.flags & ts.TypeFlags.Unknown || type.flags & ts.TypeFlags.Null) return this.printPrimitiveType(type); else if (type.flags & ts.TypeFlags.Undefined) return ""; else if (type.flags & ts.TypeFlags.Conditional) return this.printConditionType(type, inner); else if (type.isTypeParameter()) { const decl = type.getSymbol()?.declarations?.[0]; if (!decl || !ts.isTypeParameterDeclaration(decl)) return ""; const ref = ts.getEffectiveConstraintOfTypeParameter(decl); if (!ref) return ""; const refType = this.checker.getTypeAtLocation(ref); return this.printType(refType, inner); } return this.typeToString(type); } printPropsTypeArg(node, isPropertyBlacklisted) { this.isPropertyBlacklisted = isPropertyBlacklisted; const type = this.checker.getTypeAtLocation(node); return this.printType(type); } printEventsByCallSignatures(callSignatures) { return callSignatures.map((c) => { const event = c.getParameters()[0]; return this.typeToString(this.checker.getTypeOfSymbol(event)); }); } printEventsByMembers(members) { return members.map((m) => `"${escapeQuotes(m.getName())}"`); } printEventsRuntimeArg(node) { const parts = []; const type = this.checker.getTypeAtLocation(node); const callSignatures = type.getCallSignatures(); const members = type.getProperties(); if (callSignatures.length > 0 && members.length > 0) throw new Error("[@vue.ts/complex-types] You may not use old style `defineEmits` and `defineEmits` shorthand together."); if (members.length > 0) parts.push(...this.printEventsByMembers(members)); else parts.push(...this.printEventsByCallSignatures(callSignatures)); return `[${parts.join(", ")}]`; } }; //#endregion //#region src/core/transformers/defineEmits.ts const transformDefineEmits = (printer, s, id) => { const normalizedFilepath = normalizePath(id); const language = getLanguage(); const scriptSetupBlock = language.getScriptSetupBlock(normalizedFilepath); const scriptSetupAst = language.getScriptSetupAst(normalizedFilepath); const virtualFileAst = language.getVirtualFileOrTsAst(normalizedFilepath); if (!scriptSetupBlock || !scriptSetupAst || !virtualFileAst) return; const scriptSetupDefineEmitsNode = language.findNodeByRange(scriptSetupAst, (scriptSetupRanges) => scriptSetupRanges.defineEmits?.callExp); const virtualFileDefineEmitsTypeNode = language.findNodeByName(virtualFileAst, "__VLS_Emit"); if (!scriptSetupDefineEmitsNode || !virtualFileDefineEmitsTypeNode) return; const offset = scriptSetupBlock.startTagEnd; if (ts.isCallExpression(scriptSetupDefineEmitsNode) && scriptSetupDefineEmitsNode.arguments[0]) throw new Error("[@vue.ts/complex-types] `defineEmits` cannot accept both runtime argument and type argument."); const tokens = scriptSetupDefineEmitsNode.getChildren(scriptSetupAst); const lessThanToken = tokens.find((t) => t.kind === ts.SyntaxKind.LessThanToken); const greaterThanToken = tokens.find((t) => t.kind === ts.SyntaxKind.GreaterThanToken); const openParenToken = tokens.find((t) => t.kind === ts.SyntaxKind.OpenParenToken); if (!lessThanToken || !greaterThanToken || !openParenToken) return; const defineEmitsTypeArgRange = [offset + lessThanToken.pos, offset + greaterThanToken.end]; const runtimeArgPos = offset + openParenToken.end; const printedRuntimeArg = printer.printEventsRuntimeArg(virtualFileDefineEmitsTypeNode); s.remove(...defineEmitsTypeArgRange); s.appendRight(runtimeArgPos, printedRuntimeArg); }; //#endregion //#region src/core/transformers/defineProps.ts const transformDefineProps = (printer, s, id) => { const normalizedFilepath = normalizePath(id); const language = getLanguage(); const scriptSetupBlock = language.getScriptSetupBlock(normalizedFilepath); const scriptSetupAst = language.getScriptSetupAst(normalizedFilepath); const virtualFileAst = language.getVirtualFileOrTsAst(normalizedFilepath); if (!scriptSetupBlock || !scriptSetupAst || !virtualFileAst) return; const scriptSetupDefinePropsTypeRange = language.parseScriptSetupRanges(scriptSetupAst).defineProps?.typeArg; const virtualFileDefinePropsTypeNode = language.findNodeByName(virtualFileAst, "__VLS_Props"); if (!scriptSetupDefinePropsTypeRange || !virtualFileDefinePropsTypeNode) return; const printedType = printer.printPropsTypeArg(virtualFileDefinePropsTypeNode, isReservedProp); const offset = scriptSetupBlock.startTagEnd; s.overwrite(offset + scriptSetupDefinePropsTypeRange.start, offset + scriptSetupDefinePropsTypeRange.end, printedType); }; //#endregion //#region src/core/transformers/index.ts const transformers = [["defineEmits", transformDefineEmits], ["defineProps", transformDefineProps]]; const getTransformers = (options) => transformers.filter(([key]) => !!options[key]); //#endregion //#region src/core/transform.ts function transform(code, id, options) { const s = new MagicString(code); const printer = new Printer(getLanguage().tsLs.getProgram().getTypeChecker()); const transformers$1 = getTransformers(options); for (const [, transform$1] of transformers$1) transform$1(printer, s, id); return { code: s.toString(), map: s.generateMap({ hires: true }) }; } //#endregion //#region src/index.ts const languageExtRegexp = /\.((?:c|m)?(?:j|t)sx?|d\.ts|vue)$/i; const unpluginFactory = (options = {}) => { const resolvedOptions = resolveOptions(options); const filter = createFilter(resolvedOptions.include, resolvedOptions.exclude); return { name: "@vue.ts/complex-types", enforce: "pre", vite: { async handleHotUpdate(ctx) { const { file } = ctx; if (!languageExtRegexp.test(file)) return; const language = getLanguage(); const { read } = ctx; async function readAndUpdateLanguage() { const content = await read(); language.updateFile(file, content); return content; } if (!filter(file)) { await readAndUpdateLanguage(); return; } ctx.read = async () => { const code = await readAndUpdateLanguage(); return transform(code, file, resolvedOptions)?.code ?? code; }; const sfcModule = ctx.modules.find((mod) => mod.file === file); if (sfcModule) return [sfcModule]; return ctx.modules; } }, buildStart() { ensureLanguage(resolveOptions(options).tsconfigPath); }, transform: { filter: { id: { include: resolvedOptions.include, exclude: resolvedOptions.exclude } }, handler: (code, id) => transform(code, id, resolvedOptions) } }; }; const unplugin = /* @__PURE__ */ createUnplugin(unpluginFactory); var src_default = unplugin; //#endregion export { unplugin as n, unpluginFactory as r, src_default as t };