@wc-toolkit/type-parser
Version:
A set of tools for retrieving and transforming data from the Custom Elements Manifest
413 lines (409 loc) • 12.5 kB
JavaScript
// src/cem-plugin.ts
import path from "path";
import fs from "fs";
// node_modules/.pnpm/@wc-toolkit+cem-utilities@1.0.0/node_modules/@wc-toolkit/cem-utilities/dist/index.js
function deepMerge(target, source) {
if (typeof target !== "object" || target === null) {
return source;
}
if (typeof source !== "object" || source === null) {
return target;
}
const targetObj = target;
const sourceObj = source;
for (const key of Object.keys(source)) {
if (sourceObj[key] instanceof Array) {
if (!targetObj[key]) {
targetObj[key] = [];
}
targetObj[key] = targetObj[key].concat(sourceObj[key]);
} else if (sourceObj[key] instanceof Object) {
if (!targetObj[key]) {
targetObj[key] = {};
}
targetObj[key] = deepMerge(targetObj[key], sourceObj[key]);
} else {
targetObj[key] = sourceObj[key];
}
}
return targetObj;
}
// src/logger.ts
var Logger = class {
#debug;
constructor(debug = false) {
this.#debug = debug;
}
log(message, color = "\x1B[30m%s\x1B[0m") {
if (!this.#debug) {
return;
}
console.log(color, message);
}
red(message) {
this.log(message, "\x1B[31m%s\x1B[0m");
}
green(message) {
this.log(message, "\x1B[32m%s\x1B[0m");
}
yellow(message) {
this.log(message, "\x1B[33m%s\x1B[0m");
}
blue(message) {
this.log(message, "\x1B[34m%s\x1B[0m");
}
magenta(message) {
this.log(message, "\x1B[35m%s\x1B[0m");
}
cyan(message) {
this.log(message, "\x1B[36m%s\x1B[0m");
}
};
// src/cem-plugin.ts
var aliasTypes = {};
var groupedTypes = {};
var primitives = [
"string",
"number",
"boolean",
"any",
"null",
"undefined",
"unknown",
"never",
"void",
"object",
"symbol",
"bigint",
"true",
"false"
];
var currentFilename = "";
var typeChecker;
var options;
var typeScript;
var tsConfigFile;
var log;
var defaultOptions = {
parseObjectTypes: "none",
parseParameters: false,
propertyName: "parsedType"
};
function typeParserPlugin(op) {
options = deepMerge(defaultOptions, op);
log = new Logger(options.debug);
if (options.skip) {
log.yellow("[type-parser] - Skipped");
return;
}
log.log("[type-parser] - Updating Custom Elements Manifest...");
return {
name: "type-parser-plugin",
analyzePhase,
packageLinkPhase: () => {
log.green("[type-parser] - Custom Elements Manifest updated.");
}
};
}
function getTsProgram(ts, globs, configName = "tsconfig.json") {
tsConfigFile = ts.findConfigFile(
process.cwd(),
ts.sys.fileExists,
configName
);
const { config } = ts.readConfigFile(tsConfigFile, ts.sys.readFile);
const compilerOptions = ts.convertCompilerOptionsFromJson(
config.compilerOptions ?? {},
"."
);
const program = ts.createProgram(globs, compilerOptions.options);
const exclusions = config.exclude && config.exclude.length ? [...config.exclude, "node_modules"] : ["node_modules"];
typeScript = ts;
typeChecker = program.getTypeChecker();
for (const sourceFile of program.getSourceFiles()) {
currentFilename = path.resolve(sourceFile.fileName);
if (!exclusions.some((x) => currentFilename.includes(x))) {
aliasTypes[currentFilename] = {};
visitNode(sourceFile);
}
}
groupTypesByName();
return program;
}
function getParsedType(fileName, typeName) {
if (typeName?.includes("|")) {
return getUnionTypes(fileName, typeName);
}
if (typeName?.startsWith("{") && typeName?.endsWith("}")) {
return getObjectTypes(fileName, typeName);
}
if (primitives.includes(typeName) || typeof groupedTypes[typeName] === "undefined") {
return typeName;
}
if (typeof groupedTypes[typeName][fileName] !== "undefined") {
return groupedTypes[typeName][fileName];
}
if (Object.entries(groupedTypes[typeName]).length === 1) {
return Object.values(groupedTypes[typeName])[0];
}
if (typeChecker && typeScript) {
const sourceFile = typeChecker.getProgram().getSourceFile(fileName);
if (sourceFile) {
const symbols = typeChecker.getSymbolsInScope(
sourceFile,
typeScript.SymbolFlags.Type
);
const symbol = symbols.find((s) => s.name === typeName);
if (symbol) {
const type = typeChecker.getDeclaredTypeOfSymbol(symbol);
return getFinalType(type);
}
}
}
return typeName;
}
function getUnionTypes(fileName, typeName) {
return typeName?.split("|").map((part) => part.trim()).filter((part) => part.length > 0)?.map((part) => getParsedType(fileName, part)).join(" | ") || "";
}
function getObjectTypes(fileName, typeName) {
const parts = [
...new Set(
typeName?.split(/[:{}]/).map((part) => part.trim()).filter((part) => part.length > 0)
)
];
parts.forEach((part) => {
const cleanPart = part.replace(/\/\/.*|\/\*[\s\S]*?\*\//g, "");
typeName = typeName.replace(cleanPart, getParsedType(fileName, cleanPart));
});
return typeName;
}
function getFinalType(type) {
if (isNpmType(type)) {
return typeChecker.typeToString(type);
}
if (type.isUnion()) {
return type.types.map(getFinalType).join(" | ");
}
if (type.isIntersection()) {
return type.types.map(getFinalType).join(" & ");
}
if (type.flags & typeScript.TypeFlags.String) {
return "string";
}
if (type.flags & typeScript.TypeFlags.Number) {
return "number";
}
if (type.flags & typeScript.TypeFlags.Boolean) {
return "boolean";
}
if (type.flags & typeScript.TypeFlags.Unknown) {
return "unknown";
}
if (type.flags & typeScript.TypeFlags.Any) {
return "any";
}
if (type.flags & typeScript.TypeFlags.Void) {
return "void";
}
if (type.flags & typeScript.TypeFlags.Null) {
return "null";
}
if (type.flags & typeScript.TypeFlags.Undefined) {
return "undefined";
}
if (type.flags & typeScript.TypeFlags.Never) {
return "never";
}
if (type.flags & typeScript.TypeFlags.BigInt) {
return "bigint";
}
if (type.flags & typeScript.TypeFlags.ESSymbol) {
return "symbol";
}
if (type.flags & typeScript.TypeFlags.StringLiteral) {
const value = type.value;
return `"${value}"`;
}
if (type.flags & typeScript.TypeFlags.NumberLiteral || type.flags & typeScript.TypeFlags.BigIntLiteral) {
const value = type.value;
return `${value}`;
}
if (type.flags & typeScript.TypeFlags.Enum) {
const enumType = type;
const enumMembers = typeChecker.getPropertiesOfType(enumType);
const enumValues = enumMembers.map((member) => member.name);
return enumValues.join(" | ");
}
if (type.isClassOrInterface() || type.flags & typeScript.TypeFlags.Object) {
const properties = typeChecker.getPropertiesOfType(type);
const props = properties.map(
(prop) => {
const propType = typeChecker.getTypeOfSymbolAtLocation(
prop,
prop.valueDeclaration
);
let typeStr;
let isOptional = false;
if (propType.isUnion && propType.isUnion()) {
const types = propType.types;
const hasUndefined = types.some((t) => t.flags & typeScript.TypeFlags.Undefined);
const nonUndefinedTypes = types.filter((t) => !(t.flags & typeScript.TypeFlags.Undefined));
if (hasUndefined) {
isOptional = true;
}
if (options.parseObjectTypes === "partial") {
const typeNames = nonUndefinedTypes.map((t) => typeChecker.typeToString(t));
if (typeNames.every((tStr) => primitives.includes(tStr))) {
typeStr = typeNames.join(" | ");
} else {
typeStr = typeChecker.typeToString(propType);
}
} else {
typeStr = nonUndefinedTypes.map(getFinalType).join(" | ");
}
} else {
if (options.parseObjectTypes === "partial") {
const tStr = typeChecker.typeToString(propType);
if (primitives.includes(tStr)) {
typeStr = tStr;
} else {
if (propType.objectFlags && propType.objectFlags & typeScript.ObjectFlags.Anonymous) {
typeStr = getFinalType(propType);
} else {
typeStr = tStr;
}
}
} else {
typeStr = getFinalType(propType);
}
}
if (!isOptional && typeStr.endsWith(" (optional)")) {
isOptional = true;
typeStr = typeStr.replace(" (optional)", "");
}
return `${prop.name}${isOptional ? "?" : ""}: ${typeStr}`;
}
);
return `{ ${props.join(", ")} }`;
}
return typeChecker.typeToString(type);
}
function isNpmType(type) {
const symbol = type.getSymbol();
if (!symbol) return false;
const declarations = symbol.getDeclarations();
if (!declarations || declarations.length === 0) return false;
return declarations.some((decl) => {
const sourceFile = decl.getSourceFile();
return sourceFile.fileName.includes("node_modules");
});
}
function visitNode(node) {
if (typeScript.isTypeAliasDeclaration(node) || typeScript.isEnumDeclaration(node) || typeScript.isInterfaceDeclaration(node) && options.parseObjectTypes !== "none") {
const symbol = typeChecker.getSymbolAtLocation(node.name);
if (symbol) {
const type = typeChecker.getDeclaredTypeOfSymbol(symbol);
const finalType = getFinalType(type);
log.log(
`Type alias '${node.name.text}' has final computed type: ${finalType}`
);
aliasTypes[currentFilename][node.name.text] = finalType;
}
}
typeScript.forEachChild(node, visitNode);
}
function groupTypesByName() {
for (const alias in aliasTypes) {
for (const type in aliasTypes[alias]) {
if (!groupedTypes[type]) {
groupedTypes[type] = {};
}
groupedTypes[type][alias] = aliasTypes[alias][type];
}
}
}
function analyzePhase({ ts, node, moduleDoc, context }) {
moduleDoc.path = moduleDoc.path.replace(`${process.cwd()}/`, "");
if (node.kind === ts.SyntaxKind.SourceFile) {
currentFilename = path.resolve(node.fileName);
}
if (node.kind !== ts.SyntaxKind.ClassDeclaration) {
return;
}
const component = getComponent(node, moduleDoc);
if (!component) {
return;
}
updateParsedTypes(component, context);
}
function getComponent(node, moduleDoc) {
const className = node.name.getText();
return moduleDoc.declarations.find(
(dec) => dec.name === className
);
}
function getTypedMembers(component) {
return [
...component.attributes || [],
...component.members || [],
...component.events || []
].filter((item) => item?.type || options.parseParameters && item?.parameters?.length);
}
function getTypeValue(item, context) {
const importedType = context?.imports?.find(
(i) => i.name === item.type?.text
);
if (!importedType) {
return getParsedType(currentFilename, item.type.text);
}
const resolvedPath = getResolvedImportPath(currentFilename, importedType);
return getParsedType(resolvedPath, importedType.name);
}
function getResolvedImportPath(importPath, importedType) {
let resolvedPath = path.resolve(
path.dirname(currentFilename),
importedType.importPath
);
if (aliasTypes[resolvedPath]) {
return resolvedPath;
}
if (aliasTypes[resolvedPath + ".ts"]) {
resolvedPath += ".ts";
} else if (resolvedPath.endsWith(".js")) {
resolvedPath = `${resolvedPath}`.replace(".js", ".ts");
} else if (resolvedPath.endsWith(".d.ts")) {
resolvedPath = currentFilename;
} else if (fs.existsSync(resolvedPath + ".d.ts")) {
resolvedPath = currentFilename;
}
return resolvedPath;
}
function updateParsedTypes(component, context) {
const typedMembers = getTypedMembers(component);
const propName = options.propertyName || "parsedType";
typedMembers.forEach((member) => {
if (member.parameters?.length) {
member.parameters.forEach((param, i) => {
if (param.type?.text) {
const typeValue = getTypeValue(param, context);
if (typeValue !== param.type.text) {
member.parameters[i][propName] = {
text: typeValue.replace(/"/g, "'")
};
}
}
});
} else {
const typeValue = getTypeValue(member, context);
if (typeValue !== member.type.text) {
member[propName] = {
text: typeValue.replace(/"/g, "'")
};
}
}
});
}
export {
getTsProgram,
typeParserPlugin
};