web-component-analyzer
Version:
CLI that analyzes web components
5,229 lines • 200 kB
JavaScript
'use strict';
var tsModule = require('typescript');
var tsSimpleType = require('ts-simple-type');
var fs = require('fs');
var path = require('path');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var tsModule__namespace = /*#__PURE__*/_interopNamespaceDefault(tsModule);
/**
* Takes a node and tries to resolve a constant value from it.
* Returns undefined if no constant value can be resolved.
* @param node
* @param context
*/
function resolveNodeValue(node, context) {
var _a, _b;
if (node == null)
return undefined;
const { ts, checker } = context;
const depth = (context.depth || 0) + 1;
// Always break when depth is larger than 10.
// This ensures we cannot run into infinite recursion.
if (depth > 10)
return undefined;
if (ts.isStringLiteralLike(node)) {
return { value: node.text, node };
}
else if (ts.isNumericLiteral(node)) {
return { value: Number(node.text), node };
}
else if (ts.isPrefixUnaryExpression(node)) {
const value = (_a = resolveNodeValue(node.operand, { ...context, depth })) === null || _a === void 0 ? void 0 : _a.value;
return { value: applyPrefixUnaryOperatorToValue(value, node.operator, ts), node };
}
else if (ts.isObjectLiteralExpression(node)) {
const object = {};
for (const prop of node.properties) {
if (ts.isPropertyAssignment(prop)) {
// Resolve the "key"
const name = ((_b = resolveNodeValue(prop.name, { ...context, depth })) === null || _b === void 0 ? void 0 : _b.value) || prop.name.getText();
// Resolve the "value
const resolvedValue = resolveNodeValue(prop.initializer, { ...context, depth });
if (resolvedValue != null && typeof name === "string") {
object[name] = resolvedValue.value;
}
}
}
return {
value: object,
node
};
}
else if (node.kind === ts.SyntaxKind.TrueKeyword) {
return { value: true, node };
}
else if (node.kind === ts.SyntaxKind.FalseKeyword) {
return { value: false, node };
}
else if (node.kind === ts.SyntaxKind.NullKeyword) {
return { value: null, node };
}
else if (node.kind === ts.SyntaxKind.UndefinedKeyword) {
return { value: undefined, node };
}
// Resolve initializers for variable declarations
if (ts.isVariableDeclaration(node)) {
return resolveNodeValue(node.initializer, { ...context, depth });
}
// Resolve value of a property access expression. For example: MyEnum.RED
else if (ts.isPropertyAccessExpression(node)) {
return resolveNodeValue(node.name, { ...context, depth });
}
// Resolve [expression] parts of {[expression]: "value"}
else if (ts.isComputedPropertyName(node)) {
return resolveNodeValue(node.expression, { ...context, depth });
}
// Resolve initializer value of enum members.
else if (ts.isEnumMember(node)) {
if (node.initializer != null) {
return resolveNodeValue(node.initializer, { ...context, depth });
}
else {
return { value: `${node.parent.name.text}.${node.name.getText()}`, node };
}
}
// Resolve values of variables.
else if (ts.isIdentifier(node) && checker != null) {
const declarations = resolveDeclarations(node, { checker, ts });
if (declarations.length > 0) {
const resolved = resolveNodeValue(declarations[0], { ...context, depth });
if (context.strict || resolved != null) {
return resolved;
}
}
if (context.strict) {
return undefined;
}
return { value: node.getText(), node };
}
// Fallthrough
// - "my-value" as string
// - <any>"my-value"
// - ("my-value")
else if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isParenthesizedExpression(node)) {
return resolveNodeValue(node.expression, { ...context, depth });
}
// static get is() {
// return "my-element";
// }
else if ((ts.isGetAccessor(node) || ts.isMethodDeclaration(node) || ts.isFunctionDeclaration(node)) && node.body != null) {
for (const stm of node.body.statements) {
if (ts.isReturnStatement(stm)) {
return resolveNodeValue(stm.expression, { ...context, depth });
}
}
}
// [1, 2]
else if (ts.isArrayLiteralExpression(node)) {
return {
node,
value: node.elements.map(el => { var _a; return (_a = resolveNodeValue(el, { ...context, depth })) === null || _a === void 0 ? void 0 : _a.value; })
};
}
if (ts.isTypeAliasDeclaration(node)) {
return resolveNodeValue(node.type, { ...context, depth });
}
if (ts.isLiteralTypeNode(node)) {
return resolveNodeValue(node.literal, { ...context, depth });
}
if (ts.isTypeReferenceNode(node)) {
return resolveNodeValue(node.typeName, { ...context, depth });
}
return undefined;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function applyPrefixUnaryOperatorToValue(value, operator, ts) {
if (typeof value === "object" && value != null) {
return value;
}
switch (operator) {
case ts.SyntaxKind.MinusToken:
return -value;
case ts.SyntaxKind.ExclamationToken:
return !value;
case ts.SyntaxKind.PlusToken:
return +value;
}
return value;
}
/**
* Converts from snake case to camel case
* @param str
*/
/**
* Converts from camel case to snake case
* @param str
*/
function camelToDashCase(str) {
return str.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);
}
/**
* Returns if a name is private (starts with "_" or "#");
* @param name * @param name
*/
function isNamePrivate(name) {
return name.startsWith("_") || name.startsWith("#");
}
/**
* Resolves all relevant declarations of a specific node.
* @param node
* @param context
*/
function resolveDeclarations(node, context) {
if (node == null)
return [];
const symbol = getSymbol(node, context);
if (symbol == null)
return [];
return resolveSymbolDeclarations(symbol);
}
/**
* Returns the symbol of a node.
* This function follows aliased symbols.
* @param node
* @param context
*/
function getSymbol(node, context) {
if (node == null)
return undefined;
const { checker, ts } = context;
// Get the symbol
let symbol = checker.getSymbolAtLocation(node);
if (symbol == null) {
const identifier = getNodeIdentifier(node, context);
symbol = identifier != null ? checker.getSymbolAtLocation(identifier) : undefined;
}
// Resolve aliased symbols
if (symbol != null && isAliasSymbol(symbol, ts)) {
symbol = checker.getAliasedSymbol(symbol);
if (symbol == null)
return undefined;
}
return symbol;
}
/**
* Resolves the declarations of a symbol. A valueDeclaration is always the first entry in the array
* @param symbol
*/
function resolveSymbolDeclarations(symbol) {
// Filters all declarations
const valueDeclaration = symbol.valueDeclaration;
const declarations = symbol.getDeclarations() || [];
if (valueDeclaration == null) {
return declarations;
}
else {
// Make sure that "valueDeclaration" is always the first entry
return [valueDeclaration, ...declarations.filter(decl => decl !== valueDeclaration)];
}
}
/**
* Resolve a declaration by trying to find the real value by following assignments.
* @param node
* @param context
*/
function resolveDeclarationsDeep(node, context) {
const declarations = [];
const allDeclarations = resolveDeclarations(node, context);
for (const declaration of allDeclarations) {
if (context.ts.isVariableDeclaration(declaration) && declaration.initializer != null && context.ts.isIdentifier(declaration.initializer)) {
declarations.push(...resolveDeclarationsDeep(declaration.initializer, context));
}
else if (context.ts.isTypeAliasDeclaration(declaration) && declaration.type != null && context.ts.isIdentifier(declaration.type)) {
declarations.push(...resolveDeclarationsDeep(declaration.type, context));
}
else {
declarations.push(declaration);
}
}
return declarations;
}
/**
* Returns if the symbol has "alias" flag
* @param symbol
* @param ts
*/
function isAliasSymbol(symbol, ts) {
return hasFlag(symbol.flags, ts.SymbolFlags.Alias);
}
/**
* Returns a set of modifiers on a node
* @param node
* @param ts
*/
function getModifiersFromNode(node, ts) {
const modifiers = new Set();
if (hasModifier(node, ts.SyntaxKind.ReadonlyKeyword, ts)) {
modifiers.add("readonly");
}
if (hasModifier(node, ts.SyntaxKind.StaticKeyword, ts)) {
modifiers.add("static");
}
if (ts.isGetAccessor(node)) {
modifiers.add("readonly");
}
return modifiers.size > 0 ? modifiers : undefined;
}
/**
* Returns if a number has a flag
* @param num
* @param flag
*/
function hasFlag(num, flag) {
return (num & flag) !== 0;
}
/**
* Returns if a node has a specific modifier.
* @param node
* @param modifierKind
*/
function hasModifier(node, modifierKind, ts) {
if (!ts.canHaveModifiers(node)) {
return false;
}
const modifiers = ts.getModifiers(node);
if (modifiers == null)
return false;
return (node.modifiers || []).find(modifier => modifier.kind === modifierKind) != null;
}
/**
* Returns the visibility of a node
*/
function getMemberVisibilityFromNode(node, ts) {
if (hasModifier(node, ts.SyntaxKind.PrivateKeyword, ts) || ("name" in node && ts.isIdentifier(node.name) && isNamePrivate(node.name.text))) {
return "private";
}
else if (hasModifier(node, ts.SyntaxKind.ProtectedKeyword, ts)) {
return "protected";
}
else if (getNodeSourceFileLang(node) === "ts") {
// Only return "public" in typescript land
return "public";
}
return undefined;
}
/**
* Returns all keys and corresponding interface/class declarations for keys in an interface.
* @param interfaceDeclaration
* @param context
*/
function getInterfaceKeys(interfaceDeclaration, context) {
const extensions = [];
const { ts } = context;
for (const member of interfaceDeclaration.members) {
// { "my-button": MyButton; }
if (ts.isPropertySignature(member) && member.type != null) {
const resolvedKey = resolveNodeValue(member.name, context);
if (resolvedKey == null) {
continue;
}
let identifier;
let declaration;
if (ts.isTypeReferenceNode(member.type)) {
// { ____: MyButton; } or { ____: namespace.MyButton; }
identifier = member.type.typeName;
}
else if (ts.isTypeLiteralNode(member.type)) {
identifier = undefined;
declaration = member.type;
}
else {
continue;
}
if (declaration != null || identifier != null) {
extensions.push({ key: String(resolvedKey.value), keyNode: resolvedKey.node, declaration, identifier });
}
}
}
return extensions;
}
/**
* Find a node recursively walking up the tree using parent nodes.
* @param node
* @param test
*/
function findParent(node, test) {
if (node == null)
return;
return test(node) ? node : findParent(node.parent, test);
}
/**
* Find a node recursively walking down the children of the tree. Depth first search.
* @param node
* @param test
*/
function findChild(node, test) {
if (!node)
return;
if (test(node))
return node;
return node.forEachChild(child => findChild(child, test));
}
/**
* Find multiple children by walking down the children of the tree. Depth first search.
* @param node
* @param test
* @param emit
*/
function findChildren(node, test, emit) {
if (!node)
return;
if (test(node)) {
emit(node);
}
node.forEachChild(child => findChildren(child, test, emit));
}
/**
* Returns the language of the node's source file
* @param node
*/
function getNodeSourceFileLang(node) {
return node.getSourceFile().fileName.endsWith("ts") ? "ts" : "js";
}
/**
* Returns the leading comment for a given node
* @param node
* @param ts
*/
function getLeadingCommentForNode(node, ts) {
const sourceFileText = node.getSourceFile().text;
const leadingComments = ts.getLeadingCommentRanges(sourceFileText, node.pos);
if (leadingComments != null && leadingComments.length > 0) {
return sourceFileText.substring(leadingComments[0].pos, leadingComments[0].end);
}
return undefined;
}
/**
* Returns the declaration name of a given node if possible.
* @param node
* @param context
*/
function getNodeName(node, context) {
var _a;
return (_a = getNodeIdentifier(node, context)) === null || _a === void 0 ? void 0 : _a.getText();
}
/**
* Returns the declaration name of a given node if possible.
* @param node
* @param context
*/
function getNodeIdentifier(node, context) {
if (context.ts.isIdentifier(node)) {
return node;
}
else if ((context.ts.isClassLike(node) ||
context.ts.isInterfaceDeclaration(node) ||
context.ts.isVariableDeclaration(node) ||
context.ts.isMethodDeclaration(node) ||
context.ts.isPropertyDeclaration(node) ||
context.ts.isFunctionDeclaration(node)) &&
node.name != null &&
context.ts.isIdentifier(node.name)) {
return node.name;
}
return undefined;
}
/**
* Returns all decorators in either the node's `decorators` or `modifiers`.
* @param node
* @param context
*/
function getDecorators(node, context) {
var _a;
const { ts } = context;
return ts.canHaveDecorators(node) ? (_a = ts.getDecorators(node)) !== null && _a !== void 0 ? _a : [] : [];
}
/**
* Visits custom element definitions.
* @param node
* @param ts
* @param checker
*/
function discoverDefinitions$5(node, { ts, checker }) {
// customElements.define("my-element", MyElement)
if (ts.isCallExpression(node)) {
if (ts.isPropertyAccessExpression(node.expression) && node.expression.name.escapedText === "define") {
let leftExpression = node.expression.expression;
// Take "window.customElements" into account and return the "customElements" part
if (ts.isPropertyAccessExpression(leftExpression) &&
ts.isIdentifier(leftExpression.expression) &&
leftExpression.expression.escapedText === "window") {
leftExpression = leftExpression.name;
}
// Check if the "left expression" is called "customElements"
if (ts.isIdentifier(leftExpression) &&
leftExpression.escapedText === "customElements" &&
node.expression.name != null &&
ts.isIdentifier(node.expression.name)) {
// Find the arguments of: define("my-element", MyElement)
const [unresolvedTagNameNode, identifierNode] = node.arguments;
// Resolve the tag name node
// ("my-element", MyElement)
const resolvedTagNameNode = resolveNodeValue(unresolvedTagNameNode, { ts, checker, strict: true });
if (resolvedTagNameNode != null && identifierNode != null && typeof resolvedTagNameNode.value === "string") {
const tagName = resolvedTagNameNode.value;
const tagNameNode = resolvedTagNameNode.node;
// (___, MyElement)
if (ts.isIdentifier(identifierNode)) {
return [
{
tagName,
identifierNode,
tagNameNode
}
];
}
// (___, class { ... })
else if (ts.isClassLike(identifierNode) || ts.isInterfaceDeclaration(identifierNode)) {
return [
{
tagName,
tagNameNode,
declarationNode: identifierNode
}
];
}
}
}
}
return undefined;
}
// interface HTMLElementTagNameMap { "my-button": MyButton; }
if (ts.isInterfaceDeclaration(node) && ["HTMLElementTagNameMap", "ElementTagNameMap"].includes(node.name.text)) {
const extensions = getInterfaceKeys(node, { ts, checker });
return extensions.map(({ key, keyNode, identifier, declaration }) => ({
tagName: key,
tagNameNode: keyNode,
identifierNode: identifier,
declarationNode: declaration
}));
}
return undefined;
}
/**
* Flattens an array.
* Use this function to keep support for node 10
* @param items
*/
function arrayFlat(items) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ("flat" in items) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return items.flat();
}
const flattenArray = [];
for (const item of items) {
if (Array.isArray(item)) {
flattenArray.push(...item);
}
else {
flattenArray.push(item);
}
}
return flattenArray;
}
/**
* Filters an array returning only defined items
* @param array
*/
function arrayDefined(array) {
return array.filter((item) => item != null);
}
/**
* Filters an array returning only unique itesm
* @param array
*/
function arrayDedupe(array) {
const uniqueItems = [];
for (const item of array) {
if (uniqueItems.indexOf(item) === -1) {
uniqueItems.push(item);
}
}
return uniqueItems;
}
const NOTHING = Symbol();
/**
* This function wraps a callback returning a value and cahced the value.
* @param callback
*/
function lazy(callback) {
let value = NOTHING;
return () => {
if (value === NOTHING) {
value = callback();
}
return value;
};
}
/**
* Relax the type so that for example "string literal" become "string" and "function" become "any"
* This is used for javascript files to provide type checking with Typescript type inferring
* @param type
*/
function relaxType(type) {
switch (type.kind) {
case "INTERSECTION":
case "UNION":
return {
...type,
types: type.types.map(t => relaxType(t))
};
case "ENUM":
return {
...type,
types: type.types.map(t => relaxType(t))
};
case "ARRAY":
return {
...type,
type: relaxType(type.type)
};
case "PROMISE":
return {
...type,
type: relaxType(type.type)
};
case "OBJECT":
return {
name: type.name,
kind: "OBJECT"
};
case "INTERFACE":
case "FUNCTION":
case "CLASS":
return {
name: type.name,
kind: "ANY"
};
case "NUMBER_LITERAL":
return { kind: "NUMBER" };
case "STRING_LITERAL":
return { kind: "STRING" };
case "BOOLEAN_LITERAL":
return { kind: "BOOLEAN" };
case "BIG_INT_LITERAL":
return { kind: "BIG_INT" };
case "ENUM_MEMBER":
return {
...type,
type: relaxType(type.type)
};
case "ALIAS":
return {
...type,
target: relaxType(type.target)
};
case "NULL":
case "UNDEFINED":
return { kind: "ANY" };
default:
return type;
}
}
// Only search in "lib.dom.d.ts" performance reasons for now
const LIB_FILE_NAMES = ["lib.dom.d.ts"];
// Map "tsModule => name => SimpleType"
const LIB_TYPE_CACHE = new Map();
/**
* Return a Typescript library type with a specific name
* @param name
* @param ts
* @param program
*/
function getLibTypeWithName(name, { ts, program }) {
var _a;
const nameTypeCache = LIB_TYPE_CACHE.get(ts) || new Map();
if (nameTypeCache.has(name)) {
return nameTypeCache.get(name);
}
else {
LIB_TYPE_CACHE.set(ts, nameTypeCache);
}
let node;
for (const libFileName of LIB_FILE_NAMES) {
const sourceFile = program.getSourceFile(libFileName) || program.getSourceFiles().find(f => f.fileName.endsWith(libFileName));
if (sourceFile == null) {
continue;
}
for (const statement of sourceFile.statements) {
if (ts.isInterfaceDeclaration(statement) && ((_a = statement.name) === null || _a === void 0 ? void 0 : _a.text) === name) {
node = statement;
break;
}
}
if (node != null) {
break;
}
}
const checker = program.getTypeChecker();
let type = node == null ? undefined : tsSimpleType.toSimpleType(node, checker);
if (type != null) {
// Apparently Typescript wraps the type in "generic arguments" when take the type from the interface declaration
// Remove "generic arguments" here
if (type.kind === "GENERIC_ARGUMENTS") {
type = type.target;
}
}
nameTypeCache.set(name, type);
return type;
}
/**
* Returns typescript jsdoc node for a given node
* @param node
* @param ts
*/
function getJSDocNode(node, ts) {
var _a, _b, _c;
const parent = (_b = (_a = ts.getJSDocTags(node)) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.parent;
if (parent != null && ts.isJSDoc(parent)) {
return parent;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (_c = node.jsDoc) === null || _c === void 0 ? void 0 : _c.find((n) => ts.isJSDoc(n));
}
/**
* Returns jsdoc for a given node.
* @param node
* @param ts
* @param tagNames
*/
function getJsDoc(node, ts, tagNames) {
var _a;
const jsDocNode = getJSDocNode(node, ts);
// If we couldn't find jsdoc, find and parse the jsdoc string ourselves
if (jsDocNode == null) {
const leadingComment = getLeadingCommentForNode(node, ts);
if (leadingComment != null) {
const jsDoc = parseJsDocString(leadingComment);
// Return this jsdoc if we don't have to filter by tag name
if (jsDoc == null || tagNames == null || tagNames.length === 0) {
return jsDoc;
}
return {
...jsDoc,
tags: (_a = jsDoc.tags) === null || _a === void 0 ? void 0 : _a.filter(t => tagNames.includes(t.tag))
};
}
return undefined;
}
// Parse all jsdoc tags
// Typescript removes some information after parsing jsdoc tags, so unfortunately we will have to parse.
return {
description: jsDocNode.comment == null ? undefined : unescapeJSDoc(String(jsDocNode.comment)),
node: jsDocNode,
tags: jsDocNode.tags == null
? []
: arrayDefined(jsDocNode.tags.map(node => {
var _a, _b;
const tag = String(node.tagName.escapedText);
// Filter by tag name
if (tagNames != null && tagNames.length > 0 && !tagNames.includes(tag.toLowerCase())) {
return undefined;
}
// If Typescript generated a "type expression" or "name", comment will not include those.
// We can't just use what typescript parsed because it doesn't include things like optional jsdoc: name notation [...]
// Therefore we need to manually get the text and remove newlines/*
const typeExpressionPart = "typeExpression" in node ? (_a = node.typeExpression) === null || _a === void 0 ? void 0 : _a.getText() : undefined;
const namePart = "name" in node ? (_b = node.name) === null || _b === void 0 ? void 0 : _b.getText() : undefined;
const fullComment = (typeExpressionPart === null || typeExpressionPart === void 0 ? void 0 : typeExpressionPart.startsWith("@"))
? // To make matters worse, if Typescript can't parse a certain jsdoc, it will include the rest of the jsdocs tag from there in "typeExpressionPart"
// Therefore we check if there are multiple jsdoc tags in the string to only take the first one
// This will discard the following jsdocs, but at least we don't crash :-)
typeExpressionPart.split(/\n\s*\*\s?@/)[0] || ""
: `@${tag}${typeExpressionPart != null ? ` ${typeExpressionPart} ` : ""}${namePart != null ? ` ${namePart} ` : ""} ${node.comment || ""}`;
const comment = typeof node.comment === "string" ? node.comment.replace(/^\s*-\s*/, "").trim() : "";
return {
node,
tag,
comment,
parsed: lazy(() => parseJsDocTagString(fullComment))
};
}))
};
}
/**
* Converts a given string to a SimpleType
* Defaults to ANY
* See http://usejsdoc.org/tags-type.html
* @param str
* @param context
*/
function parseSimpleJsDocTypeExpression(str, context) {
// Fail safe if "str" is somehow undefined
if (str == null) {
return { kind: "ANY" };
}
// Parse normal types
switch (str.toLowerCase()) {
case "undefined":
return { kind: "UNDEFINED" };
case "null":
return { kind: "NULL" };
case "string":
return { kind: "STRING" };
case "number":
return { kind: "NUMBER" };
case "boolean":
return { kind: "BOOLEAN" };
case "array":
return { kind: "ARRAY", type: { kind: "ANY" } };
case "object":
return { kind: "OBJECT", members: [] };
case "any":
case "*":
return { kind: "ANY" };
}
// Match
// { string }
if (str.startsWith(" ") || str.endsWith(" ")) {
return parseSimpleJsDocTypeExpression(str.trim(), context);
}
// Match:
// {string|number}
if (str.includes("|")) {
return {
kind: "UNION",
types: str.split("|").map(str => {
const childType = parseSimpleJsDocTypeExpression(str, context);
// Convert ANY types to string literals so that {on|off} is "on"|"off" and not ANY|ANY
if (childType.kind === "ANY") {
return {
kind: "STRING_LITERAL",
value: str
};
}
return childType;
})
};
}
// Match:
// {?number} (nullable)
// {!number} (not nullable)
// {...number} (array of)
const prefixMatch = str.match(/^(\?|!|(\.\.\.))(.+)$/);
if (prefixMatch != null) {
const modifier = prefixMatch[1];
const type = parseSimpleJsDocTypeExpression(prefixMatch[3], context);
switch (modifier) {
case "?":
return {
kind: "UNION",
types: [
{
kind: "NULL"
},
type
]
};
case "!":
return type;
case "...":
return {
kind: "ARRAY",
type
};
}
}
// Match:
// {(......)}
const parenMatch = str.match(/^\((.+)\)$/);
if (parenMatch != null) {
return parseSimpleJsDocTypeExpression(parenMatch[1], context);
}
// Match
// {"red"}
const stringLiteralMatch = str.match(/^["'](.+)["']$/);
if (stringLiteralMatch != null) {
return {
kind: "STRING_LITERAL",
value: stringLiteralMatch[1]
};
}
// Match
// {[number]}
const arrayMatch = str.match(/^\[(.+)]$/);
if (arrayMatch != null) {
return {
kind: "ARRAY",
type: parseSimpleJsDocTypeExpression(arrayMatch[1], context)
};
}
// Match
// CustomEvent<string>
// MyInterface<string, number>
// MyInterface<{foo: string, bar: string}, number>
const genericArgsMatch = str.match(/^(.*)<(.*)>$/);
if (genericArgsMatch != null) {
// Here we split generic arguments by "," and
// afterwards remerge parts that were incorrectly split
// For example: "{foo: string, bar: string}, number" would result in
// ["{foo: string", "bar: string}", "number"]
// The correct way to improve "parseSimpleJsDocTypeExpression" is to build a custom lexer/parser.
const typeArgStrings = [];
for (const part of genericArgsMatch[2].split(/\s*,\s*/)) {
if (part.match(/[}:]/) != null && typeArgStrings.length > 0) {
typeArgStrings[typeArgStrings.length - 1] += `, ${part}`;
}
else {
typeArgStrings.push(part);
}
}
return {
kind: "GENERIC_ARGUMENTS",
target: parseSimpleJsDocTypeExpression(genericArgsMatch[1], context),
typeArguments: typeArgStrings.map(typeArg => parseSimpleJsDocTypeExpression(typeArg, context))
};
}
// If nothing else, try to find the type in Typescript global lib or else return "any"
return getLibTypeWithName(str, context) || { kind: "ANY" };
}
/**
* Finds a @type jsdoc tag in the jsdoc and returns the corresponding simple type
* @param jsDoc
* @param context
*/
function getJsDocType(jsDoc, context) {
var _a;
if (jsDoc.tags != null) {
const typeJsDocTag = jsDoc.tags.find(t => t.tag === "type");
if (typeJsDocTag != null) {
// We get the text of the node because typescript strips the type jsdoc tag under certain circumstances
const parsedJsDoc = parseJsDocTagString(((_a = typeJsDocTag.node) === null || _a === void 0 ? void 0 : _a.getText()) || "");
if (parsedJsDoc.type != null) {
return parseSimpleJsDocTypeExpression(parsedJsDoc.type, context);
}
}
}
}
const JSDOC_TAGS_WITH_REQUIRED_NAME = ["param", "fires", "@element", "@customElement"];
/**
* Takes a string that represents a value in jsdoc and transforms it to a javascript value
* @param value
*/
function parseJsDocValue(value) {
if (value == null) {
return value;
}
// Parse quoted strings
const quotedMatch = value.match(/^["'`](.*)["'`]$/);
if (quotedMatch != null) {
return quotedMatch[1];
}
// Parse keywords
switch (value) {
case "false":
return false;
case "true":
return true;
case "undefined":
return undefined;
case "null":
return null;
}
// Parse number
if (!isNaN(Number(value))) {
return Number(value);
}
return value;
}
/**
* Parses "@tag {type} name description" or "@tag name {type} description"
* @param str
*/
function parseJsDocTagString(str) {
const jsDocTag = {
tag: ""
};
if (str[0] !== "@") {
return jsDocTag;
}
const moveStr = (byLength) => {
str = str.substring(typeof byLength === "number" ? byLength : byLength.length);
};
const unqouteStr = (quotedStr) => {
return quotedStr.replace(/^['"](.+)["']$/, (_, match) => match);
};
const matchTag = () => {
// Match tag
// Example: " @mytag"
const tagResult = str.match(/^(\s*@(\S+))/);
if (tagResult == null) {
return jsDocTag;
}
else {
// Move string to the end of the match
// Example: " @mytag|"
moveStr(tagResult[1]);
jsDocTag.tag = tagResult[2];
}
};
const matchType = () => {
// Match type
// Example: " {MyType}"
const typeResult = str.match(/^(\s*{([\s\S]*)})/);
if (typeResult != null) {
// Move string to the end of the match
// Example: " {MyType}|"
moveStr(typeResult[1]);
jsDocTag.type = typeResult[2];
}
};
const matchName = () => {
// Match optional name
// Example: " [myname=mydefault]"
const defaultNameResult = str.match(/^(\s*\[([\s\S]+)\])/);
if (defaultNameResult != null) {
// Move string to the end of the match
// Example: " [myname=mydefault]|"
moveStr(defaultNameResult[1]);
// Using [...] means that this doc is optional
jsDocTag.optional = true;
// Split the inner content between [...] into parts
// Example: "myname=mydefault" => "myname", "mydefault"
const parts = defaultNameResult[2].split("=");
if (parts.length === 2) {
// Both name and default were given
jsDocTag.name = unqouteStr(parts[0]);
jsDocTag.default = parseJsDocValue(parts[1]);
}
else if (parts.length !== 0) {
// No default was given
jsDocTag.name = unqouteStr(parts[0]);
}
}
else {
// else, match required name
// Example: " myname"
// A name is needed some jsdoc tags making it possible to include omit "-"
// Therefore we don't look for "-" or line end if the name is required - in that case we only need to eat the first word to find the name.
const regex = JSDOC_TAGS_WITH_REQUIRED_NAME.includes(jsDocTag.tag) ? /^(\s*(\S+))/ : /^(\s*(\S+))((\s*-[\s\S]+)|\s*)($|[\r\n])/;
const nameResult = str.match(regex);
if (nameResult != null) {
// Move string to end of match
// Example: " myname|"
moveStr(nameResult[1]);
jsDocTag.name = unqouteStr(nameResult[2].trim());
}
}
};
const matchComment = () => {
// Match comment
if (str.length > 0) {
// The rest of the string is parsed as comment. Remove "-" if needed.
jsDocTag.description = str.replace(/^\s*-\s*/, "").trim() || undefined;
}
// Expand the name based on namespace and classname
if (jsDocTag.name != null) {
/**
* The name could look like this, so we need to parse and the remove the class name and namespace from the name
* InputSwitch#[CustomEvent]input-switch-check-changed
* InputSwitch#input-switch-check-changed
*/
const match = jsDocTag.name.match(/(.*)#(\[.*\])?(.*)/);
if (match != null) {
jsDocTag.className = match[1];
jsDocTag.namespace = match[2];
jsDocTag.name = match[3];
}
}
};
matchTag();
matchType();
matchName();
// Type can come both before and after "name"
if (jsDocTag.type == null) {
matchType();
}
matchComment();
return jsDocTag;
}
/**
* Parses an entire jsdoc string
* @param doc
*/
function parseJsDocString(doc) {
// Prepare lines
const lines = doc.split("\n").map(line => line.trim());
let description = "";
let readDescription = true;
let currentTag = "";
const tags = [];
/**
* Parsing will add to "currentTag" and commit it when necessary
*/
const commitCurrentTag = () => {
if (currentTag.length > 0) {
const tagToCommit = currentTag;
const tagMatch = tagToCommit.match(/^@(\S+)\s*/);
if (tagMatch != null) {
tags.push({
parsed: lazy(() => parseJsDocTagString(tagToCommit)),
node: undefined,
tag: tagMatch[1],
comment: tagToCommit.substr(tagMatch[0].length)
});
}
currentTag = "";
}
};
// Parse all lines one by one
for (const line of lines) {
// Don't parse the last line ("*/")
if (line.match(/\*\//)) {
continue;
}
// Match a line like: "* @mytag description"
const tagCommentMatch = line.match(/(^\s*\*\s*)@\s*/);
if (tagCommentMatch != null) {
// Commit current tag (if any has been read). Now "currentTag" will reset.
commitCurrentTag();
// Add everything on the line from "@"
currentTag += line.substr(tagCommentMatch[1].length);
// We hit a jsdoc tag, so don't read description anymore
readDescription = false;
}
else if (!readDescription) {
// If we are not reading the description, we are currently reading a multiline tag
const commentMatch = line.match(/^\s*\*\s*/);
if (commentMatch != null) {
currentTag += "\n" + line.substr(commentMatch[0].length);
}
}
else {
// Read everything after "*" into the description if we are currently reading the description
// If we are on the first line, add everything after "/*"
const startLineMatch = line.match(/^\s*\/\*\*/);
if (startLineMatch != null) {
description += line.substr(startLineMatch[0].length);
}
// Add everything after "*" into the current description
const commentMatch = line.match(/^\s*\*\s*/);
if (commentMatch != null) {
if (description.length > 0) {
description += "\n";
}
description += line.substr(commentMatch[0].length);
}
}
}
// Commit a tag if we were currently parsing one
commitCurrentTag();
if (description.length === 0 && tags.length === 0) {
return undefined;
}
return {
description: unescapeJSDoc(description),
tags
};
}
/**
* Certain characters as "@" can be escaped in order to prevent Typescript from
* parsing it as a jsdoc tag. This function unescapes these characters.
* @param str
*/
function unescapeJSDoc(str) {
return str.replace(/\\@/, "@");
}
const EVENT_NAMES = [
"Event",
"CustomEvent",
"AnimationEvent",
"ClipboardEvent",
"DragEvent",
"FocusEvent",
"HashChangeEvent",
"InputEvent",
"KeyboardEvent",
"MouseEvent",
"PageTransitionEvent",
"PopStateEvent",
"ProgressEvent",
"StorageEvent",
"TouchEvent",
"TransitionEvent",
"UiEvent",
"WheelEvent"
];
/**
* Discovers events dispatched
* @param node
* @param context
*/
function discoverEvents(node, context) {
var _a;
const { ts, checker } = context;
// new CustomEvent("my-event");
if (ts.isNewExpression(node)) {
const { expression, arguments: args } = node;
if (EVENT_NAMES.includes(expression.getText()) && args && args.length >= 1) {
const arg = args[0];
const eventName = (_a = resolveNodeValue(arg, { ...context, strict: true })) === null || _a === void 0 ? void 0 : _a.value;
if (typeof eventName === "string") {
// Either grab jsdoc from the new expression or from a possible call expression that its wrapped in
const jsDoc = getJsDoc(expression, ts) ||
(ts.isCallLikeExpression(node.parent) && getJsDoc(node.parent.parent, ts)) ||
(ts.isExpressionStatement(node.parent) && getJsDoc(node.parent, ts)) ||
undefined;
return [
{
jsDoc,
name: eventName,
node,
type: lazy(() => checker.getTypeAtLocation(node))
}
];
}
}
}
return undefined;
}
/**
* Discovers global feature defined on "HTMLElementEventMap" or "HTMLElement"
*/
const discoverGlobalFeatures$3 = {
event: (node, context) => {
var _a, _b;
const { ts, checker } = context;
if (context.ts.isInterfaceDeclaration(node) && ["HTMLElementEventMap", "GlobalEventHandlersEventMap"].includes(node.name.text)) {
const events = [];
for (const member of node.members) {
if (ts.isPropertySignature(member)) {
const name = (_a = resolveNodeValue(member.name, context)) === null || _a === void 0 ? void 0 : _a.value;
if (name != null && typeof name === "string") {
events.push({
node: member,
jsDoc: getJsDoc(member, ts),
name: name,
type: lazy(() => checker.getTypeAtLocation(member))
});
}
}
}
(_b = context === null || context === void 0 ? void 0 : context.emitContinue) === null || _b === void 0 ? void 0 : _b.call(context);
return events;
}
},
member: (node, context) => {
var _a, _b;
const { ts } = context;
if (context.ts.isInterfaceDeclaration(node) && node.name.text === "HTMLElement") {
const members = [];
for (const member of node.members) {
if (ts.isPropertySignature(member)) {
const name = (_a = resolveNodeValue(member.name, context)) === null || _a === void 0 ? void 0 : _a.value;
if (name != null && typeof name === "string") {
members.push({
priority: "medium",
node: member,
jsDoc: getJsDoc(member, ts),
kind: "property",
propName: name,
type: lazy(() => context.checker.getTypeAtLocation(member))
});
}
}
}
(_b = context === null || context === void 0 ? void 0 : context.emitContinue) === null || _b === void 0 ? void 0 : _b.call(context);
return members;
}
}
};
/**
* Discovers inheritance from a node by looking at "extends" and "implements"
* @param node
* @param baseContext
*/
function discoverInheritance$1(node, baseContext) {
let declarationKind = undefined;
const heritageClauses = [];
const declarationNodes = new Set();
const context = {
...baseContext,
emitDeclaration: decl => declarationNodes.add(decl),
emitInheritance: (kind, identifier) => heritageClauses.push({ kind, identifier, declaration: undefined }),
emitDeclarationKind: kind => (declarationKind = declarationKind || kind),
visitedNodes: new Set()
};
// Resolve the structure of the node
resolveStructure(node, context);
// Reverse heritage clauses because they come out in wrong order
heritageClauses.reverse();
return {
declarationNodes: Array.from(declarationNodes),
heritageClauses,
declarationKind
};
}
function resolveStructure(node, context) {
const { ts } = context;
if (context.visitedNodes.has(node)) {
return;
}
context.visitedNodes.add(node);
// Call this function recursively if this node is an identifier
if (ts.isIdentifier(node)) {
for (const decl of resolveDeclarationsDeep(node, context)) {
resolveStructure(decl, context);
}
}
// Emit declaration node if we've found a class of interface
else if (ts.isClassLike(node) || ts.isInterfaceDeclaration(node)) {
context.emitDeclarationKind(ts.isClassLike(node) ? "class" : "interface");
context.emitDeclaration(node);
// Resolve inheritance
for (const heritage of node.heritageClauses || []) {
for (const type of heritage.types || []) {
resolveHeritage(heritage, type.expression, context);
}
}
}
// Emit a declaration node if this node is a type literal
else if (ts.isTypeLiteralNode(node) || ts.isObjectLiteralExpression(node)) {
context.emitDeclarationKind("interface");
context.emitDeclaration(node);
}
// Emit a mixin if this node is a function
else if (ts.isFunctionLike(node) || ts.isCallLikeExpression(node)) {
context.emitDeclarationKind("mixin");
if (ts.isFunctionLike(node) && node.getSourceFile().isDeclarationFile) {
// Find any identifiers if the node is in a declaration file
findChildren(node.type, ts.isIdentifier, identifier => {
resolveStructure(identifier, context);
});
}
else {
// Else find the first class declaration in the block
// Note that we don't look for a return statement because this would complicate things
const clzDecl = findChild(node, ts.isClassLike);
if (clzDecl != null) {
resolveStructure(clzDecl, context);
return;
}
// If we didn't find any class declarations, we might be in a function that wraps a mixin
// Therefore find the return statement and call this method recursively
const returnNode = findChild(node, ts.isReturnStatement);
if (returnNode != null && returnNode.expression != null && returnNode.expression !== node) {
const returnNodeExp = returnNode.expression;
// If a function call is returned, this function call expression is followed, and the arguments are treated as heritage
// Example: return MyFirstMixin(MySecondMixin(Base)) --> MyFirstMixin is followed, and MySecondMixin + Base are inherited
if (ts.isCallExpression(returnNodeExp) && returnNodeExp.expression != null) {
for (const arg of returnNodeExp.arguments) {
resolveHeritage(undefined, arg, context);
}
resolveStructure(returnNodeExp.expression, context);
}
return;
}
}
}
else if (ts.isVariableDeclaration(node) && (node.initializer != null || node.type != null)) {
resolveStructure((node.initializer || node.type), context);
}
else if (ts.isIntersectionTypeNode(node)) {
emitTypeLiteralsDeclarations(node, context);
}
}
function resolveHeritage(heritage, node, context) {
const { ts } = context;
/**
* Parse mixins
*/
if (ts.isCallExpression(node)) {
// Mixins
const { expression: identifier, arguments: args } = node;
// Extend classes given to the mixin
// Example: class MyElement extends MyMixin(MyBase) --> MyBase
// Example: class MyElement extends MyMixin(MyBase1, MyBase2) --> MyBase1, MyBase2
for (const arg of args) {
resolveHeritage(heritage, arg, context);
}
// Resolve and traverse the mixin function
// Example: class MyElement extends MyMixin(MyBase) --> MyMixin
if (identifier != null && ts.isIdentifier(identifier)) {
resolveHeritage("mixin", identifier, context);
}
}
else if (ts.isIdentifier(node)) {
// Try to handle situation like this, by resolving the variable in between
// const Base = ExtraMixin(base);
// class MixinClass extends Base { }
let dontEmitHeritageClause = false;
// Resolve the declaration of this identifier
const declarations = resolveDeclarationsDeep(node, context);
for (const decl of declarations) {
// If the resolved declaration is a variable declaration assigned to a function, try to follow the assignments.
// Example: const MyBase = MyMixin(Base); return class extends MyBase { ... }
if (context.ts.isVariableDeclaration(decl) && decl.initializer != null) {
if (context.ts.isCallExpression(decl.initializer)) {
let hasDeclaration = false;
resolveStructure(decl, {
...context,
emitInheritance: () => { },
emitDeclarationKind: () => { },
emitDeclaration: () => {
hasDeclaration = true;
}
});
if (!hasDeclaration) {
resolveHeritage(heritage, decl.initializer, context);
dontEmitHeritageClause = true;
}
}
}
// Don't emit inheritance if it's a parameter, because the parameter
// is a subsitution for the actual base class which we have already resolved.
else if (context.ts.isParameter(decl)) {
dontEmitHeritageClause = true;
}
}
if (!dontEmitHeritageClause) {
// This is an "implements" clause if implement keyword is used or if all the resolved declarations are interfaces
const kind = heritage != null && typeof heritage === "string"
? heritage
: (heritage === null || heritage === void 0 ? void 0 : heritage.token) === ts.SyntaxKind.ImplementsKeyword ||
(declarations.length > 0 && !declarations.some(decl => !context.ts.isInterfaceDeclaration(decl)))
? "implements"
: "extends";
context.emitInheritance(kind, node);
}
}
}
/**
* Emits "type literals" in the AST. Emits them with "emitDeclaration"
* @param node
* @param context
*/
function emitTypeLiteralsDeclarations(node, context) {
var _a;
if (context.ts.isTypeLiteralNode(node)) {
// If we encounter a construct signature, follow the type
const construct = (_a = node.members) === null || _a === void 0 ? void 0 : _a.find((member) => context.ts.isConstructSignatureDeclaration(member));
if (construct != null && construct.type != null) {
context.emitDeclarationKind("mixin");
emitTypeLiteralsDeclarations(construct.type, context);
}
else {
context.emitDeclaration(node);
}
}
else {
node.forEachChild(n => emitTypeLiteralsDeclarations(n, context));
}
}
/**
* Discovers members based on standard vanilla custom element rules
* @param node
* @param context
*/
function discoverMembers$2(node, context) {
var _a, _b;
const { ts, checker } = context;
// Never pick up members not declared directly on the declaration node being traversed
if (node.parent !== context.declarationNode) {
return undefined;
}
// static get observedAttributes() { return ['c', 'l']; }
if (ts.isGetAccessor(node) && hasModifier(node, ts.SyntaxKind.StaticKeyword, ts)) {
if (node.name.getText() === "observedAttributes" && node.body != null) {
const members = [];
// Find either the first "return" statement or the first "array literal expression"
const arrayLiteralExpression = (_b = (_a = node.body.statements.find(statement => ts.isReturnStatement(statement))) === null || _a === void 0 ? void 0 : _a.expression) !== null && _b !== void 0 ? _b : node.body.statements.find(statement => ts.isArrayLiteralExpression(statement));
if (arrayLiteralExpression != null && ts.isArrayLiteralExpression(arrayLiteralExpression)) {
// Emit an attribute for each string literal in the array.
for (const attrNameNode of arrayLiteralExpression.elements) {
const attrName = ts.isStringLiteralLike(attrNameNode) ? attrNameNode.text : undefined;
if (attrName == null)
continue;
members.push({
priority: "medium",
node: attrNameNode,
jsDoc: getJsDoc(attrNameNode, ts),
kind: "attribute",
attrName,
type: undefined // () => ({ kind: "ANY" } as SimpleType),
});
}
}
return members;
}
}
// class { myProp = "hello"; }
else if (ts.isPropertyDeclaration(node) || ts.isPropertySignature(node)) {
const { name, initializer } = (() => {
if (ts.isPropertySignature(node)) {
return { name: node.name, initializer: undefined };
}
return node;
})();
if (ts.isIdentifier(name) || ts.isStringLiteralLike(name)) {
// Always ignore the "prototype" property
if (name.text === "prototype") {
return undefined;
}
// Find default value based on initializer
const resolvedDefaultValue = initializer != null ? resolveNodeValue(initializer, context) : undefined;
const def = resolvedDefaultValue != null ? resolvedDefaultValue.value : initializer === null || initializer === void 0 ? void 0 : initializer.getText();
return [
{
priority: "high",
node,
kind: "property",
jsDoc: getJsDoc(node, ts),
propName: name.text,
type: lazy(() => checker.getTypeAtLocation(node)),
default: def,
visibility: getMemberVisibilityFromNode(node, ts),
modifiers: getModifiersFromNode(node, ts)
//required: isPropertyRequired(node, context.checker),
}
];
}
}
// class { set myProp(value: string) { ... } }
else if (ts.isSetAccessor(node) || ts.isGetAccessor(node)) {
const { name, parameters } = node;
if (ts.isIdentifier(name)) {
const parameter = ts.isSetAccessor(node) != null && (parameters === null || parameters === void 0 ? void 0 : parameters.length) > 0 ? parameters[0] : undefined;
return [
{
priority: "high",
node,
jsDoc: getJsDoc(node, ts),
kind: "property",
propName: name.text,
type: lazy(() => (parameter == null ? context.checker.getTypeAtLocation(node) : context.checker.getTypeAtLocation(parameter))),
visibility: getMemberVisibilityFromNode(node, ts),
modifiers: getModifiersFromNode(node, ts)
}
];
}
}
// constructor { super(); this.title = "Hello"; }
else if (ts.isConstructorDeclaration(node)) {
if (node.body != null) {
const assignments = node.body.statements
.filter((stmt) => ts.isExpressionStatement(stmt))
.map(stmt => stmt.expression)
.filter((exp) => ts.isBinaryExpression(exp));
const members = [];
for (const assignment of assignments) {
const { left, right } = assignment;
if (ts.isPropertyAccessExpression(left)) {
if (left.expression.kind === ts.SyntaxKind.ThisKeyword) {
const propName = left.name.getText();
const resolvedInitializer = resolveNodeValue(right, context);
const def = resolvedInitializer != null ? resolvedInitializer.value : undefined; //right.getText();
members.push({
priority: "low",
node,
kind: "property",
propName,
default: def,
type: () => relaxType(tsSimpleType.toSimpleType(checker.getTypeAtLocation(right), checker)),
jsDoc: getJsDoc(assignment.parent, ts),
visibility: isNamePrivate(propName) ? "private" : undefined
});
}
}
}
return members;
}
}
return undefined;
}
/**
* Discovers methods
* @param node
* @param context
*/
function discoverMethods(node, context) {
var _a;
const { ts } = context;
// Never pick up method declaration not declared directly on the declaration node being traversed
if (node.parent !== context.declarationNode) {
return undefined;
}
// class { myMethod () {} }
if ((ts.isMethodDeclaration(node) || ts.isMethodSignature(node)) && !hasModifier(node, ts.SyntaxKind.StaticKeyword, ts)) {
// Outscope static methods for now
const name = node.name.getText();
if (!context.config.analyzeDefaultLib && isHTMLElementMethodName(name)) {
return undefined;
}
// Allow the analyzer to analyze within methods
(_a = context.emitContinue) === null || _a === void 0 ? void 0 : _a.call(context);
return [
{
jsDoc: getJsDoc(node, ts),
name,
node: node,
visibility: getMemberVisibilityFromNode(node, ts),
type: lazy(() => context.checker.getTypeAtLocation(node))
}
];
}
return undefined;
}
function isHTMLElementMethodName(name) {
return ["attributeChangedCallback", "connectedCallback", "disconnectedCallback"].includes(name);
}
/**
* Excludes nodes from "lib.dom.d.ts" if analyzeLibDom is false
* @param node
* @param context
*/
function excludeNode$2(node, context) {
if (context.config.analyzeDefaultLib) {
return undefined;
}
return isLibDom(node);
}
function isLibDom(node) {
return node.getSourceFile().fileName.endsWith("lib.dom.d.ts");
}
/**
* A flavor that discovers using standard custom element rules
*/
class CustomElementFlavor {
constructor() {
this.excludeNode = excludeNode$2;
this.discoverDefinitions = discoverDefinitions$5;
this.discoverFeatures = {
member: discoverMembers$2,
event: discoverEvents,
method: discoverMethods
};
this.discoverGlobalFeatures = discoverGlobalFeatures$3;
this.discoverInheritance = discoverInheritance$1;
}
}
/**
* Transforms jsdoc tags to a T array using a "transform"
* @param node
* @param tagNames
* @param transform
* @param context
*/
function parseJsDocForNode(node, tagNames, transform, context) {
var _a;
const { tags } = getJsDoc(node, context.ts, tagNames) || {};
if (tags != null && tags.length > 0) {
(_a = context.emitContinue) === null || _a === void 0 ? void 0 : _a.call(context);
return arrayDefined(tags.map(tag => transform(tag.node, tag.parsed())));
}
return undefined;
}
/**
* Discovers definitions using "@customElement" or "@element" jsdoc
* @param node
* @param context
*/
function discoverDefinitions$4(node, context) {
// /** @customElement my-element */ myClass extends HTMLElement { ... }
if (context.ts.isInterfaceDeclaration(node) || context.ts.isClassDeclaration(node)) {
const identifier = getNodeIdentifier(node, context);
return parseJsDocForNode(node, ["customelement", "element"], (tagNode, { name }) => {
return {
tagName: name || "",
definitionNode: tagNode,
identifierNode: identifier,
tagNameNode: tagNode
};
}, context);
}
}
const discoverFeatures$1 = {
csspart: (node, context) => {
if (context.ts.isInterfaceDeclaration(node) || context.ts.isClassDeclaration(node)) {
return parseJsDocForNode(node, ["csspart"], (tagNode, { name, description }) => {
if (name != null && name.length > 0) {
return {
name: name,
jsDoc: description != null ? { description } : undefined
};
}
}, context);
}
},
cssproperty: (node, context) => {
if (context.ts.isInterfaceDeclaration(node) || context.ts.isClassDeclaration(node)) {
return parseJsDocForNode(node, ["cssprop", "cssproperty", "cssvar", "cssvariable"], (tagNode, { name, description, type, default: def }) => {
if (name != null && name.length > 0) {
return {
name: name,
jsDoc: description != null ? { description } : undefined,
typeHint: type || undefined,
default: def
};
}
}, context);
}
},
event: (node, context) => {
if (context.ts.isInterfaceDeclaration(node) || context.ts.isClassDeclaration(node)) {
return parseJsDocForNode(node, ["event", "fires", "emits"], (tagNode, { name, description, type }) => {
if (name != null && name.length > 0 && tagNode != null) {
return {
name: name,
jsDoc: description != null ? { description } : undefined,
type: type != null ? lazy(() => parseSimpleJsDocTypeExpression(type, context) || { kind: "ANY" }) : undefined,
typeHint: type,
node: tagNode
};
}
}, context);
}
},
slot: (node, context) => {
if (context.ts.isInterfaceDeclaration(node) || context.ts.isClassDeclaration(node)) {
return parseJsDocForNode(node, ["slot"], (tagNode, { name, type, description }) => {
// Treat "-" as unnamed slot
if (name === "-") {
name = undefined;
}
// Grab the type from jsdoc and use it to find permitted tag names
// Example: @slot {"div"|"span"} myslot
const permittedTagNameType = type == null ? undefined : parseSimpleJsDocTypeExpression(type, context);
const permittedTagNames = (() => {
if (permittedTagNameType == null) {
return undefined;
}
switch (permittedTagNameType.kind) {
case "STRING_LITERAL":
return [permittedTagNameType.value];
case "UNION":
return permittedTagNameType.types
.filter((type) => type.kind === "STRING_LITERAL")
.map(type => type.value);
default:
return undefined;
}
})();
return {
name: name,
jsDoc: description != null ? { description } : undefined,
permittedTagNames
};
}, context);
}
},
member: (node, context) => {
if (context.ts.isInterfaceDeclaration(node) || context.ts.isClassDeclaration(node)) {
const priority = getNodeSourceFileLang(node) === "js" ? "high" : "medium";
const properties = parseJsDocForNode(node, ["prop", "property"], (tagNode, { name, default: def, type, description }) => {
if (name != null && name.length > 0) {
return {
priority,
kind: "property",
propName: name,
jsDoc: description != null ? { description } : undefined,
typeHint: type,
type: lazy(() => (type && parseSimpleJsDocTypeExpression(type, context)) || { kind: "ANY" }),
node: tagNode,
default: def,
visibility: undefined,
reflect: undefined,
required: undefined,
deprecated: undefined
};
}
}, context);
const attributes = parseJsDocForNode(node, ["attr", "attribute"], (tagNode, { name, default: def, type, description }) => {
if (name != null && name.length > 0) {
return {
priority,
kind: "attribute",
attrName: name,
jsDoc: description != null ? { description } : undefined,
type: lazy(() => (type && parseSimpleJsDocTypeExpression(type, context)) || { kind: "ANY" }),
typeHint: type,
node: tagNode,
default: def,
visibility: undefined,
reflect: undefined,
required: undefined,
deprecated: undefined
};
}
}, context);
if (attributes != null || properties != null) {
return [...(attributes || []), ...(properties || [])];
}
return undefined;
}
}
};
const discoverGlobalFeatures$2 = {
csspart: (node, context) => {
var _a;
if (context.ts.isInterfaceDeclaration(node) && node.name.text === "HTMLElement") {
return (_a = discoverFeatures$1.csspart) === null || _a === void 0 ? void 0 : _a.call(discoverFeatures$1, node, context);
}
},
cssproperty: (node, context) => {
var _a;
if (context.ts.isInterfaceDeclaration(node) && node.name.text === "HTMLElement") {
return (_a = discoverFeatures$1.cssproperty) === null || _a === void 0 ? void 0 : _a.call(discoverFeatures$1, node, context);
}
},
event: (node, context) => {
var _a;
if (context.ts.isInterfaceDeclaration(node) && node.name.text === "HTMLElement") {
return (_a = discoverFeatures$1.event) === null || _a === void 0 ? void 0 : _a.call(discoverFeatures$1, node, context);
}
},
slot: (node, context) => {
var _a;
if (context.ts.isInterfaceDeclaration(node) && node.name.text === "HTMLElement") {
return (_a = discoverFeatures$1.slot) === null || _a === void 0 ? void 0 : _a.call(discoverFeatures$1, node, context);
}
},
member: (node, context) => {
var _a;
if (context.ts.isInterfaceDeclaration(node) && node.name.text === "HTMLElement") {
return (_a = discoverFeatures$1 === null || discoverFeatures$1 === void 0 ? void 0 : discoverFeatures$1.member) === null || _a === void 0 ? void 0 : _a.call(discoverFeatures$1, node, context);
}
}
};
/**
* Refines a component declaration by using jsdoc tags
* @param declaration
* @param context
*/
function refineDeclaration$1(declaration, context) {
if (declaration.jsDoc == null || declaration.jsDoc.tags == null) {
return undefined;
}
// Applies the "@deprecated" jsdoc tag
const deprecatedTag = declaration.jsDoc.tags.find(t => t.tag === "deprecated");
if (deprecatedTag != null) {
return {
...declaration,
deprecated: deprecatedTag.comment || true
};
}
return undefined;
}
/**
* Refines features by looking at the jsdoc tags on the feature
*/
const refineFeature$3 = {
event: (event, context) => {
if (event.jsDoc == null || event.jsDoc.tags == null)
return event;
// Check if the feature has "@ignore" jsdoc tag
if (hasIgnoreJsDocTag(event.jsDoc)) {
return undefined;
}
return [applyJsDocDeprecated, applyJsDocVisibility, applyJsDocType].reduce((event, applyFunc) => applyFunc(event, event.jsDoc, context), event);
},
method: (method, context) => {
if (method.jsDoc == null || method.jsDoc.tags == null)
return method;
// Check if the feature has "@ignore" jsdoc tag
if (hasIgnoreJsDocTag(method.jsDoc)) {
return undefined;
}
method = [applyJsDocDeprecated, applyJsDocVisibility].reduce((method, applyFunc) => applyFunc(method, method.jsDoc, context), method);
return method;
},
member: (member, context) => {
// Return right away if the member doesn't have jsdoc
if (member.jsDoc == null || member.jsDoc.tags == null)
return member;
// Check if the feature has "@ignore" jsdoc tag
if (hasIgnoreJsDocTag(member.jsDoc)) {
return undefined;
}
return [
applyJsDocDeprecated,
applyJsDocVisibility,
applyJsDocRequired,
applyJsDocDefault,
applyJsDocReflect,
applyJsDocType,
applyJsDocAttribute,
applyJsDocModifiers
].reduce((member, applyFunc) => applyFunc(member, member.jsDoc, context), member);
}
};
/**
* Applies the "@deprecated" jsdoc tag
* @param feature
* @param jsDoc
*/
function applyJsDocDeprecated(feature, jsDoc) {
var _a;
const deprecatedTag = (_a = jsDoc.tags) === null || _a === void 0 ? void 0 : _a.find(tag => tag.tag === "deprecated");
if (deprecatedTag != null) {
return {
...feature,
deprecated: deprecatedTag.comment || true
};
}
return feature;
}
/**
* Applies the "@access" jsdoc tag
* @param feature
* @param jsDoc
*/
function applyJsDocVisibility(feature, jsDoc) {
var _a;
const visibilityTag = (_a = jsDoc.tags) === null || _a === void 0 ? void 0 : _a.find(tag => ["public", "protected", "private", "package", "access"].includes(tag.tag)); // member + method
if (visibilityTag != null) {
return {
...feature,
visibility: (() => {
switch (visibilityTag.tag) {
case "public":
return "public";
case "protected":
return "protected";
case "package":
case "private":
return "private";
case "access":
switch (visibilityTag.parsed().name) {
case "public":
return "public";
case "protected":
return "protected";
case "private":
case "package":
return "private";
default:
return undefined;
}
default:
return undefined;
}
})()
};
}
return feature;
}
/**
* Applies the "@attribute" jsdoc tag
* @param feature
* @param jsDoc
* @param context
*/
function applyJsDocAttribute(feature, jsDoc, context) {
var _a, _b, _c;
const attributeTag = (_a = jsDoc.tags) === null || _a === void 0 ? void 0 : _a.find(tag => ["attr", "attribute"].includes(tag.tag));
if (attributeTag != null && feature.attrName == null) {
const parsed = attributeTag.parsed();
const result = {
...feature,
attrName: attributeTag.parsed().name || feature.propName,
default: (_b = feature.default) !== null && _b !== void 0 ? _b : parsed.default
};
// @attr jsdoc tag can also include the type of attribute
if (parsed.type != null && result.typeHint == null) {
result.typeHint = parsed.type;
result.type = (_c = feature.type) !== null && _c !== void 0 ? _c : lazy(() => parseSimpleJsDocTypeExpression(parsed.type || "", context));
}
return result;
}
return feature;
}
/**
* Applies the "@required" jsdoc tag
* @param feature
* @param jsDoc
*/
function applyJsDocRequired(feature, jsDoc) {
var _a;
const requiredTag = (_a = jsDoc.tags) === null || _a === void 0 ? void 0 : _a.find(tag => ["optional", "required"].includes(tag.tag));
if (requiredTag != null) {
return {
...feature,
required: requiredTag.tag === "required"
};
}
return feature;
}
/**
* Applies the "@readonly" jsdoc tag
* @param feature
* @param jsDoc
*/
function applyJsDocModifiers(feature, jsDoc) {
var _a;
const readonlyTag = (_a = jsDoc.tags) === null || _a === void 0 ? void 0 : _a.find(tag => tag.tag === "readonly");
if (readonlyTag != null) {
return {
...feature,
modifiers: (feature.modifiers != null ? new Set(feature.modifiers) : new Set()).add("readonly")
};
}
return feature;
}
/**
* Applies the "@default" jsdoc tag
* @param feature
* @param jsDoc
*/
function applyJsDocDefault(feature, jsDoc) {
var _a;
const defaultTag = (_a = jsDoc.tags) === null || _a === void 0 ? void 0 : _a.find(tag => tag.tag === "default");
if (defaultTag != null) {
return {
...feature,
default: defaultTag.comment
};
}
return feature;
}
/**
* Applies the "@reflect" jsdoc tag
* @param feature
* @param jsDoc
*/
function applyJsDocReflect(feature, jsDoc) {
var _a;
const reflectTag = (_a = jsDoc.tags) === null || _a === void 0 ? void 0 : _a.find(tag => tag.tag === "reflect");
if (reflectTag != null && feature.reflect == null) {
return {
...feature,
reflect: (() => {
switch (reflectTag.comment) {
case "to-attribute":
return "to-attribute";
case "to-property":
return "to-property";
case "both":
return "both";
default:
return undefined;
}
})()
};
}
return feature;
}
/**
* Applies the "@type" jsdoc tag
* @param feature
* @param jsDoc
* @param context
*/
function applyJsDocType(feature, jsDoc, context) {
var _a, _b;
const typeTag = (_a = jsDoc.tags) === null || _a === void 0 ? void 0 : _a.find(tag => tag.tag === "type");
if (typeTag != null && feature.typeHint == null) {
const parsed = typeTag.parsed();
if (parsed.type != null && parsed.type.length > 0) {
return {
...feature,
typeHint: parsed.type,
type: (_b = feature.type) !== null && _b !== void 0 ? _b : lazy(() => parseSimpleJsDocTypeExpression(parsed.type || "", context))
};
}
}
return feature;
}
/**
* Returns if jsdoc contains an ignore node
* @param jsDoc
*/
function hasIgnoreJsDocTag(jsDoc) {
var _a;
return ((_a = jsDoc === null || jsDoc === void 0 ? void 0 : jsDoc.tags) === null || _a === void 0 ? void 0 : _a.find(tag => tag.tag === "ignore")) != null;
}
/**
* Flavors for analyzing jsdoc related features
*/
class JsDocFlavor {
constructor() {
this.discoverDefinitions = discoverDefinitions$4;
this.discoverFeatures = discoverFeatures$1;
this.discoverGlobalFeatures = discoverGlobalFeatures$2;
this.refineFeature = refineFeature$3;
this.refineDeclaration = refineDeclaration$1;
}
}
/**
* Discovers element definitions in "IntrinsicElements"
* @param node
* @param context
*/
function discoverDefinitions$3(node, context) {
const { ts } = context;
if (ts.isInterfaceDeclaration(node)) {
if (node.name.text === "IntrinsicElements") {
const extensions = getInterfaceKeys(node, context);
return extensions.map(({ key, keyNode, identifier, declaration }) => ({
tagName: key,
tagNameNode: keyNode,
identifierNode: identifier,
declarationNode: declaration
}));
}
}
return undefined;
}
/**
* Discovers members declared on "IntrinsicAttributes"
*/
const discoverGlobalFeatures$1 = {
member: (node, context) => {
var _a, _b;
const { ts } = context;
if (ts.isInterfaceDeclaration(node) && node.name.text === "IntrinsicAttributes") {
const members = [];
for (const member of node.members) {
if (ts.isPropertySignature(member)) {
const name = (_a = resolveNodeValue(member.name, context)) === null || _a === void 0 ? void 0 : _a.value;
if (name != null && typeof name === "string") {
members.push({
priority: "medium",
node: member,
jsDoc: getJsDoc(member, ts),
kind: "property",
propName: name,
attrName: name,
type: () => context.checker.getTypeAtLocation(member)
});
}
}
}
(_b = context === null || context === void 0 ? void 0 : context.emitContinue) === null || _b === void 0 ? void 0 : _b.call(context);
return members;
}
}
};
/**
* Flavors for analyzing jsx related features
*/
class JSXFlavor {
constructor() {
this.discoverDefinitions = discoverDefinitions$3;
this.discoverGlobalFeatures = discoverGlobalFeatures$1;
}
}
/**
* Visits lit-element related definitions.
* Specifically it finds the usage of the @customElement decorator.
* @param node
* @param context
*/
function discoverDefinitions$2(node, context) {
const { ts, checker } = context;
// @customElement("my-element")
if (ts.isClassDeclaration(node)) {
// Visit all decorators on the class
for (const decorator of getDecorators(node, context)) {
const callExpression = decorator.expression;
// Find "@customElement"
if (ts.isCallExpression(callExpression) && ts.isIdentifier(callExpression.expression)) {
const decoratorIdentifierName = callExpression.expression.escapedText;
// Decorators called "customElement"
if (decoratorIdentifierName === "customElement") {
// Resolve the value of the first argument. This is the tag name.
const unresolvedTagNameNode = callExpression.arguments[0];
const resolvedTagNameNode = resolveNodeValue(unresolvedTagNameNode, { ts, checker, strict: true });
const identifier = getNodeIdentifier(node, context);
if (resolvedTagNameNode != null && typeof resolvedTagNameNode.value === "string") {
return [
{
tagName: resolvedTagNameNode.value,
tagNameNode: resolvedTagNameNode.node,
identifierNode: identifier
}
];
}
}
}
}
return;
}
node.forEachChild(child => {
discoverDefinitions$2(child, context);
});
}
const LIT_ELEMENT_PROPERTY_DECORATOR_KINDS = ["property", "internalProperty", "state"];
/**
* Returns a potential lit element property decorator.
* @param node
* @param context
*/
function getLitElementPropertyDecorator(node, context) {
const { ts } = context;
// Find a decorator with "property" name.
for (const decorator of getDecorators(node, context)) {
const expression = decorator.expression;
// We find the first decorator calling specific identifier name (found in LIT_ELEMENT_PROPERTY_DECORATOR_KINDS)
if (ts.isCallExpression(expression) && ts.isIdentifier(expression.expression)) {
const identifier = expression.expression;
const kind = identifier.text;
if (LIT_ELEMENT_PROPERTY_DECORATOR_KINDS.includes(kind)) {
return { expression, kind };
}
}
}
return undefined;
}
/**
* Returns a potential lit property decorator configuration.
* @param node
* @param context
*/
function getLitElementPropertyDecoratorConfig(node, context) {
// Get reference to a possible "@property" decorator.
const decorator = getLitElementPropertyDecorator(node, context);
if (decorator != null) {
// Parse the first argument to the decorator which is the lit-property configuration.
const configNode = decorator.expression.arguments[0];
// Add decorator to "nodes"
const config = { node: { decorator: decorator.expression } };
// Apply specific config based on the decorator kind
switch (decorator.kind) {
case "internalProperty":
case "state":
config.attribute = false;
config.state = true;
break;
}
if (configNode == null) {
return config;
}
const resolved = resolveNodeValue(configNode, context);
return resolved != null ? getLitPropertyOptions(resolved.node, resolved.value, context, config) : config;
}
return undefined;
}
/**
* Determines if a given object has the specified property, used
* as a type-guard.
* @param obj
* @param key
*/
function hasOwnProperty(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key);
}
/**
* Computes the correct type for a given node for use in lit property
* configuration.
* @param ts
* @param node
*/
function getLitPropertyType(ts, node) {
const value = ts.isIdentifier(node) ? node.text : undefined;
switch (value) {
case "String":
case "StringConstructor":
return { kind: "STRING" };
case "Number":
case "NumberConstructor":
return { kind: "NUMBER" };
case "Boolean":
case "BooleanConstructor":
return { kind: "BOOLEAN" };
case "Array":
case "ArrayConstructor":
return { kind: "ARRAY", type: { kind: "ANY" } };
case "Object":
case "ObjectConstructor":
return { kind: "OBJECT", members: [] };
default:
// This is an unknown type, so set the name as a string
return node.getText();
}
}
/**
* Parses an object literal expression and returns a lit property configuration.
* @param node
* @param existingConfig
* @param context
*/
function getLitPropertyOptions(node, object, context, existingConfig = {}) {
const { ts } = context;
const result = { ...existingConfig };
let attributeInitializer;
let typeInitializer;
if (typeof object === "object" && object !== null && !Array.isArray(object)) {
if (hasOwnProperty(object, "converter") && object.converter !== undefined) {
result.hasConverter = true;
}
if (hasOwnProperty(object, "reflect") && object.reflect !== undefined) {
result.reflect = object.reflect === true;
}
if (hasOwnProperty(object, "state") && object.state !== undefined) {
result.state = object.state === true;
}
if (hasOwnProperty(object, "value")) {
result.default = object.value;
}
if (hasOwnProperty(object, "attribute") && (typeof object.attribute === "boolean" || typeof object.attribute === "string")) {
result.attribute = object.attribute;
if (ts.isObjectLiteralExpression(node)) {
const prop = node.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "attribute");
if (prop) {
attributeInitializer = prop.initializer;
}
}
}
}
if (ts.isObjectLiteralExpression(node)) {
const typeProp = node.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "type");
if (typeProp) {
typeInitializer = typeProp.initializer;
result.type = getLitPropertyType(ts, typeProp.initializer);
}
}
return {
...result,
node: {
...(result.node || {}),
attribute: attributeInitializer,
type: typeInitializer
}
};
}
/**
* Parses lit-related declaration members.
* This is primary by looking at the "@property" decorator and the "static get properties()".
* @param node
* @param context
*/
function discoverMembers$1(node, context) {
const { ts } = context;
// Never pick up members not declared directly on the declaration node being traversed
if (node.parent !== context.declarationNode) {
return undefined;
}
// static get properties() { return { myProp: {type: String} } }
if (ts.isGetAccessor(node) && hasModifier(node, ts.SyntaxKind.StaticKeyword, ts)) {
const name = node.name.getText();
if (name === "properties" && node.body != null) {
const returnStatement = node.body.statements.find(ts.isReturnStatement.bind(ts));
if (returnStatement != null) {
return parseStaticProperties(returnStatement, context);
}
}
}
// @property({type: String}) myProp = "hello";
else if (ts.isSetAccessor(node) || ts.isGetAccessor(node) || ts.isPropertyDeclaration(node) || ts.isPropertySignature(node)) {
return parsePropertyDecorator$1(node, context);
}
}
/**
* Visits a lit property decorator and returns members based on it.
* @param node
* @param context
*/
function parsePropertyDecorator$1(node, context) {
const { ts, checker } = context;
// Parse the content of a possible lit "@property" decorator.
const litConfig = getLitElementPropertyDecoratorConfig(node, context);
if (litConfig != null) {
const propName = node.name.getText();
// Get the attribute based on the configuration
const attrName = getLitAttributeName(propName, litConfig, context);
// Find the default value for this property
const initializer = "initializer" in node ? node.initializer : undefined;
const resolvedDefaultValue = initializer != null ? resolveNodeValue(initializer, context) : undefined;
const def = resolvedDefaultValue != null ? resolvedDefaultValue.value : initializer === null || initializer === void 0 ? void 0 : initializer.getText();
// Find our if the property/attribute is required
//const required = ("initializer" in node && isPropertyRequired(node, context.checker)) || undefined;
const required = undefined;
const jsDoc = getJsDoc(node, ts);
// Emit a property with "attrName"
return [
{
priority: "high",
kind: "property",
propName,
attrName,
type: lazy(() => {
const propType = checker.getTypeAtLocation(node);
const inJavascriptFile = getNodeSourceFileLang(node) === "js";
return inJavascriptFile && typeof litConfig.type === "object" && litConfig.type.kind === "ANY" ? litConfig.type : propType;
}),
node,
default: def,
required,
jsDoc,
meta: litConfig,
visibility: getMemberVisibilityFromNode(node, ts),
reflect: litConfig.reflect ? "both" : attrName != null ? "to-property" : undefined,
modifiers: getModifiersFromNode(node, ts)
}
];
}
return undefined;
}
/**
* Returns if we are in a Polymer context.
* @param context
*/
function inPolymerFlavorContext(context) {
var _a, _b, _c;
const declaration = context.getDeclaration();
// TODO: find a better way to construct a cache key
const cacheKey = `isPolymerFlavorContext:${((_a = context.sourceFile) === null || _a === void 0 ? void 0 : _a.fileName) || "unknown"}`;
if (context.cache.general.has(cacheKey)) {
return context.cache.general.get(cacheKey);
}
let result = false;
// Use "@polymer" jsdoc tag to indicate that this is polymer context
if ((_c = (_b = declaration.jsDoc) === null || _b === void 0 ? void 0 : _b.tags) === null || _c === void 0 ? void 0 : _c.some(t => t.tag === "polymer" || t.tag === "polymerElement")) {
result = true;
}
// TODO: This only checks the immediate inheritance. Make it recursive to go throught the entire inheritance chain.
if (context.getDeclaration().heritageClauses.some(c => ["PolymerElement", "Polymer.Element"].includes(c.identifier.getText()))) {
result = true;
}
context.cache.general.set(cacheKey, result);
return result;
}
/**
* Returns an attribute name based on a property name and a lit-configuration
* @param propName
* @param litConfig
* @param context
*/
function getLitAttributeName(propName, litConfig, context) {
// Don't emit attribute if the value is specifically "false"
if (litConfig.attribute === false) {
return undefined;
}
// Get the attribute name either by looking at "{attribute: ...}" or just taking the property name.
let attrName = typeof litConfig.attribute === "string" ? litConfig.attribute : propName;
if (inPolymerFlavorContext(context)) {
// From the documentation: https://polymer-library.polymer-project.org/3.0/docs/devguide/properties#attribute-reflection
attrName = camelToDashCase(attrName).toLowerCase();
}
return attrName;
}
/**
* Visits static properties
* static get properties() { return { myProp: {type: String, attribute: "my-attr"} } }
* @param returnStatement
* @param context
*/
function parseStaticProperties(returnStatement, context) {
const { ts } = context;
const memberResults = [];
if (returnStatement.expression != null && ts.isObjectLiteralExpression(returnStatement.expression)) {
// Each property in the object literal expression corresponds to a class field.
for (const propNode of returnStatement.expression.properties) {
// Get propName
const propName = propNode.name != null && ts.isIdentifier(propNode.name) ? propNode.name.text : undefined;
if (propName == null) {
continue;
}
// Parse the lit property config for this property
// Treat non-object-literal-expressions like the "type" (to support Polymer specific syntax)
let litConfig = {};
if (ts.isPropertyAssignment(propNode)) {
if (inPolymerFlavorContext(context) && !ts.isObjectLiteralExpression(propNode.initializer)) {
litConfig = { type: getLitPropertyType(ts, propNode.initializer) };
}
else {
const resolved = resolveNodeValue(propNode.initializer, context);
if (resolved) {
litConfig = getLitPropertyOptions(resolved.node, resolved.value, context, litConfig);
}
}
}
// Get attrName based on the litConfig
const attrName = getLitAttributeName(propName, litConfig, context);
// Get more metadata
const jsDoc = getJsDoc(propNode, ts);
const emitAttribute = litConfig.attribute !== false;
// Emit either the attribute or the property
memberResults.push({
priority: "high",
kind: "property",
type: lazy(() => {
return (jsDoc && getJsDocType(jsDoc, context)) || (typeof litConfig.type === "object" && litConfig.type) || { kind: "ANY" };
}),
propName: propName,
attrName: emitAttribute ? attrName : undefined,
jsDoc,
node: propNode,
meta: litConfig,
default: litConfig.default,
reflect: litConfig.reflect ? "both" : attrName != null ? "to-property" : undefined,
visibility: isNamePrivate(propName) ? "private" : undefined
});
}
}
return memberResults;
}
function excludeNode$1(node, context) {
if (context.config.analyzeDependencies) {
return undefined;
}
// Exclude lit element related super classes if "analyzeLib" is false
const declName = getNodeName(node, context);
if (declName != null) {
return declName === "LitElement" || declName === "UpdatingElement";
}
else {
const fileName = node.getSourceFile().fileName;
return fileName.includes("/lit-element.") || fileName.endsWith("/updating-element.");
}
}
const refineFeature$2 = {
method: (method, context) => {
// This is temporary, but for now we force lit-element named methods to be protected
if (LIT_ELEMENT_PROTECTED_METHODS.includes(method.name)) {
return {
...method,
visibility: "protected"
};
}
return method;
}
};
const LIT_ELEMENT_PROTECTED_METHODS = [
"render",
"requestUpdate",
"firstUpdated",
"updated",
"update",
"shouldUpdate",
"hasUpdated",
"updateComplete"
];
/**
* Flavors for analyzing LitElement related features: https://lit-element.polymer-project.org/
*/
class LitElementFlavor {
constructor() {
this.excludeNode = excludeNode$1;
this.discoverDefinitions = discoverDefinitions$2;
this.discoverFeatures = {
member: discoverMembers$1
};
this.refineFeature = refineFeature$2;
}
}
const LWCCACHE = Symbol("LWC Component");
function getLwcComponent(node, context) {
const { ts } = context;
if (ts.isClassDeclaration(node)) {
if (node[LWCCACHE]) {
return node[LWCCACHE];
}
const r = _isLwcComponent(node, context);
node[LWCCACHE] = r;
return r;
}
return undefined;
}
function _isLwcComponent(node, context) {
// Return right away if the node is not a class declaration
if (!context.ts.isClassDeclaration(node)) {
return undefined;
}
const jsName = node.getSourceFile().fileName;
const splitjsName = jsName.split("/");
if (splitjsName.length < 3) {
return;
}
const nameSpace = splitjsName[splitjsName.length - 3];
const componentName = splitjsName[splitjsName.length - 2];
const tagName = nameSpace + "-" + camelToDashCase(componentName);
// Main case (~100% of the cases)
// The class is a default export and there is a template (.html) starting with <template>
// Moreover the JS file name should match the directory name, minus the extension (js|ts)
// https://lwc.dev/guide/reference#html-file
const flags = context.ts.getCombinedModifierFlags(node);
if (flags & context.ts.ModifierFlags.ExportDefault && (jsName.endsWith(".js") || jsName.endsWith(".ts"))) {
const fileName = splitjsName[splitjsName.length - 1];
const fileNoExt = fileName.substring(0, fileName.length - 3);
if (fileNoExt === componentName) {
const htmlName = jsName.substring(0, jsName.length - 3) + ".html";
if (fs.existsSync(htmlName)) {
const content = fs.readFileSync(htmlName, "utf8").trim();
if (content.startsWith("<template>")) {
return { tagName };
}
}
}
}
// Edge case
// The components are not matching the file naming recommendations, so we check the inheritance
const lightning = inheritFromLightning(node, context);
if (lightning) {
return { tagName };
}
// Finally, we use a JS doc definition in case none of the above work
// This is the last resort
const v = parseJsDocForNode(node, ["lwcelement"], (tagNode, { name }) => {
return { tagName: name || tagName };
}, context);
if (v && v.length === 1) {
return v[0];
}
}
// Check if the Class inherits from lighning
// For now, we just check one level
function inheritFromLightning(node, context) {
const { checker } = context;
if (node.heritageClauses) {
for (const clause of node.heritageClauses) {
// OK we are getting strange results here with the token beeing 87 (ElseKeyword), 89 (ExportKeyword)
// Not sure why for now, so we skip checking the keyword as 'LightningElement' is dicriminant enough
if (clause.token == context.ts.SyntaxKind.ExtendsKeyword) {
const symbol = checker.getSymbolAtLocation(clause.types[0].expression);
if ((symbol === null || symbol === void 0 ? void 0 : symbol.escapedName) === "LightningElement") {
return true;
}
}
}
}
return false;
}
// In case we need to debug the nodes
// export function print(node: Node, context: AnalyzerVisitContext): void {
// const {ts} = context;
// // eslint-disable-next-line no-console
// console.log(`\n========== Syntax tree for ${ts.SyntaxKind[node.kind]}`);
// let indent = 1;
// function _print(node: Node) {
// // eslint-disable-next-line no-console
// console.log(new Array(indent + 1).join(' ') + ts.SyntaxKind[node.kind]);
// indent++;
// ts.forEachChild(node, _print);
// indent--;
// }
// _print(node);
// // eslint-disable-next-line no-console
// console.log(`======================\n`);
// }
/**
* Checks if the element has an lwc property decorator (@api).
* @param node
* @param context
*/
function hasLwcApiPropertyDecorator(node, context) {
const { ts } = context;
// Find a decorator with "api" name.
for (const decorator of getDecorators(node, context)) {
const expression = decorator.expression;
// We find the first decorator calling specific identifier name (@api)
// Note that this is not a call expression, so we just check that the decorator is an identifier
if (ts.isIdentifier(expression)) {
const kind = expression.text;
if (kind === "api") {
return true;
}
}
}
return false;
}
/**
* Parses LWC related declaration members.
* This is primary by looking at the "@api" decorator
* @param node
* @param context
*/
function discoverMembers(node, context) {
const { ts } = context;
// Never pick up members not declared directly on the declaration node being traversed
if (node.parent !== context.declarationNode) {
return undefined;
}
// @api myProp = "hello";
if (ts.isSetAccessor(node) || ts.isGetAccessor(node) || ts.isPropertyDeclaration(node) || ts.isPropertySignature(node)) {
return parsePropertyDecorator(node, context);
}
}
/**
* Visits a LWC property decorator and returns members based on it.
* @param node
* @param context
*/
function parsePropertyDecorator(node, context) {
const { ts, checker } = context;
// Parse the content of a possible lit "@api" decorator.
const lwcApi = hasLwcApiPropertyDecorator(node, context);
if (lwcApi) {
const propName = node.name.getText();
// In LWC, the attribute name is deduced from the property name
// There is currently no way to for it to a different value
const attrName = lwcAttrName(propName);
// Find the default value for this property
const initializer = "initializer" in node ? node.initializer : undefined;
const resolvedDefaultValue = initializer != null ? resolveNodeValue(initializer, context) : undefined;
const def = resolvedDefaultValue != null ? resolvedDefaultValue.value : initializer === null || initializer === void 0 ? void 0 : initializer.getText();
// Find if the property/attribute is required
//const required = ("initializer" in node && isPropertyRequired(node, context.checker)) || undefined;
const required = undefined;
const jsDoc = getJsDoc(node, ts);
// Emit a property with "attrName"
return [
{
priority: "high",
kind: "property",
propName,
attrName,
type: lazy(() => {
const propType = checker.getTypeAtLocation(node);
return propType;
}),
node,
default: def,
required,
jsDoc,
visibility: getMemberVisibilityFromNode(node, ts),
reflect: undefined,
modifiers: getModifiersFromNode(node, ts)
}
];
}
return undefined;
}
const HTMLAttrs = {
accessKey: "accesskey",
bgColor: "bgcolor",
colSpan: "colspan",
contentEditable: "contenteditable",
crossOrigin: "crossorigin",
dateTime: "datetime",
htmlFor: "for",
formAction: "formaction",
isMap: "ismap",
maxLength: "maxlength",
minLength: "minlength",
noValidate: "novalidate",
readOnly: "readonly",
rowSpan: "rowspan",
tabIndex: "tabindex",
useMap: "usemap"
};
// LWC attribute names
// https://lwc.dev/guide/javascript#html-attribute-names
function lwcAttrName(propName) {
// Look for a global HTML name
const htmlAttr = HTMLAttrs[propName];
if (htmlAttr) {
return htmlAttr;
}
// Calculate the attribute name from the property
return camelToDashCase(propName).toLowerCase();
}
function discoverDefinitions$1(node, context) {
var _a;
const { ts } = context;
if (ts.isClassDeclaration(node)) {
const lwc = getLwcComponent(node, context);
if (lwc) {
return [
{
tagName: lwc.tagName,
tagNameNode: (_a = node.heritageClauses) === null || _a === void 0 ? void 0 : _a[0].types[0],
declarationNode: node
}
];
}
}
return undefined;
}
// In LWC, the public properties & methods must be tagged with @api
// everything else becomes protected and not accessible externally
function hasApiDecorator(node, context) {
if (!node) {
return false;
}
const { ts } = context;
for (const decorator of getDecorators(node, context)) {
const expression = decorator.expression;
// We find the first decorator calling specific identifier name (found in LWC_PROPERTY_DECORATOR_KINDS)
if (ts.isIdentifier(expression)) {
const identifier = expression;
const kind = identifier.text;
if (kind === "api") {
return true;
}
}
}
return false;
}
function findClassDeclaration(node, { ts }) {
while (node) {
if (ts.isClassDeclaration(node)) {
return node;
}
node = node.parent;
}
}
function isLWCComponent(component, context) {
var _a;
const node = findClassDeclaration((_a = component.declaration) === null || _a === void 0 ? void 0 : _a.node, context);
if (node) {
return !!getLwcComponent(node, context);
}
// You can't assume that everything is a LWC component - that will cause huge
// problems with the refinement rules below that switch default visibility to protected!!
return false;
}
const refineFeature$1 = {
member: (member, context) => {
if (isLWCComponent(member, context)) {
const visibility = hasApiDecorator(member.node, context) ? "public" : "protected";
return {
...member,
visibility
};
}
return member;
},
method: (method, context) => {
if (isLWCComponent(method, context)) {
const visibility = hasApiDecorator(method.node, context) ? "public" : "protected";
return {
...method,
visibility
};
}
return method;
}
};
/**
* Flavors for analyzing LWC related features: https://lwc.dev/
*/
class LwcFlavor {
constructor() {
this.discoverDefinitions = discoverDefinitions$1;
this.discoverFeatures = {
member: discoverMembers
};
this.refineFeature = refineFeature$1;
}
}
const VERSION = "2.0.0";
const DEFAULT_FLAVORS = [
new LitElementFlavor(),
new LwcFlavor(),
new CustomElementFlavor(),
new JsDocFlavor(),
new JSXFlavor()
];
const DEFAULT_FEATURE_COLLECTION_CACHE = new WeakMap();
const DEFAULT_COMPONENT_DECLARATION_CACHE = new WeakMap();
const ALL_COMPONENT_FEATURES = ["member", "method", "cssproperty", "csspart", "event", "slot"];
/**
* Creates an "analyzer visit context" based on some options
* @param options
*/
function makeContextFromConfig(options) {
var _a, _b, _c, _d, _e, _f, _g, _h;
if (options.program == null) {
throw new Error("A program is required when running 'analyzeSourceFile'");
}
// Assign defaults
const flavors = options.flavors || DEFAULT_FLAVORS;
const ts = options.ts || tsModule__namespace;
const checker = options.program.getTypeChecker();
// Create context
return {
checker,
program: options.program,
ts,
flavors,
cache: {
featureCollection: DEFAULT_FEATURE_COLLECTION_CACHE,
componentDeclarationCache: DEFAULT_COMPONENT_DECLARATION_CACHE,
general: new Map()
},
config: {
...options.config,
analyzeDefaultLib: (_b = (_a = options.config) === null || _a === void 0 ? void 0 : _a.analyzeDefaultLib) !== null && _b !== void 0 ? _b : false,
analyzeDependencies: (_d = (_c = options.config) === null || _c === void 0 ? void 0 : _c.analyzeDependencies) !== null && _d !== void 0 ? _d : false,
excludedDeclarationNames: (_f = (_e = options.config) === null || _e === void 0 ? void 0 : _e.excludedDeclarationNames) !== null && _f !== void 0 ? _f : [],
features: (_h = (_g = options.config) === null || _g === void 0 ? void 0 : _g.features) !== null && _h !== void 0 ? _h : ALL_COMPONENT_FEATURES
}
};
}
/**
* Prepares a map of component features and a callback map that adds to the component feature map.
*/
function prepareRefineEmitMap() {
const collection = {
members: [],
methods: [],
events: [],
slots: [],
cssProperties: [],
cssParts: []
};
const refineEmitMap = {
event: event => collection.events.push(event),
member: member => collection.members.push(member),
csspart: cssPart => collection.cssParts.push(cssPart),
cssproperty: cssProperty => collection.cssProperties.push(cssProperty),
method: method => collection.methods.push(method),
slot: slot => collection.slots.push(slot)
};
return {
collection,
refineEmitMap
};
}
/**
* Uses flavors to refine a feature
* Flavors can also remove a feature
* @param featureKind
* @param value
* @param context
* @param emitMap
*/
function refineFeature(featureKind, value, context, emitMap) {
/*if (Array.isArray(value)) {
value.forEach(v => refineComponentFeature(featureKind, v, context, emitMap));
return;
}*/
var _a;
let refinedValue = value;
// Add "declaration" to the feature if necessary
if ("getDeclaration" in context && refinedValue != null) {
const decl = context.getDeclaration();
if (Array.isArray(refinedValue)) {
for (const val of refinedValue) {
if (val.declaration == null) {
val.declaration = decl;
}
}
}
else if (refinedValue.declaration == null) {
refinedValue.declaration = decl;
}
}
for (const flavor of context.flavors) {
const refineFunc = (_a = flavor.refineFeature) === null || _a === void 0 ? void 0 : _a[featureKind];
if (refineFunc != null) {
if (refinedValue == null) {
return;
}
else if (Array.isArray(refinedValue)) {
const newValue = [];
for (const val of refinedValue) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const refined = refineFunc(val, context);
if (refined != null) {
newValue.push(...(Array.isArray(refined) ? refined : [refined]));
}
}
refinedValue = newValue.length === 0 ? undefined : newValue;
}
else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
refinedValue = refineFunc(refinedValue, context);
}
}
}
if (refinedValue != null) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Array.isArray(refinedValue) ? refinedValue : [refinedValue]).forEach(v => { var _a; return (_a = emitMap === null || emitMap === void 0 ? void 0 : emitMap[featureKind]) === null || _a === void 0 ? void 0 : _a.call(emitMap, v); });
}
}
/**
* Uses flavors to find features for a node
* @param node
* @param context
* @param emitMap
*/
function visitFeatures(node, context, emitMap) {
const visitMaps = arrayDefined(context.flavors.map(flavor => flavor.discoverFeatures));
visitFeaturesWithVisitMaps(node, context, visitMaps, emitMap);
}
/**
* Uses flavors to find features for a node, using a visit map
* @param node
* @param context
* @param visitMaps
* @param emitMap
*/
function visitFeaturesWithVisitMaps(node, context, visitMaps, emitMap) {
var _a;
for (const feature of context.config.features || []) {
// Visit all features: always "continue"
for (const functionMap of visitMaps) {
const func = functionMap === null || functionMap === void 0 ? void 0 : functionMap[feature];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const value = func === null || func === void 0 ? void 0 : func(node, context);
if (value != null) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(_a = emitMap[feature]) === null || _a === void 0 ? void 0 : _a.call(emitMap, value);
}
}
}
// Visit child nodes
node.forEachChild(child => {
visitFeaturesWithVisitMaps(child, context, visitMaps, emitMap);
});
}
/**
* Merges based on a name
* @param entries
* @param direction
* @param getName
* @param merge
*/
function mergeNamedEntries(entries, getName, merge) {
const merged = new Map();
for (const entry of entries) {
const name = getName(entry);
const existing = merged.get(name);
if (existing == null) {
merged.set(name, entry);
}
else if (merge != null) {
merged.set(name, merge(existing, entry));
}
}
return Array.from(merged.values());
}
/**
* Merges two jsdocs
* @param leftJsDoc
* @param rightJsDoc
*/
function mergeJsDoc(leftJsDoc, rightJsDoc) {
var _a;
if (leftJsDoc == null) {
return rightJsDoc;
}
else if (rightJsDoc == null) {
return leftJsDoc;
}
return {
...(leftJsDoc !== null && leftJsDoc !== void 0 ? leftJsDoc : rightJsDoc),
description: (_a = leftJsDoc.description) !== null && _a !== void 0 ? _a : rightJsDoc.description
};
}
/**
* Merges modifiers
* @param leftModifiers
* @param rightModifiers
*/
function mergeModifiers(leftModifiers, rightModifiers) {
const newSet = new Set();
if ((leftModifiers === null || leftModifiers === void 0 ? void 0 : leftModifiers.has("static")) && (rightModifiers === null || rightModifiers === void 0 ? void 0 : rightModifiers.has("static"))) {
newSet.add("static");
}
if ((leftModifiers === null || leftModifiers === void 0 ? void 0 : leftModifiers.has("readonly")) && (rightModifiers === null || rightModifiers === void 0 ? void 0 : rightModifiers.has("readonly"))) {
newSet.add("readonly");
}
if (newSet.size === 0) {
return undefined;
}
return newSet;
}
/**
* Merges entries using a "merge" callback
* @param entries
* @param isMergeable
* @param merge
*/
/*export function mergeEntries<T>(entries: T[], isMergeable: (entry: T, merged: T) => boolean, merge: (left: T, right: T) => T): T[] {
let mergedEntries: T[] = [];
for (const entry of entries) {
let mergeableEntry: T | undefined = undefined;
for (const mergedEntry of mergedEntries) {
if (isMergeable(entry, mergedEntry)) {
mergeableEntry = mergedEntry;
break;
}
}
let newEntry: T | undefined = undefined;
if (mergeableEntry == null) {
newEntry = entry;
} else {
mergedEntries = mergedEntries.filter(mergedEntry => mergedEntry !== entry && mergedEntry !== mergeableEntry);
newEntry = merge(mergeableEntry, entry);
}
mergedEntries.push(newEntry);
}
return mergedEntries;
}*/
/**
* Merges multiple slots
* @param slots
*/
function mergeSlots(slots) {
return mergeNamedEntries(slots, slot => slot.name || "");
}
/**
* Merges multiple css parts
* @param cssParts
*/
function mergeCssParts(cssParts) {
return mergeNamedEntries(cssParts, cssPart => cssPart.name);
}
/**
* Merges multiple css properties
* @param cssProps
*/
function mergeCssProperties(cssProps) {
return mergeNamedEntries(cssProps, cssProp => cssProp.name);
}
/**
* Merges multiple methods
* @param methods
*/
function mergeMethods(methods) {
return mergeNamedEntries(methods, method => method.name, (left, right) => ({
...left,
jsDoc: mergeJsDoc(left.jsDoc, right.jsDoc)
//modifiers: mergeModifiers(left.modifiers, right.modifiers)
}));
/*return mergeEntries(
methods,
(method, mergedMethod) => {
if (method.name === mergedMethod.name) {
return (method.modifiers?.has("static") || false) === (mergedMethod.modifiers?.has("static") || false);
}
return false;
},
(left, right) => ({
...left,
jsDoc: mergeJsDoc(left.jsDoc, right.jsDoc),
modifiers: mergeModifiers(left.modifiers, right.modifiers)
})
);*/
}
/**
* Merges multiple events
* @param events
*/
function mergeEvents(events) {
return mergeNamedEntries(events, event => event.name, (left, right) => ({
...left,
jsDoc: mergeJsDoc(left.jsDoc, right.jsDoc),
type: () => (left.type != null ? left.type() : right.type != null ? right.type() : { kind: "ANY" }),
typeHint: left.typeHint || right.typeHint
}));
}
const priorityValueMap = {
low: 0,
medium: 1,
high: 2
};
/**
* Merges multiple members based on priority
* @param members
* @param context
*/
function mergeMembers(members, context) {
// Start merging by sorting member results from high to low priority.
// If two priorities are the same: prioritize the first found element
// From node 11, equal elements keep their order after sort, but not in node 10
// Therefore we use "indexOf" to return correct order if two priorities are equal
members = [...members].sort((a, b) => {
const vA = priorityValueMap[a.priority || "low"];
const vB = priorityValueMap[b.priority || "low"];
if (vA === vB) {
const iA = members.indexOf(a);
const iB = members.indexOf(b);
return iA < iB ? -1 : 1;
}
return vA < vB ? 1 : -1;
});
// Keep track of merged props and merged attributes
// These are stored in maps for speed, because we are going to lookup a member per each memberResult
const mergeMap = {
props: new Map(),
attrs: new Map()
};
// Merge all members one by one adding them to the merge map
for (const member of members) {
// Find a member that is similar to this member
const mergeableMember = findMemberToMerge(member, mergeMap);
let newMember = undefined;
if (mergeableMember == null) {
// No mergeable member was found, so just add this to the map
newMember = member;
}
else {
// Remove "member" and "mergeableMember" from the merge map
// We are going to merge those and add the result to the merge map again
clearMergeMapWithMember(mergeableMember, mergeMap);
clearMergeMapWithMember(member, mergeMap);
newMember = mergeMemberIntoMember(mergeableMember, member, context.checker);
}
// Add to merge map
switch (newMember.kind) {
case "attribute":
mergeMap.attrs.set(newMember.attrName, newMember);
break;
case "property":
mergeMap.props.set(newMember.propName, newMember);
break;
}
}
// Return merged results with only "high" priorities
return [...mergeMap.props.values(), ...mergeMap.attrs.values()].map(member => ({ ...member, priority: "high" }));
}
/**
* Removes a member from the merge map
* @param member
* @param mergeMap
*/
function clearMergeMapWithMember(member, mergeMap) {
switch (member.kind) {
case "attribute":
mergeMap.attrs.delete(member.attrName);
break;
case "property":
mergeMap.props.delete(member.propName);
if (member.attrName != null) {
mergeMap.attrs.delete(member.attrName);
}
break;
}
}
/**
* Finds a mergeable member
* @param similar
* @param mergeMap
*/
function findMemberToMerge(similar, mergeMap) {
const attrName = similar.attrName; //?.toLowerCase(); // (similar.kind === "attribute" && similar.attrName.toLowerCase()) || undefined;
const propName = similar.propName; /*?.toLowerCase()*/ //(similar.kind === "property" && similar.propName.toLowerCase()) || undefined;
// Return a member that matches either propName (prioritized) or attrName
if (propName != null) {
const mergeable = mergeMap.props.get(propName) || mergeMap.attrs.get(propName);
if (mergeable != null) {
return mergeable;
}
}
if (attrName != null) {
const mergeableAttr = mergeMap.attrs.get(attrName);
if (mergeableAttr != null) {
return mergeableAttr;
}
// Try to find a prop with the attr name.
// Don't return the prop if it already has an attribute that is not equals to the attr name
const mergeableProp = mergeMap.props.get(attrName);
if (mergeableProp != null && mergeableProp.attrName == null) {
return mergeableProp;
}
for (const mergedAttr of mergeMap.props.values()) {
if (mergedAttr.attrName === attrName) {
return mergedAttr;
}
}
}
}
/**
* Merges two members of the same kind into each other.
* This operation prioritizes leftMember
* @param leftMember
* @param rightMember
* @param checker
*/
function mergeMemberIntoMember(leftMember, rightMember, checker) {
var _a, _b, _c, _d, _e, _f, _g;
// Always prioritize merging attribute into property if possible
if (leftMember.kind === "attribute" && rightMember.kind === "property") {
return mergeMemberIntoMember(rightMember, leftMember);
}
return {
...leftMember,
attrName: (_a = leftMember.attrName) !== null && _a !== void 0 ? _a : rightMember.attrName,
type: (() => {
var _a, _b;
// Always prioritize a "property" over an "attribute" when merging types
if (leftMember.kind === rightMember.kind || leftMember.kind === "property") {
return (_a = leftMember.type) !== null && _a !== void 0 ? _a : rightMember.type;
}
else if (rightMember.kind === "property") {
return (_b = rightMember.type) !== null && _b !== void 0 ? _b : leftMember.type;
}
})(),
typeHint: (_b = leftMember.typeHint) !== null && _b !== void 0 ? _b : rightMember.typeHint,
jsDoc: mergeJsDoc(leftMember.jsDoc, rightMember.jsDoc),
modifiers: mergeModifiers(leftMember.modifiers, rightMember.modifiers),
meta: (_c = leftMember.meta) !== null && _c !== void 0 ? _c : rightMember.meta,
default: leftMember.default === undefined ? rightMember.default : leftMember.default,
required: (_d = leftMember.required) !== null && _d !== void 0 ? _d : rightMember.required,
visibility: (_e = leftMember.visibility) !== null && _e !== void 0 ? _e : rightMember.visibility,
deprecated: (_f = leftMember.deprecated) !== null && _f !== void 0 ? _f : rightMember.deprecated,
declaration: (_g = rightMember.declaration) !== null && _g !== void 0 ? _g : leftMember.declaration
};
}
/**
* Merges all features in collections of features
* @param collection
* @param context
*/
function mergeFeatures(collection, context) {
if (Array.isArray(collection)) {
if (collection.length === 1) {
return collection[0];
}
collection = {
cssParts: arrayFlat(collection.map(c => c.cssParts)),
cssProperties: arrayFlat(collection.map(c => c.cssProperties)),
events: arrayFlat(collection.map(c => c.events)),
members: arrayFlat(collection.map(c => c.members)),
methods: arrayFlat(collection.map(c => c.methods)),
slots: arrayFlat(collection.map(c => c.slots))
};
return mergeFeatures(collection, context);
}
return {
cssParts: mergeCssParts(collection.cssParts),
cssProperties: mergeCssProperties(collection.cssProperties),
events: mergeEvents(collection.events),
members: mergeMembers(collection.members, context),
methods: mergeMethods(collection.methods),
slots: mergeSlots(collection.slots)
};
}
/**
* Discovers features for a given node using flavors
* @param node
* @param context
*/
function discoverFeatures(node, context) {
// Return the result if we already found this node
if (context.cache.featureCollection.has(node)) {
return context.cache.featureCollection.get(node);
}
const { collection, refineEmitMap } = prepareRefineEmitMap();
// Discovers features for "node" using flavors
visitFeatures(node, context, {
event: event => refineFeature("event", event, context, refineEmitMap),
member: memberResult => refineFeature("member", memberResult, context, refineEmitMap),
csspart: cssPart => refineFeature("csspart", cssPart, context, refineEmitMap),
cssproperty: cssProperty => refineFeature("cssproperty", cssProperty, context, refineEmitMap),
method: method => refineFeature("method", method, context, refineEmitMap),
slot: slot => refineFeature("slot", slot, context, refineEmitMap)
});
// Merge features that were found
const mergedCollection = mergeFeatures(collection, context);
// Cache the features for this node
context.cache.featureCollection.set(node, mergedCollection);
return mergedCollection;
}
/**
* Uses flavors to find inheritance for a node
* @param node
* @param context
* @param emit
* @param visitSet
*/
function visitInheritance(node, context, emit, visitSet) {
var _a;
for (const flavor of context.flavors) {
const result = (_a = flavor.discoverInheritance) === null || _a === void 0 ? void 0 : _a.call(flavor, node, context);
if (result != null) {
emit(result);
}
}
}
/**
* Uses flavors in order to discover inheritance from one of more nodes.
* @param startNode
* @param visitedNodes
* @param context
*/
function discoverInheritance(startNode, visitedNodes, context) {
const nodes = Array.isArray(startNode) ? startNode : [startNode];
let declarationKind = undefined;
const heritageClauses = [];
const declarationNodes = new Set();
for (const node of nodes) {
visitedNodes.add(node);
// Visit inheritance using flavors
visitInheritance(node, context, result => {
// Combine results into one single result
declarationKind = declarationKind || result.declarationKind;
if (result.declarationNodes != null) {
for (const node of result.declarationNodes) {
declarationNodes.add(node);
}
}
if (result.heritageClauses != null) {
heritageClauses.push(...result.heritageClauses);
}
});
}
return {
declarationNodes: Array.from(declarationNodes),
heritageClauses,
declarationKind: declarationKind || "class"
};
}
/**
* Uses flavors to determine if a node should be excluded from the output
* @param node
* @param context
*/
function excludeNode(node, context) {
var _a;
for (const flavor of context.flavors) {
const exclude = (_a = flavor.excludeNode) === null || _a === void 0 ? void 0 : _a.call(flavor, node, context);
if (exclude) {
return true;
}
}
return false;
}
/**
* Uses flavors to refine a declaration
* @param declaration
* @param context
*/
function refineDeclaration(declaration, context) {
var _a, _b;
for (const flavor of context.flavors) {
declaration = (_b = (_a = flavor.refineDeclaration) === null || _a === void 0 ? void 0 : _a.call(flavor, declaration, context)) !== null && _b !== void 0 ? _b : declaration;
}
return declaration;
}
/**
* Discovers features on component declaration nodes
* @param initialDeclarationNodes
* @param baseContext
* @param options
*/
function analyzeComponentDeclaration(initialDeclarationNodes, baseContext, options = {}) {
const mainDeclarationNode = initialDeclarationNodes[0];
if (mainDeclarationNode == null) {
return undefined;
//throw new Error("Couldn't find main declaration node");
}
// Check if there exists a cached declaration for this node.
// If a cached declaration was found, test if it should be invalidated (by looking at inherited declarations)
const cachedDeclaration = baseContext.cache.componentDeclarationCache.get(mainDeclarationNode);
if (cachedDeclaration != null && !shouldInvalidateCachedDeclaration(cachedDeclaration, baseContext)) {
return cachedDeclaration;
}
options.visitedNodes = options.visitedNodes || new Set();
// Discover inheritance
const { declarationKind, declarationNodes, heritageClauses } = discoverInheritance(initialDeclarationNodes, options.visitedNodes, baseContext);
// Expand all heritage clauses with the component declaration
for (const heritageClause of heritageClauses) {
// Only resolve declarations we haven't yet seen and shouldn't be excluded
const declarations = resolveDeclarations(heritageClause.identifier, baseContext).filter(n => { var _a; return !((_a = options.visitedNodes) === null || _a === void 0 ? void 0 : _a.has(n)) && !shouldExcludeNode(n, baseContext); });
if (declarations.length > 0) {
heritageClause.declaration = analyzeComponentDeclaration(declarations, baseContext, options);
}
}
// Get symbol of main declaration node
const symbol = getSymbol(mainDeclarationNode, baseContext);
const sourceFile = mainDeclarationNode.getSourceFile();
const baseDeclaration = {
sourceFile,
node: mainDeclarationNode,
declarationNodes: new Set(declarationNodes),
symbol,
heritageClauses,
kind: declarationKind || "class",
events: [],
cssParts: [],
cssProperties: [],
members: [],
methods: [],
slots: [],
jsDoc: getJsDoc(mainDeclarationNode, baseContext.ts)
};
// Add the "get declaration" hook to the context
const context = {
...baseContext,
declarationNode: mainDeclarationNode,
sourceFile: mainDeclarationNode.getSourceFile(),
getDeclaration: () => baseDeclaration
};
// Find features on all declaration nodes
const featureCollections = [];
for (const node of declarationNodes) {
if (shouldExcludeNode(node, context)) {
continue;
}
// Discover component features using flavors
featureCollections.push(discoverFeatures(node, {
...context,
declarationNode: node,
sourceFile: node.getSourceFile()
}));
}
// Add all inherited features to the feature collections array
for (const heritageClause of heritageClauses) {
if (heritageClause.declaration != null) {
featureCollections.push({
...heritageClause.declaration,
members: heritageClause.declaration.members
});
}
}
// If all nodes were excluded, return empty declaration
if (featureCollections.length === 0) {
return baseDeclaration;
}
// Merge all features into one single collection prioritizing features found in first
const mergedFeatureCollection = mergeFeatures(featureCollections, context);
// Refine the declaration and return the result
const refinedDeclaration = refineDeclaration({
...baseDeclaration,
cssParts: mergedFeatureCollection.cssParts,
cssProperties: mergedFeatureCollection.cssProperties,
events: mergedFeatureCollection.events,
methods: mergedFeatureCollection.methods,
members: mergedFeatureCollection.members,
slots: mergedFeatureCollection.slots
}, context);
Object.assign(baseDeclaration, refinedDeclaration);
// Update the cache
baseContext.cache.componentDeclarationCache.set(mainDeclarationNode, baseDeclaration);
return baseDeclaration;
}
/**
* Returns if a node should be excluded from the analyzing
* @param node
* @param context
*/
function shouldExcludeNode(node, context) {
var _a;
// Uses flavors to determine if the node should be excluded
if (excludeNode(node, context)) {
return true;
}
// It's possible to exclude declaration names
const name = getNodeName(node, context);
if (name != null && ((_a = context.config.excludedDeclarationNames) === null || _a === void 0 ? void 0 : _a.includes(name))) {
return true;
}
return false;
}
/**
* Returns if the declaration should be invalidated by testing
* if any of the inherited declarations in the tree has been invalidated
* @param componentDeclaration
* @param context
*/
function shouldInvalidateCachedDeclaration(componentDeclaration, context) {
var _a;
for (const heritageClause of componentDeclaration.heritageClauses) {
if (heritageClause.declaration != null) {
// This declaration shouldn't be invalidated if the existing "node.getSourceFile()" is equal to the "program.getSourceFile(...)" with the same file name,
const node = heritageClause.declaration.node;
const oldSourceFile = node.getSourceFile();
const newSourceFile = context.program.getSourceFile(oldSourceFile.fileName);
const foundInCache = (_a = (newSourceFile != null && newSourceFile === oldSourceFile)) !== null && _a !== void 0 ? _a : false;
// Return "true" that the declaration should invalidate if it wasn't found in the cache
if (!foundInCache) {
return true;
}
// Test the inherited declarations recursively
if (shouldInvalidateCachedDeclaration(heritageClause.declaration, context)) {
return true;
}
}
}
return false;
}
/**
* Visits the source file and finds all component definitions using flavors
* @param sourceFile
* @param context
*/
function discoverDeclarations(sourceFile, context) {
const declarations = [];
const symbol = context.checker.getSymbolAtLocation(sourceFile);
if (symbol != null) {
// Get all exports in the source file
const exports = context.checker.getExportsOfModule(symbol);
// Find all class declarations in the source file
for (const symbol of exports) {
const node = symbol.valueDeclaration;
if (node != null) {
if (context.ts.isClassDeclaration(node) /* || context.ts.isInterfaceDeclaration(node)*/) {
const nodes = resolveSymbolDeclarations(symbol);
const decl = analyzeComponentDeclaration(nodes, context);
if (decl != null) {
declarations.push(decl);
}
}
}
}
}
return declarations;
}
/**
* Executes functions in a function map until some function returns a non-undefined value.
* @param functionMaps
* @param keys
* @param arg
* @param context
*/
function executeFunctionsUntilMatch(functionMaps, keys, arg, context) {
keys = Array.isArray(keys) ? keys : [keys];
for (const key of keys) {
// Loop through each function
for (const functionMap of functionMaps) {
const func = functionMap[key];
if (func == null)
continue;
// Save a "continue" flag if necessary
let shouldContinue = false;
const result = func(arg, {
...context,
emitContinue() {
shouldContinue = true;
}
});
// Return a result if not undefined
if (result != null) {
return { value: result, shouldContinue };
}
}
}
return undefined;
}
/**
* Uses flavors to visit definitions
* @param node
* @param context
* @param emit
*/
function visitDefinitions(node, context, emit) {
const result = executeFunctionsUntilMatch(context.flavors, "discoverDefinitions", node, context);
if (result != null) {
emit(result.value);
if (!result.shouldContinue)
return;
}
// Visit child nodes
node.forEachChild(child => {
visitDefinitions(child, context, emit);
});
}
/**
* Visits the source file and finds all component definitions using flavors
* @param sourceFile
* @param context
* @param analyzeDeclaration
*/
function discoverDefinitions(sourceFile, context, analyzeDeclaration) {
// Find all definitions in the file using flavors
const definitionResults = analyzeAndDedupeDefinitions(sourceFile, context);
return Array.from(definitionResults.entries()).map(([definition, declarationSet]) => {
let declaration;
let didEvaluateDeclaration = false;
return {
...definition,
get declaration() {
if (!didEvaluateDeclaration) {
declaration = analyzeDeclaration(definition, Array.from(declarationSet));
didEvaluateDeclaration = true;
}
return declaration;
}
};
});
}
/**
* Finds all component definitions in a file and combine multiple declarations with same tag name.
* @param sourceFile
* @param context
*/
function analyzeAndDedupeDefinitions(sourceFile, context) {
if (sourceFile == null)
return new Map();
// Keep a map of "tag name" ==> "definition"
const tagNameDefinitionMap = new Map();
// Keep a map of "definition" ==> "declaration nodes"
const definitionToDeclarationMap = new Map();
// Discover definitions using flavors
visitDefinitions(sourceFile, context, results => {
// Definitions are unique by tag name and are merged when pointing to multiple declaration nodes.
// This is because multiple definitions can exist side by side for the same tag name (think global TagName type definition and customElements.define)
for (const result of results) {
// Find existing definition with the result name
let definition = tagNameDefinitionMap.get(result.tagName);
if (definition == null) {
// No existing definition was found, - create one!
definition = {
sourceFile,
tagName: result.tagName,
tagNameNodes: new Set(),
identifierNodes: new Set()
};
tagNameDefinitionMap.set(result.tagName, definition);
}
// Add the discovered identifier node to the definition
if (result.identifierNode != null) {
definition.identifierNodes.add(result.identifierNode);
}
// Add the discovered tag name node to the definition
if (result.tagNameNode) {
definition.tagNameNodes.add(result.tagNameNode);
}
// Add the discovered declaration node to the map from "definition" ==> "declaration nodes"
let declarationNodeSet = definitionToDeclarationMap.get(definition);
if (declarationNodeSet == null) {
declarationNodeSet = new Set();
definitionToDeclarationMap.set(definition, declarationNodeSet);
}
// Grab the symbol from the identifier node and get the declarations
// If the is no symbol on the result, use "result.declarationNode" instead
const symbol = result.identifierNode != null ? getSymbol(result.identifierNode, context) : undefined;
const declarations = symbol != null ? resolveSymbolDeclarations(symbol) : result.declarationNode != null ? [result.declarationNode] : [];
for (const decl of declarations) {
declarationNodeSet.add(decl);
}
}
});
// Remove duplicates where "tagName" is equals to "" if the declaration node is not used in any other definition.
const results = Array.from(definitionToDeclarationMap.entries());
for (const [definition, declarations] of results) {
if (definition.tagName === "") {
for (const [checkDefinition, checkDeclarations] of results) {
// Find duplicated based on overlapping declarations
if (definition !== checkDefinition && Array.from(declarations).find(decl => checkDeclarations.has(decl) != null)) {
definitionToDeclarationMap.delete(definition);
break;
}
}
}
}
return definitionToDeclarationMap;
}
/**
* Uses flavors to find global features
* @param node
* @param context
* @param emitMap
*/
function visitGlobalFeatures(node, context, emitMap) {
const visitMaps = arrayDefined(context.flavors.map(flavor => flavor.discoverGlobalFeatures));
visitFeaturesWithVisitMaps(node, context, visitMaps, emitMap);
}
/**
* Discover all global features using flavors
* @param node
* @param context
*/
function discoverGlobalFeatures(node, context) {
const { collection, refineEmitMap } = prepareRefineEmitMap();
// Discovers global features using flavors
visitGlobalFeatures(node, context, {
event: event => refineFeature("event", event, context, refineEmitMap),
member: memberResult => refineFeature("member", memberResult, context, refineEmitMap),
csspart: cssPart => refineFeature("csspart", cssPart, context, refineEmitMap),
cssproperty: cssProperty => refineFeature("cssproperty", cssProperty, context, refineEmitMap),
method: method => refineFeature("method", method, context, refineEmitMap),
slot: slot => refineFeature("slot", slot, context, refineEmitMap)
});
// Merge features in the collection
return mergeFeatures(collection, context);
}
/**
* Analyzes all components in a source file.
* @param sourceFile
* @param options
*/
function analyzeSourceFile(sourceFile, options) {
// Create a new context
const context = makeContextFromConfig(options);
// Analyze all components
const componentDefinitions = discoverDefinitions(sourceFile, context, (definition, declarationNodes) =>
// The component declaration is analyzed lazily
analyzeComponentDeclaration(declarationNodes, context));
// Analyze global features
let globalFeatures = undefined;
if (context.config.analyzeGlobalFeatures) {
globalFeatures = discoverGlobalFeatures(sourceFile, context);
}
// Analyze exported declarations
let declarations = undefined;
if (context.config.analyzeAllDeclarations) {
declarations = discoverDeclarations(sourceFile, context);
}
return {
sourceFile,
componentDefinitions,
globalFeatures,
declarations
};
}
/**
* Returns the superclass heritage clause
* @param declaration
*/
function getSuperclassHeritageClause(declaration) {
return (declaration.heritageClauses.find(clause => { var _a; return clause.kind === "extends" && ((_a = clause.declaration) === null || _a === void 0 ? void 0 : _a.kind) === "class"; }) ||
declaration.heritageClauses.find(clause => clause.kind === "extends" && clause.declaration == null));
}
/**
* Returns mixin heritage clauses for the declaration
* @param declaration
*/
function getMixinHeritageClauses(declaration) {
return declaration.heritageClauses.filter(clause => { var _a; return clause.kind === "mixin" || ((_a = clause.declaration) === null || _a === void 0 ? void 0 : _a.kind) === "mixin"; });
}
/**
* Returns all extends heritage clauses for the declaration
* @param declaration
*/
function getExtendsHeritageClauses(declaration) {
return declaration.heritageClauses.filter(clause => clause.kind === "extends");
}
/**
* Returns mixin heritage clauses for the declaration and all inherited declarations
* @param declaration
*/
function getMixinHeritageClausesInChain(declaration) {
const clauses = [];
visitAllHeritageClauses(declaration, clause => {
if (clause.kind === "mixin") {
clauses.push(clause);
}
});
return clauses;
}
/**
* Returns extends heritage clauses for the declaration and all inherited declarations
* @param declaration
*/
function getExtendsHeritageClausesInChain(declaration) {
const clauses = [];
visitAllHeritageClauses(declaration, clause => {
if (clause.kind === "extends") {
clauses.push(clause);
}
});
return clauses;
}
/**
* A helper function that makes it possible to visit all heritage clauses in the inheritance chain.
* @param declaration
* @param emit
*/
function visitAllHeritageClauses(declaration, emit) {
for (const clause of declaration.heritageClauses) {
emit(clause);
if (clause.declaration != null) {
visitAllHeritageClauses(clause.declaration, emit);
}
}
}
/* eslint-disable @typescript-eslint/no-explicit-any */
function isTypescriptNode(value) {
return value instanceof Object && "kind" in value && "flags" in value;
}
function isTypescriptSourceFile(value) {
return value instanceof Object && "kind" in value && "fileName" in value;
}
function isTypescriptType(value) {
return value instanceof Object && "flags" in value && "checker" in value;
}
/**
* Returns a representation of the input that can be JSON stringified
*/
function stripTypescriptValues(input, checker, seenValues = new Set()) {
var _a, _b, _c, _d;
if (seenValues.has(input)) {
return "[Circular]";
}
seenValues = new Set(seenValues);
seenValues.add(input);
if (input == null) {
return input;
}
else if (typeof input === "function") {
return stripTypescriptValues(input(), checker, seenValues);
}
else if (isTypescriptSourceFile(input)) {
return `{SOURCEFILE:${(_a = input.fileName.match(".*/(.+)")) === null || _a === void 0 ? void 0 : _a[1]}}`;
}
else if (isTypescriptNode(input)) {
const title = "escapedText" in input ? input.escapedText : undefined;
return `{NODE:${(_d = (_c = (_b = input.getSourceFile) === null || _b === void 0 ? void 0 : _b.call(input)) === null || _c === void 0 ? void 0 : _c.fileName.match(".*/(.+)")) === null || _d === void 0 ? void 0 : _d[1]}${title != null ? `:${title}` : ""}:${input.pos}}`;
}
else if (isTypescriptType(input)) {
if (checker == null) {
return "{TYPE}";
}
return `{TYPE:${checker.typeToString(input)}}`;
}
else if (tsSimpleType.isSimpleType(input)) {
return `{SIMPLE_TYPE:${tsSimpleType.typeToString(input)}}`;
}
else if (Array.isArray(input)) {
return input.map(i => stripTypescriptValues(i, checker, seenValues));
}
else if (input instanceof Set) {
return stripTypescriptValues(Array.from(input), checker, seenValues);
}
else if (input instanceof Map) {
return stripTypescriptValues(Array.from(input), checker, seenValues);
}
else if (input instanceof Object) {
const obj = {};
for (const [key, value] of Object.entries(input)) {
const strippedValue = stripTypescriptValues(value, checker, seenValues);
if (strippedValue !== undefined && (!Array.isArray(strippedValue) || strippedValue.length > 0)) {
obj[key] = strippedValue;
}
}
return obj;
}
return input;
}
/**
* Transforms results to json.
* @param results
* @param program
* @param config
*/
const debugJsonTransformer = (results, program, config) => {
const definitions = arrayFlat(results.map(res => res.componentDefinitions));
return JSON.stringify(stripTypescriptValues(definitions, program.getTypeChecker()), null, 2);
};
/**
* Returns a "type hint" from a type
* The type hint is an easy to read representation of the type and is not made for being parsed.
* @param type
* @param checker
* @param config
*/
function getTypeHintFromType(type, checker, config) {
if (type == null)
return undefined;
if (typeof type === "string")
return type;
let typeHint;
if (config.inlineTypes) {
// Inline aliased types
if (tsSimpleType.isSimpleType(type)) {
// Expand a possible alias
if (isUnionTypeAlias(type)) {
type = type.target;
}
typeHint = tsSimpleType.typeToString(type);
}
else {
// Transform using Typescript natively, to avoid transforming all types to simple types (overhead).
// The "InTypeAlias" flag expands the type.
typeHint = checker.typeToString(type, undefined, tsModule.TypeFormatFlags.InTypeAlias);
}
}
else {
// Transform types to string
typeHint = tsSimpleType.typeToString(type, checker);
}
// Replace "anys" and "{}" with more human friendly representations
if (typeHint === "any")
return undefined;
if (typeHint === "any[]")
return "array";
if (typeHint === "{}")
return "object";
// "CustomEvent<unknown>" and "Event" of no interest
if (typeHint === "CustomEvent<unknown>" || typeHint === "Event")
return undefined;
return typeHint;
}
/**
* Checks if a type is a type alias simple type
* @param simpleType
*/
function isUnionTypeAlias(simpleType) {
return simpleType.kind === "ALIAS" && simpleType.target.kind === "UNION";
}
const VISIBILITY_NUMBER_MAP = {
private: 1,
protected: 2,
public: 3
};
/**
* Removes all items from an array with visibilities that are less visible than "visibility".
* @param visibility
* @param array
*/
function filterVisibility(visibility = "public", array) {
const target = VISIBILITY_NUMBER_MAP[visibility];
return array.filter(item => VISIBILITY_NUMBER_MAP[item.visibility || "public"] >= target);
}
/**
* Returns the first element in the set
* @param set
*/
function getFirst(set) {
return set.values().next().value;
}
/**
* Transforms results to json.
* @param results
* @param program
* @param config
*/
const jsonTransformer = (results, program, config) => {
const checker = program.getTypeChecker();
// Get all definitions
const definitions = arrayFlat(results.map(res => res.componentDefinitions));
// Transform all definitions into "tags"
const tags = definitions.map(d => definitionToHtmlDataTag$1(d, checker, config));
const htmlData = {
version: "experimental",
tags
};
return JSON.stringify(htmlData, null, 2);
};
function definitionToHtmlDataTag$1(definition, checker, config) {
// Grab path to the definition file if possible
const node = getFirst(definition.tagNameNodes) || getFirst(definition.identifierNodes);
const fileName = node === null || node === void 0 ? void 0 : node.getSourceFile().fileName;
const path$1 = fileName != null && config.cwd != null ? `./${path.relative(config.cwd, fileName)}` : undefined;
const declaration = definition.declaration;
if (declaration == null) {
return {
name: definition.tagName,
path: path$1
};
}
const attributes = arrayDefined(filterVisibility(config.visibility, declaration.members).map(d => componentMemberToHtmlDataAttribute(d, checker, config)));
const properties = arrayDefined(filterVisibility(config.visibility, declaration.members).map(d => componentMemberToHtmlDataProperty(d, checker, config)));
const events = arrayDefined(filterVisibility(config.visibility, declaration.events).map(e => componentEventToHtmlDataEvent(e)));
const slots = arrayDefined(declaration.slots.map(e => componentSlotToHtmlDataSlot(e)));
const cssProperties = arrayDefined(declaration.cssProperties.map(p => componentCssPropToHtmlCssProp(p)));
const cssParts = arrayDefined(declaration.cssParts.map(p => componentCssPartToHtmlCssPart(p)));
return {
name: definition.tagName,
path: path$1,
description: getDescriptionFromJsDoc(declaration.jsDoc),
attributes: attributes.length === 0 ? undefined : attributes,
properties: properties.length === 0 ? undefined : properties,
events: events.length === 0 ? undefined : events,
slots: slots.length === 0 ? undefined : slots,
cssProperties: cssProperties.length === 0 ? undefined : cssProperties,
cssParts: cssParts.length === 0 ? undefined : cssParts,
deprecated: declaration.deprecated === true || typeof declaration.deprecated === "string" || undefined,
deprecatedMessage: typeof declaration.deprecated === "string" ? declaration.deprecated : undefined
};
}
function componentCssPropToHtmlCssProp(prop, checker) {
return {
name: prop.name || "",
description: getDescriptionFromJsDoc(prop.jsDoc),
type: prop.typeHint,
default: prop.default != null ? JSON.stringify(prop.default) : undefined
};
}
function componentCssPartToHtmlCssPart(part, checker) {
return {
name: part.name || "",
description: getDescriptionFromJsDoc(part.jsDoc)
};
}
function componentSlotToHtmlDataSlot(slot, checker) {
return {
name: slot.name || "",
description: getDescriptionFromJsDoc(slot.jsDoc)
};
}
function componentEventToHtmlDataEvent(event, checker) {
return {
name: event.name,
description: getDescriptionFromJsDoc(event.jsDoc),
deprecated: event.deprecated === true || undefined,
deprecatedMessage: typeof event.deprecated === "string" ? event.deprecated : undefined
};
}
function componentMemberToHtmlDataAttribute(member, checker, config) {
var _a, _b;
if (member.attrName == null) {
return undefined;
}
return {
name: member.attrName,
description: getDescriptionFromJsDoc(member.jsDoc),
type: getTypeHintFromType((_a = member.typeHint) !== null && _a !== void 0 ? _a : (_b = member.type) === null || _b === void 0 ? void 0 : _b.call(member), checker, config),
default: member.default != null ? JSON.stringify(member.default) : undefined,
deprecated: member.deprecated === true || undefined,
deprecatedMessage: typeof member.deprecated === "string" ? member.deprecated : undefined
};
}
function componentMemberToHtmlDataProperty(member, checker, config) {
var _a, _b;
if (member.propName == null) {
return undefined;
}
return {
name: member.propName,
attribute: member.attrName,
description: getDescriptionFromJsDoc(member.jsDoc),
type: getTypeHintFromType((_a = member.typeHint) !== null && _a !== void 0 ? _a : (_b = member.type) === null || _b === void 0 ? void 0 : _b.call(member), checker, config),
default: member.default != null ? JSON.stringify(member.default) : undefined,
deprecated: member.deprecated === true || undefined,
deprecatedMessage: typeof member.deprecated === "string" ? member.deprecated : undefined
};
}
function getDescriptionFromJsDoc(jsDoc) {
return jsDoc === null || jsDoc === void 0 ? void 0 : jsDoc.description;
}
/**
* This method returns a "type hint" that represents the method signature
* The resulting type takes jsdoc into account.
* I couldn't find a way for Typescript to return the signature string taking jsdoc into account
* so therefore I had to do some regex-magic in this method.
*/
function getTypeHintFromMethod(method, checker) {
var _a, _b, _c, _d, _e, _f, _g;
let signature = getTypeHintFromType((_a = method.type) === null || _a === void 0 ? void 0 : _a.call(method), checker, {}) || "";
// Replace "=>" with ":" and the return type with the returnTypeHint if present
signature = signature.replace(/\)\s*=>\s?(.*)$/, `): ${(_e = (_d = (_c = (_b = method.jsDoc) === null || _b === void 0 ? void 0 : _b.tags) === null || _c === void 0 ? void 0 : _c.find(tag => ["returns", "return"].includes(tag.tag))) === null || _d === void 0 ? void 0 : _d.parsed().type) !== null && _e !== void 0 ? _e : "$1"}`);
// Replace all "any" types with corresponding type hints
for (const parameterJsDocTag of ((_g = (_f = method.jsDoc) === null || _f === void 0 ? void 0 : _f.tags) === null || _g === void 0 ? void 0 : _g.filter(tag => tag.tag === "param")) || []) {
const parsed = parameterJsDocTag.parsed();
if (parsed.type != null) {
signature = signature.replace(new RegExp(`${parsed.name}(.*?:\\s*)any\\[?]?`), `${parsed.name}$1${parsed.type}`);
}
}
// Replace "{}" with more pleasant string
signature = signature.replace("{}", "object");
return signature;
}
/**
* Transforms results to json using the schema found in the PR at https://github.com/webcomponents/custom-elements-json/pull/9
* @param results
* @param program
* @param config
*/
const json2Transformer = (results, program, config) => {
const context = {
config,
checker: program.getTypeChecker(),
program,
ts: tsModule__namespace
};
// Flatten analyzer results expanding inherited declarations into the declaration array.
const flattenedAnalyzerResults = flattenAnalyzerResults(results);
// Transform all analyzer results into modules
const modules = flattenedAnalyzerResults.map(result => analyzerResultToModuleDoc(result, context));
const htmlData = {
version: "experimental",
modules
};
return JSON.stringify(htmlData, null, 2);
};
/**
* Transforms an analyzer result into a module doc
* @param result
* @param context
*/
function analyzerResultToModuleDoc(result, context) {
// Get all export docs from the analyzer result
const exports = getExportsDocsFromAnalyzerResult(result, context);
return {
path: getRelativePath(result.sourceFile.fileName, context),
exports: exports.length === 0 ? undefined : exports
};
}
/**
* Returns ExportDocs in an analyzer result
* @param result
* @param context
*/
function getExportsDocsFromAnalyzerResult(result, context) {
// Return all class- and variable-docs
return [
...getDefinitionDocsFromAnalyzerResult(result, context),
...getClassDocsFromAnalyzerResult(result, context),
...getVariableDocsFromAnalyzerResult(result, context),
...getFunctionDocsFromAnalyzerResult()
];
}
/**
* Returns FunctionDocs in an analyzer result
* @param result
* @param context
*/
function getFunctionDocsFromAnalyzerResult(result, context) {
// TODO: support function exports
return [];
}
function getDefinitionDocsFromAnalyzerResult(result, context) {
return arrayDefined(result.componentDefinitions.map(definition => {
// It's not possible right now to model a tag name where the
// declaration couldn't be resolved because the "declaration" is required
if (definition.declaration == null) {
return undefined;
}
return {
kind: "definition",
name: definition.tagName,
declaration: getReferenceForNode(definition.declaration.node, context)
};
}));
}
/**
* Returns VariableDocs in an analyzer result
* @param result
* @param context
*/
function getVariableDocsFromAnalyzerResult(result, context) {
const varDocs = [];
// Get all export symbols in the source file
const symbol = context.checker.getSymbolAtLocation(result.sourceFile);
if (symbol == null) {
return [];
}
const exports = context.checker.getExportsOfModule(symbol);
// Convert all export variables to VariableDocs
for (const exp of exports) {
switch (exp.flags) {
case tsModule__namespace.SymbolFlags.BlockScopedVariable:
case tsModule__namespace.SymbolFlags.Variable: {
const node = exp.valueDeclaration;
if (node && tsModule__namespace.isVariableDeclaration(node)) {
// Get the nearest variable statement in order to read the jsdoc
const variableStatement = findParent(node, tsModule__namespace.isVariableStatement) || node;
const jsDoc = getJsDoc(variableStatement, tsModule__namespace);
varDocs.push({
kind: "variable",
name: node.name.getText(),
description: jsDoc === null || jsDoc === void 0 ? void 0 : jsDoc.description,
type: getTypeHintFromType(context.checker.getTypeAtLocation(node), context.checker, context.config),
summary: getSummaryFromJsDoc(jsDoc)
});
}
break;
}
}
}
return varDocs;
}
/**
* Returns ClassDocs in an analyzer result
* @param result
* @param context
*/
function getClassDocsFromAnalyzerResult(result, context) {
const classDocs = [];
// Convert all declarations to class docs
for (const decl of result.declarations || []) {
const doc = getExportsDocFromDeclaration(decl, result, context);
if (doc != null) {
classDocs.push(doc);
}
}
return classDocs;
}
/**
* Converts a component declaration to ClassDoc, CustomElementDoc or MixinDoc
* @param declaration
* @param result
* @param context
*/
function getExportsDocFromDeclaration(declaration, result, context) {
var _a, _b;
// Only include "mixin" and "class" in the output. Interfaces are not outputted..
if (declaration.kind === "interface") {
return undefined;
}
// Get the superclass of this declaration
const superclassHeritage = getSuperclassHeritageClause(declaration);
const superclassRef = superclassHeritage == null ? undefined : getReferenceFromHeritageClause(superclassHeritage, context);
// Get all mixins
const mixinHeritage = getMixinHeritageClauses(declaration);
const mixinRefs = arrayDefined(mixinHeritage.map(h => getReferenceFromHeritageClause(h, context)));
const members = getClassMemberDocsFromDeclaration(declaration, context);
const classDoc = {
kind: "class",
superclass: superclassRef,
mixins: mixinRefs.length > 0 ? mixinRefs : undefined,
description: (_a = declaration.jsDoc) === null || _a === void 0 ? void 0 : _a.description,
name: ((_b = declaration.symbol) === null || _b === void 0 ? void 0 : _b.name) || getNodeName(declaration.node, { ts: tsModule__namespace }) || "",
members: members.length > 0 ? members : undefined,
summary: getSummaryFromJsDoc(declaration.jsDoc)
};
// Find the first corresponding custom element definition for this declaration
const definition = result.componentDefinitions.find(def => { var _a; return ((_a = def.declaration) === null || _a === void 0 ? void 0 : _a.node) === declaration.node; });
if (definition != null) {
const events = getEventDocsFromDeclaration(declaration, context);
const slots = getSlotDocsFromDeclaration(declaration, context);
const attributes = getAttributeDocsFromDeclaration(declaration, context);
const cssProperties = getCSSPropertyDocsFromDeclaration(declaration, context);
const cssParts = getCSSPartDocsFromDeclaration(declaration, context);
// Return a custom element doc if a definition was found
const customElementDoc = {
...classDoc,
tagName: definition.tagName,
events: events.length > 0 ? events : undefined,
slots: slots.length > 0 ? slots : undefined,
attributes: attributes.length > 0 ? attributes : undefined,
cssProperties: cssProperties.length > 0 ? cssProperties : undefined,
cssParts: cssParts.length > 0 ? cssParts : undefined
};
return customElementDoc;
}
return classDoc;
}
/**
* Returns event docs for a declaration
* @param declaration
* @param context
*/
function getEventDocsFromDeclaration(declaration, context) {
return filterVisibility(context.config.visibility, declaration.events).map(event => {
var _a, _b;
const type = ((_a = event.type) === null || _a === void 0 ? void 0 : _a.call(event)) || { kind: "ANY" };
const simpleType = tsSimpleType.isSimpleType(type) ? type : tsSimpleType.toSimpleType(type, context.checker);
const typeName = simpleType.kind === "GENERIC_ARGUMENTS" ? simpleType.target.name : simpleType.name;
const customEventDetailType = typeName === "CustomEvent" && simpleType.kind === "GENERIC_ARGUMENTS" ? simpleType.typeArguments[0] : undefined;
return {
description: (_b = event.jsDoc) === null || _b === void 0 ? void 0 : _b.description,
name: event.name,
inheritedFrom: getInheritedFromReference(declaration, event, context),
type: typeName == null || simpleType.kind === "ANY" ? "Event" : typeName,
detailType: customEventDetailType != null ? getTypeHintFromType(customEventDetailType, context.checker, context.config) : undefined
};
});
}
/**
* Returns slot docs for a declaration
* @param declaration
* @param context
*/
function getSlotDocsFromDeclaration(declaration, context) {
return declaration.slots.map(slot => {
var _a;
return ({
description: (_a = slot.jsDoc) === null || _a === void 0 ? void 0 : _a.description,
name: slot.name || "",
inheritedFrom: getInheritedFromReference(declaration, slot, context)
});
});
}
/**
* Returns css properties for a declaration
* @param declaration
* @param context
*/
function getCSSPropertyDocsFromDeclaration(declaration, context) {
return declaration.cssProperties.map(cssProperty => {
var _a;
return ({
name: cssProperty.name,
description: (_a = cssProperty.jsDoc) === null || _a === void 0 ? void 0 : _a.description,
type: cssProperty.typeHint,
default: cssProperty.default != null ? JSON.stringify(cssProperty.default) : undefined,
inheritedFrom: getInheritedFromReference(declaration, cssProperty, context)
});
});
}
/**
* Returns css parts for a declaration
* @param declaration
* @param context
*/
function getCSSPartDocsFromDeclaration(declaration, context) {
return declaration.cssParts.map(cssPart => {
var _a;
return ({
name: cssPart.name,
description: (_a = cssPart.jsDoc) === null || _a === void 0 ? void 0 : _a.description,
inheritedFrom: getInheritedFromReference(declaration, cssPart, context)
});
});
}
/**
* Returns attribute docs for a declaration
* @param declaration
* @param context
*/
function getAttributeDocsFromDeclaration(declaration, context) {
var _a, _b;
const attributeDocs = [];
for (const member of filterVisibility(context.config.visibility, declaration.members)) {
if (member.attrName != null) {
attributeDocs.push({
name: member.attrName,
fieldName: member.propName,
defaultValue: member.default != null ? JSON.stringify(member.default) : undefined,
description: (_a = member.jsDoc) === null || _a === void 0 ? void 0 : _a.description,
type: getTypeHintFromType(member.typeHint || ((_b = member.type) === null || _b === void 0 ? void 0 : _b.call(member)), context.checker, context.config),
inheritedFrom: getInheritedFromReference(declaration, member, context)
});
}
}
return attributeDocs;
}
/**
* Returns class member docs for a declaration
* @param declaration
* @param context
*/
function getClassMemberDocsFromDeclaration(declaration, context) {
return [...getFieldDocsFromDeclaration(declaration, context), ...getMethodDocsFromDeclaration(declaration, context)];
}
/**
* Returns method docs for a declaration
* @param declaration
* @param context
*/
function getMethodDocsFromDeclaration(declaration, context) {
var _a;
const methodDocs = [];
for (const method of filterVisibility(context.config.visibility, declaration.methods)) {
const parameters = [];
let returnType = undefined;
const node = method.node;
if (node !== undefined && tsModule__namespace.isMethodDeclaration(node)) {
// Build a list of parameters
for (const param of node.parameters) {
const name = param.name.getText();
const { description, typeHint } = getParameterFromJsDoc(name, method.jsDoc);
parameters.push({
name: name,
type: getTypeHintFromType(typeHint || (param.type != null ? context.checker.getTypeAtLocation(param.type) : undefined), context.checker, context.config),
description: description
});
}
// Get return type
const signature = context.checker.getSignatureFromDeclaration(node);
if (signature != null) {
returnType = context.checker.getReturnTypeOfSignature(signature);
}
}
// Get return info from jsdoc
const { description: returnDescription, typeHint: returnTypeHint } = getReturnFromJsDoc(method.jsDoc);
methodDocs.push({
kind: "method",
name: method.name,
privacy: method.visibility,
type: getTypeHintFromMethod(method, context.checker),
description: (_a = method.jsDoc) === null || _a === void 0 ? void 0 : _a.description,
parameters,
return: {
description: returnDescription,
type: getTypeHintFromType(returnTypeHint || returnType, context.checker, context.config)
},
inheritedFrom: getInheritedFromReference(declaration, method, context),
summary: getSummaryFromJsDoc(method.jsDoc)
// TODO: "static"
});
}
return methodDocs;
}
/**
* Returns field docs from a declaration
* @param declaration
* @param context
*/
function getFieldDocsFromDeclaration(declaration, context) {
var _a, _b;
const fieldDocs = [];
for (const member of filterVisibility(context.config.visibility, declaration.members)) {
if (member.propName != null) {
fieldDocs.push({
kind: "field",
name: member.propName,
privacy: member.visibility,
description: (_a = member.jsDoc) === null || _a === void 0 ? void 0 : _a.description,
type: getTypeHintFromType(member.typeHint || ((_b = member.type) === null || _b === void 0 ? void 0 : _b.call(member)), context.checker, context.config),
default: member.default != null ? JSON.stringify(member.default) : undefined,
inheritedFrom: getInheritedFromReference(declaration, member, context),
summary: getSummaryFromJsDoc(member.jsDoc)
// TODO: "static"
});
}
}
return fieldDocs;
}
function getInheritedFromReference(onDeclaration, feature, context) {
if (feature.declaration != null && feature.declaration !== onDeclaration) {
return getReferenceForNode(feature.declaration.node, context);
}
return undefined;
}
/**
* Returns a Reference to a node
* @param node
* @param context
*/
function getReferenceForNode(node, context) {
const sourceFile = node.getSourceFile();
const name = getNodeName(node, context);
// Test if the source file is from a typescript lib
// TODO: Find a better way of checking this
const isLib = sourceFile.isDeclarationFile && sourceFile.fileName.match(/typescript\/lib.*\.d\.ts$/) != null;
if (isLib) {
// Only return the name of the declaration if it's from lib
return {
name
};
}
// Test if the source file is located in a package
const packageName = getPackageName(sourceFile);
if (packageName != null) {
return {
name,
package: packageName
};
}
// Get the module path name
const module = getRelativePath(sourceFile.fileName, context);
return {
name,
module
};
}
/**
* Returns the name of the package (if any)
* @param sourceFile
*/
function getPackageName(sourceFile) {
// TODO: Make it possible to access the ModuleResolutionHost
// in order to resolve the package using "resolveModuleNames"
// The following approach is very, very naive and is only temporary.
const match = sourceFile.fileName.match(/node_modules\/(.*?)\//);
if (match != null) {
return match[1];
}
return undefined;
}
/**
* Returns a relative path based on "cwd" in the config
* @param fullPath
* @param context
*/
function getRelativePath(fullPath, context) {
return context.config.cwd != null ? `./${path.relative(context.config.cwd, fullPath)}` : path.basename(fullPath);
}
/**
* Returns description and typeHint based on jsdoc for a specific parameter name
* @param name
* @param jsDoc
*/
function getParameterFromJsDoc(name, jsDoc) {
if ((jsDoc === null || jsDoc === void 0 ? void 0 : jsDoc.tags) == undefined) {
return {};
}
for (const tag of jsDoc.tags) {
const parsed = tag.parsed();
if (parsed.tag === "param" && parsed.name === name) {
return { description: parsed.description, typeHint: parsed.type };
}
}
return {};
}
/**
* Get return description and return typeHint from jsdoc
* @param jsDoc
*/
function getReturnFromJsDoc(jsDoc) {
var _a;
const tag = (_a = jsDoc === null || jsDoc === void 0 ? void 0 : jsDoc.tags) === null || _a === void 0 ? void 0 : _a.find(tag => ["returns", "return"].includes(tag.tag));
if (tag == null) {
return {};
}
const parsed = tag.parsed();
return { description: parsed.description, typeHint: parsed.type };
}
/**
* Converts a heritage clause into a reference
* @param heritage
* @param context
*/
function getReferenceFromHeritageClause(heritage, context) {
var _a;
const node = (_a = heritage.declaration) === null || _a === void 0 ? void 0 : _a.node;
const identifier = heritage.identifier;
// Return a reference for this node if any
if (node != null) {
return getReferenceForNode(node, context);
}
// Try to get declaration of the identifier if no node was found
const [declaration] = resolveDeclarations(identifier, context);
if (declaration != null) {
return getReferenceForNode(declaration, context);
}
// Just return the name of the reference if nothing could be resolved
const name = getNodeName(identifier, context);
if (name != null) {
return { name };
}
return undefined;
}
/**
* Flatten all analyzer results with inherited declarations
* @param results
*/
function flattenAnalyzerResults(results) {
// Keep track of declarations in each source file
const declarationMap = new Map();
/**
* Add a declaration to the declaration map
* @param declaration
*/
function addDeclarationToMap(declaration) {
const sourceFile = declaration.node.getSourceFile();
const exportDocs = declarationMap.get(sourceFile) || new Set();
if (!declarationMap.has(sourceFile)) {
declarationMap.set(sourceFile, exportDocs);
}
exportDocs.add(declaration);
}
for (const result of results) {
for (const decl of result.declarations || []) {
// Add all existing declarations to the map
addDeclarationToMap(decl);
visitAllHeritageClauses(decl, clause => {
// Flatten all component declarations
if (clause.declaration != null) {
addDeclarationToMap(clause.declaration);
}
});
}
}
// Return new results with flattened declarations
return results.map(result => {
const declarations = declarationMap.get(result.sourceFile);
return {
...result,
declarations: declarations != null ? Array.from(declarations) : result.declarations
};
});
}
/**
* Returns the content of the summary jsdoc tag if any
* @param jsDoc
*/
function getSummaryFromJsDoc(jsDoc) {
var _a;
const summaryTag = (_a = jsDoc === null || jsDoc === void 0 ? void 0 : jsDoc.tags) === null || _a === void 0 ? void 0 : _a.find(tag => tag.tag === "summary");
if (summaryTag == null) {
return undefined;
}
return summaryTag.comment;
}
/**
* Parses and returns examples for a component.
* @param declaration
*/
function getExamplesFromComponent(declaration) {
var _a, _b;
const examples = ((_b = (_a = declaration.jsDoc) === null || _a === void 0 ? void 0 : _a.tags) === null || _b === void 0 ? void 0 : _b.filter(tag => tag.tag === "example" || tag.tag === "demo")) || [];
return examples.map(exampleFromJsDocTag);
}
/**
* Returns an example based on a jsdoc tag
* @param tag
*/
function exampleFromJsDocTag(tag) {
const { code, lang, description } = discoverCodeFromExampleText(tag.comment || "");
return {
lang,
description,
code
};
}
/**
* Parses some text and returns the first found example
* @param text
*/
function discoverCodeFromExampleText(text) {
// Check if there is a code example already like this: ```code here ```
const escapedCodeMatch = text.match(/([\s\S]*)```(\S*)([\s\S]+)```/);
if (escapedCodeMatch != null) {
return {
description: (escapedCodeMatch[1] || "").trim() || undefined,
lang: escapedCodeMatch[2] || undefined,
code: (escapedCodeMatch[3] || "").trim()
};
}
// Else, assume that the text is the code
return { code: text.trim(), lang: discoverLanguageFromExampleText(text) };
}
/**
* Returns the language of some code based on assumptions
* @param code
*/
function discoverLanguageFromExampleText(code) {
if (code.includes("html`")) {
return "javascript";
}
if (code.match(/<\S/)) {
return "html";
}
return "javascript";
}
/**
* Returns a markdown header with a specific level taking global start title level into account.
* @param title
* @param level
* @param config
*/
function markdownHeader(title, level, config) {
var _a, _b;
level = level - 1 + (((_a = config.markdown) === null || _a === void 0 ? void 0 : _a.headerLevel) || ((_b = config.markdown) === null || _b === void 0 ? void 0 : _b.titleLevel) || 1);
return `${"#".repeat(level)} ${title}`;
}
/**
* Returns a markdown table representation of the rows.
* Strips unused columns.
* @param rows
* @param removeEmptyColumns
*/
function markdownTable(rows, { removeEmptyColumns } = { removeEmptyColumns: true }) {
// Constants for pretty printing the markdown tables
const MIN_CELL_WIDTH = 3;
const MAX_CELL_WIDTH = 50;
const CELL_PADDING = 1;
// Count the number of columns
let columnCount = Math.max(...rows.map(r => r.length));
if (removeEmptyColumns) {
// Create a boolean array where each entry tells if a column is used or not (excluding the header)
const emptyColumns = Array(columnCount)
.fill(false)
.map((b, i) => i !== 0 && rows.slice(1).find(r => r[i] != null && r[i].length > 0) == null);
// Remove unused columns if necessary
if (emptyColumns.includes(true)) {
// Filter out the unused columns in each row
rows = rows.map(row => row.filter((column, i) => !emptyColumns[i]));
// Adjust the column count
columnCount = Math.max(...rows.map(r => r.length));
}
}
// Escape all cells in the markdown output
rows = rows.map(r => r.map(markdownEscapeTableCell));
// Create a boolean array where each entry corresponds to the preferred column width.
// This is done by taking the largest width of all cells in each column.
const columnWidths = Array(columnCount)
.fill(0)
.map((c, i) => Math.min(MAX_CELL_WIDTH, Math.max(MIN_CELL_WIDTH, ...rows.map(r => (r[i] || "").length)) + CELL_PADDING * 2));
// Build up the table
return `
|${rows[0].map((r, i) => fillWidth(r, columnWidths[i], CELL_PADDING)).join("|")}|
|${columnWidths.map(c => "-".repeat(c)).join("|")}|
${rows
.slice(1)
.map(r => `|${r.map((r, i) => fillWidth(r, columnWidths[i], CELL_PADDING)).join("|")}|`)
.join("\n")}
`;
}
/**
* Escape a text so it can be used in a markdown table
* @param text
*/
function markdownEscapeTableCell(text) {
return text.replace(/\n/g, "<br />").replace(/\|/g, "\\|");
}
/**
* Highlights some text
* @param text
*/
function markdownHighlight(text) {
if (text == null || text.length === 0)
return "";
return `\`${text}\``;
}
/**
* Creates padding around some text with a target width.
* @param text
* @param width
* @param paddingStart
*/
function fillWidth(text, width, paddingStart) {
return " ".repeat(paddingStart) + text + " ".repeat(Math.max(1, width - text.length - paddingStart));
}
/**
* Transforms the component results to markdown
* @param results
* @param program
* @param config
*/
const markdownTransformer = (results, program, config) => {
// Grab all definitions
const definitions = arrayFlat(results.map(res => res.componentDefinitions));
// Transform all definitions to markdown
const markdownSegments = arrayDefined(definitions.map(definition => {
var _a, _b;
// Add tagName as header
let segmentText = markdownHeader(definition.tagName, 1, config) + "\n";
const declaration = definition.declaration;
if (declaration == null) {
return undefined;
}
// Add component jsdoc comment to the output
if (((_a = declaration.jsDoc) === null || _a === void 0 ? void 0 : _a.description) != null)
segmentText += `\n${(_b = declaration.jsDoc) === null || _b === void 0 ? void 0 : _b.description}\n`;
// Add mixins (don't include mixins prefixed with _)
const mixins = arrayDedupe(arrayDefined(getMixinHeritageClausesInChain(declaration).map(clause => { var _a, _b; return ((_b = (_a = clause.declaration) === null || _a === void 0 ? void 0 : _a.symbol) === null || _b === void 0 ? void 0 : _b.name) || clause.identifier.getText(); })).filter(name => !name.startsWith("_")));
if (mixins.length > 0) {
segmentText += `\n**Mixins:** ${mixins.join(", ")}\n`;
}
// Add examples
const examples = getExamplesFromComponent(declaration);
if (examples.length > 0) {
segmentText += "\n" + markdownHeader(`Example${examples.length > 1 ? "s" : ""}`, 2, config) + "\n";
for (const example of examples) {
if (example.description != null) {
segmentText += `\n${example.description}\n`;
}
segmentText += `\n\`\`\`${example.lang || ""}\n${example.code}\n\`\`\`\n`;
}
}
// Grab all items from the component and add them as tables to the output.
const properties = filterVisibility(config.visibility, declaration.members
.filter((m) => m.kind === "property")
.filter(m => m.modifiers == null || !m.modifiers.has("static"))).sort((a, b) => (a.propName < b.propName ? -1 : 1));
const attributes = filterVisibility(config.visibility, declaration.members.filter((m) => m.kind === "attribute")).sort((a, b) => (a.attrName < b.attrName ? -1 : 1));
const methods = filterVisibility(config.visibility, declaration.methods).sort((a, b) => (a.name < b.name ? -1 : 1));
const slots = declaration.slots.sort((a, b) => (a.name == null ? -1 : b.name == null ? 1 : a.name < b.name ? -1 : 1));
const events = declaration.events.sort((a, b) => (a.name < b.name ? -1 : 1));
const cssProps = declaration.cssProperties.sort((a, b) => (a.name < b.name ? -1 : 1));
const cssParts = declaration.cssParts.sort((a, b) => (a.name < b.name ? -1 : 1));
if (attributes.length > 0) {
segmentText += "\n" + memberAttributeSection(attributes, program.getTypeChecker(), config);
}
if (properties.length > 0) {
segmentText += "\n" + memberPropertySection(properties, program.getTypeChecker(), config);
}
if (methods.length > 0) {
segmentText += "\n" + methodSection(methods, program.getTypeChecker(), config);
}
if (events.length > 0) {
segmentText += "\n" + eventSection(events, program.getTypeChecker(), config);
}
if (slots.length > 0) {
segmentText += "\n" + slotSection(slots, config);
}
if (cssParts.length > 0) {
segmentText += "\n" + cssPartSection(cssParts, config);
}
if (cssProps.length > 0) {
segmentText += "\n" + cssPropSection(cssProps, config);
}
return segmentText;
}));
return markdownSegments.join("\n\n");
};
/**
* Returns a markdown table with css props
* @param cssProperties
* @param config
*/
function cssPropSection(cssProperties, config) {
const rows = [["Property", "Type", "Default", "Description"]];
rows.push(...cssProperties.map(prop => {
var _a;
const def = (prop.default !== undefined ? JSON.stringify(prop.default) : "") || "";
return [(prop.name && markdownHighlight(prop.name)) || "", prop.typeHint || "", def, ((_a = prop.jsDoc) === null || _a === void 0 ? void 0 : _a.description) || ""];
}));
return markdownHeader("CSS Custom Properties", 2, config) + "\n" + markdownTable(rows);
}
/**
* Returns a markdown table with css parts
* @param cssPart
* @param config
*/
function cssPartSection(cssPart, config) {
const rows = [["Part", "Description"]];
rows.push(...cssPart.map(part => { var _a; return [(part.name && markdownHighlight(part.name)) || "", ((_a = part.jsDoc) === null || _a === void 0 ? void 0 : _a.description) || ""]; }));
return markdownHeader("CSS Shadow Parts", 2, config) + "\n" + markdownTable(rows);
}
/**
* Returns a markdown table with methods
* @param methods
* @param checker
* @param config
*/
function methodSection(methods, checker, config) {
const showVisibility = shouldShowVisibility(methods, config);
const rows = [["Method", "Visibility", "Type", "Description"]];
rows.push(...methods.map(method => {
var _a, _b, _c;
// Build up a description of parameters
const paramDescription = ((_b = (_a = method.jsDoc) === null || _a === void 0 ? void 0 : _a.tags) === null || _b === void 0 ? void 0 : _b.filter(tag => tag.tag === "param" && tag.comment != null).map(tag => {
return `**${tag.parsed().name}**: ${tag.parsed().description}`;
}).join("\n").trim()) || undefined;
const description = ((_c = method.jsDoc) === null || _c === void 0 ? void 0 : _c.description) || undefined;
return [
method.name != null ? markdownHighlight(method.name) : "",
showVisibility ? method.visibility || "public" : "",
markdownHighlight(getTypeHintFromMethod(method, checker)),
`${description || ""}${description != null && paramDescription != null ? "\n\n" : ""}${paramDescription || ""}`
];
}));
return markdownHeader("Methods", 2, config) + "\n" + markdownTable(rows);
}
/**
* Returns a markdown table with events
* @param events
* @param config
* @param checker
*/
function eventSection(events, checker, config) {
const showVisibility = shouldShowVisibility(events, config);
const rows = [["Event", "Visibility", "Type", "Description"]];
rows.push(...events.map(event => {
var _a, _b, _c;
return [
(event.name && markdownHighlight(event.name)) || "",
showVisibility ? event.visibility || "public" : "",
markdownHighlight(getTypeHintFromType((_a = event.typeHint) !== null && _a !== void 0 ? _a : (_b = event.type) === null || _b === void 0 ? void 0 : _b.call(event), checker, config)),
((_c = event.jsDoc) === null || _c === void 0 ? void 0 : _c.description) || ""
];
}));
return markdownHeader("Events", 2, config) + "\n" + markdownTable(rows);
}
/**
* Returns a markdown table with slots
* @param slots
* @param config
*/
function slotSection(slots, config) {
const rows = [["Name", "Permitted Tag Names", "Description"]];
rows.push(...slots.map(slot => {
var _a;
return [
(slot.name && markdownHighlight(slot.name)) || "",
(slot.permittedTagNames && slot.permittedTagNames.map(tagName => markdownHighlight(tagName)).join(" | ")) || "",
((_a = slot.jsDoc) === null || _a === void 0 ? void 0 : _a.description) || ""
];
}));
return markdownHeader("Slots", 2, config) + "\n" + markdownTable(rows);
}
/**
* Returns a markdown table with attributes.
* @param members
* @param checker
* @param config
*/
function memberAttributeSection(members, checker, config) {
var _a, _b, _c;
const showVisibility = shouldShowVisibility(members, config);
const rows = [["Attribute", "Visibility", "Type", "Default", "Description"]];
// Add members as rows one by one
for (const member of members) {
const attrName = markdownHighlight(member.attrName);
const type = markdownHighlight(getTypeHintFromType((_a = member.typeHint) !== null && _a !== void 0 ? _a : (_b = member.type) === null || _b === void 0 ? void 0 : _b.call(member), checker, config));
const visibility = member.visibility || "public";
const def = (member.default !== undefined ? JSON.stringify(member.default) : "") || (member.required && "**required**") || "";
const comment = ((_c = member.jsDoc) === null || _c === void 0 ? void 0 : _c.description) || "";
rows.push([attrName, showVisibility ? visibility : "", type, def, comment]);
}
return markdownHeader("Attributes", 2, config) + "\n" + markdownTable(rows);
}
/**
* Returns a markdown table with properties
* @param members
* @param checker
* @param config
*/
function memberPropertySection(members, checker, config) {
var _a, _b, _c;
const showVisibility = shouldShowVisibility(members, config);
const rows = [["Property", "Attribute", "Visibility", "Modifiers", "Type", "Default", "Description"]];
// Add properties as rows one by one
for (const member of members) {
const propName = markdownHighlight(member.propName);
const attrName = (member.attrName && markdownHighlight(member.attrName)) || "";
const visibility = member.visibility || "public";
const type = markdownHighlight(getTypeHintFromType((_a = member.typeHint) !== null && _a !== void 0 ? _a : (_b = member.type) === null || _b === void 0 ? void 0 : _b.call(member), checker, config));
const mods = member.modifiers != null ? Array.from(member.modifiers).join(", ") : "";
const def = (member.default !== undefined ? JSON.stringify(member.default) : "") || (member.required && "**required**") || "";
const comment = ((_c = member.jsDoc) === null || _c === void 0 ? void 0 : _c.description) || "";
rows.push([propName, attrName, showVisibility ? visibility : "", mods, type, def, comment]);
}
return markdownHeader("Properties", 2, config) + "\n" + markdownTable(rows);
}
function shouldShowVisibility(items, config) {
return (config.visibility != null && config.visibility !== "public" && items.some(method => method.visibility != null && method.visibility !== "public"));
}
/**
* Vscode json output format transformer.
* @param results
* @param program
* @param config
*/
const vscodeTransformer = (results, program, config) => {
const checker = program.getTypeChecker();
// Grab all definitions
const definitions = results.map(res => res.componentDefinitions).reduce((acc, cur) => [...acc, ...cur], []);
// Transform all definitions into "tags"
const tags = definitions.map(d => definitionToHtmlDataTag(d, checker));
const vscodeJson = {
version: 1,
tags,
globalAttributes: [],
valueSets: []
};
return JSON.stringify(vscodeJson, null, 2);
};
function definitionToHtmlDataTag(definition, checker) {
const declaration = definition.declaration;
if (declaration == null) {
return {
name: definition.tagName,
attributes: []
};
}
// Transform all members into "attributes"
const customElementAttributes = arrayDefined(declaration.members.map(d => componentMemberToVscodeAttr(d, checker)));
const eventAttributes = arrayDefined(declaration.events.map(e => componentEventToVscodeAttr(e, checker)));
const attributes = [...customElementAttributes, ...eventAttributes];
return {
name: definition.tagName,
description: formatMetadata(declaration.jsDoc, {
Events: declaration.events.map(e => { var _a; return formatEntryRow(e.name, e.jsDoc, (_a = e.type) === null || _a === void 0 ? void 0 : _a.call(e), checker); }),
Slots: declaration.slots.map(s => formatEntryRow(s.name || " ", s.jsDoc, s.permittedTagNames && s.permittedTagNames.map(n => `"${markdownHighlight(n)}"`).join(" | "), checker)),
Attributes: declaration.members
.map(m => { var _a; return ("attrName" in m && m.attrName != null ? formatEntryRow(m.attrName, m.jsDoc, m.typeHint || ((_a = m.type) === null || _a === void 0 ? void 0 : _a.call(m)), checker) : undefined); })
.filter(m => m != null),
Properties: declaration.members
.map(m => { var _a; return ("propName" in m && m.propName != null ? formatEntryRow(m.propName, m.jsDoc, m.typeHint || ((_a = m.type) === null || _a === void 0 ? void 0 : _a.call(m)), checker) : undefined); })
.filter(m => m != null)
}),
attributes
};
}
function componentEventToVscodeAttr(event, checker) {
var _a;
return {
name: `on${event.name}`,
description: formatEntryRow(event.name, event.jsDoc, (_a = event.type) === null || _a === void 0 ? void 0 : _a.call(event), checker)
};
}
function componentMemberToVscodeAttr(member, checker) {
var _a, _b;
if (member.attrName == null) {
return undefined;
}
return {
name: member.attrName,
description: formatMetadata(formatEntryRow(member.attrName, member.jsDoc, member.typeHint || ((_a = member.type) === null || _a === void 0 ? void 0 : _a.call(member)), checker), {
Property: "propName" in member ? member.propName : undefined,
Default: member.default === undefined ? undefined : String(member.default)
}),
...((member.type && typeToVscodeValuePart((_b = member.type) === null || _b === void 0 ? void 0 : _b.call(member), checker)) || {})
};
}
/**
* Converts a type to either a value set or string unions.
* @param type
* @param checker
*/
function typeToVscodeValuePart(type, checker) {
const simpleType = tsSimpleType.isSimpleType(type) ? type : tsSimpleType.toSimpleType(type, checker);
switch (simpleType.kind) {
case "BOOLEAN":
return { valueSet: "v" };
case "STRING_LITERAL":
return { values: [{ name: simpleType.value }] };
case "ENUM":
return { values: typesToStringUnion(simpleType.types.map(({ type }) => type)) };
case "UNION":
return { values: typesToStringUnion(simpleType.types) };
}
return undefined;
}
/**
* Returns a list of strings that represents the types.
* Only looks at literal types and strips the rest.
* @param types
*/
function typesToStringUnion(types) {
return arrayDefined(types.map(t => {
switch (t.kind) {
case "STRING_LITERAL":
case "NUMBER_LITERAL":
return { name: t.value.toString() };
default:
return undefined;
}
}));
}
/**
* Formats description and metadata so that it can be used in documentation.
* @param doc
* @param metadata
*/
function formatMetadata(doc, metadata) {
const metaText = arrayDefined(Object.entries(metadata).map(([key, value]) => {
if (value == null) {
return undefined;
}
else if (Array.isArray(value)) {
const filtered = arrayDefined(value);
if (filtered.length === 0)
return undefined;
return `${key}:\n\n${filtered.map(v => ` * ${v}`).join(`\n\n`)}`;
}
else {
return `${key}: ${value}`;
}
})).join(`\n\n`);
const comment = typeof doc === "string" ? doc : (doc === null || doc === void 0 ? void 0 : doc.description) || "";
return `${comment || ""}${metadata ? `${comment ? `\n\n` : ""}${metaText}` : ""}` || undefined;
}
/**
* Formats name, doc and type so that it can be presented in documentation
* @param name
* @param doc
* @param type
* @param checker
*/
function formatEntryRow(name, doc, type, checker) {
const comment = typeof doc === "string" ? doc : (doc === null || doc === void 0 ? void 0 : doc.description) || "";
const typeText = typeof type === "string" ? type : type == null ? "" : formatType(type, checker);
return `${markdownHighlight(name)}${typeText == null ? "" : ` {${typeText}}`}${comment == null ? "" : " - "}${comment || ""}`;
}
/**
* Formats a type to present in documentation
* @param type
* @param checker
*/
function formatType(type, checker) {
return !tsSimpleType.isAssignableToSimpleTypeKind(type, "ANY", checker) ? markdownHighlight(tsSimpleType.typeToString(type, checker)) : undefined;
}
const transformerFunctionMap = {
debug: debugJsonTransformer,
json: jsonTransformer,
json2: json2Transformer,
markdown: markdownTransformer,
md: markdownTransformer,
vscode: vscodeTransformer
};
/**
* Transforms the analyzer results into a string representation based on the transformer kind
* @param kind
* @param results
* @param program
* @param config
*/
function transformAnalyzerResult(kind, results, program, config = {}) {
const func = transformerFunctionMap[kind];
if (func == null) {
throw new Error(`Couldn't find transformer function for transformer kind: ${kind}`);
}
return func(Array.isArray(results) ? results : [results], program, {
visibility: "public",
...config
});
}
exports.ALL_COMPONENT_FEATURES = ALL_COMPONENT_FEATURES;
exports.CustomElementFlavor = CustomElementFlavor;
exports.DEFAULT_COMPONENT_DECLARATION_CACHE = DEFAULT_COMPONENT_DECLARATION_CACHE;
exports.DEFAULT_FEATURE_COLLECTION_CACHE = DEFAULT_FEATURE_COLLECTION_CACHE;
exports.JsDocFlavor = JsDocFlavor;
exports.VERSION = VERSION;
exports.analyzeComponentDeclaration = analyzeComponentDeclaration;
exports.analyzeSourceFile = analyzeSourceFile;
exports.arrayDefined = arrayDefined;
exports.arrayFlat = arrayFlat;
exports.getExtendsHeritageClauses = getExtendsHeritageClauses;
exports.getExtendsHeritageClausesInChain = getExtendsHeritageClausesInChain;
exports.getJsDoc = getJsDoc;
exports.getJsDocType = getJsDocType;
exports.getMixinHeritageClauses = getMixinHeritageClauses;
exports.getMixinHeritageClausesInChain = getMixinHeritageClausesInChain;
exports.getSuperclassHeritageClause = getSuperclassHeritageClause;
exports.makeContextFromConfig = makeContextFromConfig;
exports.parseSimpleJsDocTypeExpression = parseSimpleJsDocTypeExpression;
exports.stripTypescriptValues = stripTypescriptValues;
exports.transformAnalyzerResult = transformAnalyzerResult;
exports.visitAllHeritageClauses = visitAllHeritageClauses;