web-component-analyzer
Version:
CLI that analyzes web components
1,395 lines (1,380 loc) • 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;