@vue.ts/complex-types
Version:
Resolve complex types in Vue SFCs.
223 lines (215 loc) • 9.91 kB
JavaScript
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 };