diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..23100c19 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,10 @@ +{ + "bracketSpacing": false, + "printWidth": 100, + "semi": true, + "singleQuote": false, + "tabWidth": 1, + "trailingComma": "none", + "useTabs": true, + "arrowParens": "always" +} diff --git a/package-lock.json b/package-lock.json index cb400490..157cb0a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "2.0.0-next.3", "license": "MIT", "dependencies": { + "custom-elements-manifest": "^1.0.0", "fast-glob": "^3.2.2", "ts-simple-type": "2.0.0-next.0", "typescript": "^4.4.3", @@ -1540,6 +1541,11 @@ "node": ">=0.10.0" } }, + "node_modules/custom-elements-manifest": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/custom-elements-manifest/-/custom-elements-manifest-1.0.0.tgz", + "integrity": "sha512-j59k0ExGCKA8T6Mzaq+7axc+KVHwpEphEERU7VZ99260npu/p/9kd+Db+I3cGKxHkM5y6q5gnlXn00mzRQkX2A==" + }, "node_modules/date-time": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz", @@ -6541,6 +6547,11 @@ "array-find-index": "^1.0.1" } }, + "custom-elements-manifest": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/custom-elements-manifest/-/custom-elements-manifest-1.0.0.tgz", + "integrity": "sha512-j59k0ExGCKA8T6Mzaq+7axc+KVHwpEphEERU7VZ99260npu/p/9kd+Db+I3cGKxHkM5y6q5gnlXn00mzRQkX2A==" + }, "date-time": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz", diff --git a/package.json b/package.json index 14bd33ec..8f688a23 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ }, "homepage": "https://github.com/runem/web-component-analyzer#readme", "dependencies": { + "custom-elements-manifest": "^1.0.0", "fast-glob": "^3.2.2", "ts-simple-type": "2.0.0-next.0", "typescript": "^4.4.3", diff --git a/src/cli/analyze/analyze-cli-command.ts b/src/cli/analyze/analyze-cli-command.ts index 419861a2..73eb8c72 100644 --- a/src/cli/analyze/analyze-cli-command.ts +++ b/src/cli/analyze/analyze-cli-command.ts @@ -28,7 +28,7 @@ export const analyzeCliCommand: CliCommand = async (config: AnalyzerCliConfig): ` !!!!!!!!!!!!! WARNING !!!!!!!!!!!!! The custom-elements.json format is for experimental purposes. You can expect changes to this format. -Please follow and contribute to the discussion at: +Please follow and contribute to the discussion at: - https://github.com/webcomponents/custom-elements-json - https://github.com/w3c/webcomponents/issues/776 !!!!!!!!!!!!! WARNING !!!!!!!!!!!!! @@ -42,6 +42,7 @@ Please follow and contribute to the discussion at: if (config.outDir == null && config.outFile == null && config.outFiles == null) { switch (config.format) { case "json2": + case "custom-elements-manifest": // "json2" will need to output everything at once return "console_bulk"; default: diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 46635e4f..2a77fe3f 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -50,7 +50,7 @@ o {tagname}: The element's tag name`, }) .option("format", { describe: `Specify output format`, - choices: ["md", "markdown", "json", "json2", "vscode"], + choices: ["md", "markdown", "json", "json2", "vscode", "custom-elements-manifest"], nargs: 1, alias: "f" }) diff --git a/src/transformers/custom-elements-manifest/get-declarations.ts b/src/transformers/custom-elements-manifest/get-declarations.ts new file mode 100644 index 00000000..144731c2 --- /dev/null +++ b/src/transformers/custom-elements-manifest/get-declarations.ts @@ -0,0 +1,443 @@ +/** + * @fileoverview A collection of utilities for computing custom element + * manifest declarations and exports from a given analyzer result. + * + * The entrypoint here is the `getDeclarationsFromResult` function which + * is responsible for traversing the internal analyzer result model + * and mapping to the appropriate manifest model. + * + * For example, mapping known exports into a list of manifest declarations. + */ +import * as tsModule from "typescript"; +import {isSimpleType, toSimpleType} from "ts-simple-type"; +import {ComponentDeclaration} from "../../analyze/types/component-declaration"; +import { + getMixinHeritageClauses, + getSuperclassHeritageClause +} from "../../analyze/util/component-declaration-util"; +import {findParent, getNodeName} from "../../analyze/util/ast-util"; +import {getJsDoc} from "../../analyze/util/js-doc-util"; +import {getTypeHintFromType} from "../../util/get-type-hint-from-type"; +import {AnalyzerResult} from "../../analyze/types/analyzer-result"; +import * as schema from "custom-elements-manifest/schema"; +import {TransformerContext} from "../transformer-context"; +import { + typeToSchemaType, + getSummaryFromJsDoc, + getParameterFromJsDoc, + getReturnFromJsDoc, + getReferenceFromHeritageClause, + getInheritedFromReference +} from "./utils"; + +/** + * Computes the exported symbols of a result (e.g. variables, functions, etc.) + * @param result + * @param context + */ +function* getExportedDeclarationsFromResult( + result: AnalyzerResult, + context: TransformerContext +): IterableIterator { + // Get all exported symbols in the source file + const symbol = context.checker.getSymbolAtLocation(result.sourceFile); + + if (symbol == null) { + return; + } + + const exports = context.checker.getExportsOfModule(symbol); + + // Iterate through exported symbols the typescript compiler detected + for (const exp of exports) { + const node = exp.valueDeclaration; + const variableFlags = tsModule.SymbolFlags.BlockScopedVariable | tsModule.SymbolFlags.Variable; + + // Produce the correct manifest declaration for each symbol + if (exp.flags & variableFlags && node && tsModule.isVariableDeclaration(node)) { + // Get the nearest variable statement in order to read the jsdoc + const variableStatement = findParent(node, tsModule.isVariableStatement) || node; + const jsDoc = getJsDoc(variableStatement, tsModule); + + yield { + kind: "variable", + name: node.name.getText(), + description: jsDoc?.description, + type: typeToSchemaType(context, context.checker.getTypeAtLocation(node)), + summary: getSummaryFromJsDoc(jsDoc) + }; + } + + if ( + exp.flags & tsModule.SymbolFlags.Function && + node && + tsModule.isFunctionDeclaration(node) && + node.name + ) { + const jsDoc = getJsDoc(node, tsModule); + const parameters: schema.Parameter[] = []; + let returnType: tsModule.Type | undefined = undefined; + + for (const param of node.parameters) { + const name = param.name.getText(); + const {description, typeHint} = getParameterFromJsDoc(name, jsDoc); + const type = typeToSchemaType( + context, + typeHint ?? (param.type != null ? context.checker.getTypeAtLocation(param.type) : undefined) + ); + + parameters.push({ + name, + type, + description, + optional: param.questionToken !== undefined + }); + } + + const signature = context.checker.getSignatureFromDeclaration(node); + if (signature != null) { + returnType = context.checker.getReturnTypeOfSignature(signature); + } + + const {description: returnDescription, typeHint: returnTypeHint} = getReturnFromJsDoc(jsDoc); + + yield { + kind: "function", + name: node.name.getText(), + description: jsDoc?.description, + summary: getSummaryFromJsDoc(jsDoc), + parameters, + return: { + type: typeToSchemaType(context, returnTypeHint || returnType), + description: returnDescription + } + }; + } + } +} + +/** + * Computes the class fields for a given component declaration + * @param declaration + * @param context + */ +function* getClassFieldsForComponent( + declaration: ComponentDeclaration, + context: TransformerContext +): IterableIterator { + const visibility = context.config.visibility ?? "public"; + for (const member of declaration.members) { + if (member.visibility === visibility && member.propName != null) { + yield { + kind: "field", + name: member.propName, + privacy: member.visibility, + description: member.jsDoc?.description, + type: typeToSchemaType(context, member.typeHint || member.type?.()), + default: member.default != null ? JSON.stringify(member.default) : undefined, + inheritedFrom: getInheritedFromReference(declaration, member, context), + summary: getSummaryFromJsDoc(member.jsDoc) + // TODO: "static" + }; + } + } +} + +/** + * Computes the methods for a given component declaration + * @param declaration + * @param context + */ +function* getMethodsForComponent( + declaration: ComponentDeclaration, + context: TransformerContext +): IterableIterator { + const visibility = context.config.visibility ?? "public"; + for (const method of declaration.methods) { + const parameters: schema.Parameter[] = []; + const node = method.node; + let returnType: tsModule.Type | undefined = undefined; + + if ( + method.visibility === visibility && + node !== undefined && + tsModule.isMethodDeclaration(node) + ) { + for (const param of node.parameters) { + const name = param.name.getText(); + const {description, typeHint} = getParameterFromJsDoc(name, method.jsDoc); + + parameters.push({ + name: name, + type: typeToSchemaType( + context, + typeHint || (param.type != null ? context.checker.getTypeAtLocation(param.type) : undefined) + ), + description: description, + optional: param.questionToken !== undefined + }); + } + + // 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 + ); + + yield { + kind: "method", + name: method.name, + privacy: method.visibility, + description: method.jsDoc?.description, + parameters, + return: { + description: returnDescription, + type: typeToSchemaType(context, returnTypeHint || returnType) + }, + inheritedFrom: getInheritedFromReference(declaration, method, context), + summary: getSummaryFromJsDoc(method.jsDoc) + // TODO: "static" + }; + } +} + +/** + * Computes the class members for a given component declaration + * @param declaration + * @param context + */ +function* getClassMembersForDeclaration( + declaration: ComponentDeclaration, + context: TransformerContext +): IterableIterator { + yield* getClassFieldsForComponent(declaration, context); + yield* getMethodsForComponent(declaration, context); +} + +/** + * Computes the events for a given component declaration + * @param declaration + * @param context + */ +function* getEventsFromComponent( + declaration: ComponentDeclaration, + context: TransformerContext +): IterableIterator { + const visibility = context.config.visibility ?? "public"; + for (const event of declaration.events) { + if (event.visibility === visibility) { + const type = event.type?.() ?? {kind: "ANY"}; + const simpleType = isSimpleType(type) ? type : toSimpleType(type, context.checker); + const typeName = + simpleType.kind === "GENERIC_ARGUMENTS" ? simpleType.target.name : simpleType.name; + yield { + description: event.jsDoc?.description, + name: event.name, + inheritedFrom: getInheritedFromReference(declaration, event, context), + type: + typeName === null || typeName === undefined || simpleType.kind === "ANY" + ? {text: "Event"} + : {text: typeName} + }; + } + } +} + +/** + * Computes the slots for a given component declaration + * @param declaration + * @param context + */ +function* getSlotsFromComponent( + declaration: ComponentDeclaration, + context: TransformerContext +): IterableIterator { + for (const slot of declaration.slots) { + yield { + description: slot.jsDoc?.description, + name: slot.name ?? "" + }; + } +} + +/** + * Computes the attributes for a given component declaration + * @param declaration + * @param context + */ +function* getAttributesFromComponent( + declaration: ComponentDeclaration, + context: TransformerContext +): IterableIterator { + const visibility = context.config.visibility ?? "public"; + for (const member of declaration.members) { + if (member.visibility === visibility && member.attrName) { + const type = getTypeHintFromType( + member.typeHint ?? member.type?.(), + context.checker, + context.config + ); + yield { + name: member.attrName, + fieldName: member.propName, + default: member.default != null ? JSON.stringify(member.default) : undefined, + description: member.jsDoc?.description, + type: type === undefined ? undefined : {text: type}, + inheritedFrom: getInheritedFromReference(declaration, member, context) + }; + } + } +} + +/** + * Computes the CSS properties for a given component declaration + * @param declaration + * @param context + */ +function* getCSSPropertiesFromComponent( + declaration: ComponentDeclaration, + context: TransformerContext +): IterableIterator { + for (const cssProperty of declaration.cssProperties) { + // TODO (43081j): somehow populate the syntax property + yield { + name: cssProperty.name, + description: cssProperty.jsDoc?.description, + default: cssProperty.default != null ? JSON.stringify(cssProperty.default) : undefined + }; + } +} + +/** + * Computes the CSS parts (::part) for a given component declaration + * @param declaration + * @param context + */ +function* getCSSPartsFromComponent( + declaration: ComponentDeclaration, + context: TransformerContext +): IterableIterator { + for (const cssPart of declaration.cssParts) { + yield { + name: cssPart.name, + description: cssPart.jsDoc?.description + }; + } +} + +/** + * Computes the manifest declaration of a given component declaration + * @param declaration + * @param result + * @param context + */ +function getDeclarationForComponent( + declaration: ComponentDeclaration, + result: AnalyzerResult, + context: TransformerContext +): schema.Declaration | undefined { + if (declaration.kind === "interface") { + return undefined; + } + + const superClassClause = getSuperclassHeritageClause(declaration); + const superClass = superClassClause + ? getReferenceFromHeritageClause(superClassClause, context) + : undefined; + const definition = result.componentDefinitions.find( + (def) => def.declaration?.node === declaration.node + ); + const mixinClauses = getMixinHeritageClauses(declaration); + const mixins = mixinClauses + .map((c) => getReferenceFromHeritageClause(c, context)) + .filter((c): c is schema.Reference => c !== undefined); + const members = [...getClassMembersForDeclaration(declaration, context)]; + const name = declaration.symbol?.name ?? getNodeName(declaration.node, {ts: tsModule}); + + if (!name) { + return undefined; + } + + const classDecl: schema.ClassDeclaration = { + kind: "class", + name, + superclass: superClass, + mixins: mixins.length > 0 ? mixins : undefined, + description: declaration.jsDoc?.description, + members: members.length > 0 ? members : undefined, + summary: getSummaryFromJsDoc(declaration.jsDoc) + }; + + if (!definition) { + return classDecl; + } + + const events = [...getEventsFromComponent(declaration, context)]; + const slots = [...getSlotsFromComponent(declaration, context)]; + const attributes = [...getAttributesFromComponent(declaration, context)]; + const cssProperties = [...getCSSPropertiesFromComponent(declaration, context)]; + const cssParts = [...getCSSPartsFromComponent(declaration, context)]; + + // Return a custom element doc if a definition was found + // TODO (43081j): remove the type union once custom-elements-manifest + // has a new version published to NPM + const customElementDoc: schema.CustomElementDeclaration & schema.CustomElement = { + ...classDecl, + customElement: true, + 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; +} + +/** + * Computes the manifest declarations for a given analyzer result + * @param result + * @param context + */ +function* getComponentDeclarationsFromResult( + result: AnalyzerResult, + context: TransformerContext +): IterableIterator { + if (result.declarations) { + for (const decl of result.declarations) { + const schemaDecl = getDeclarationForComponent(decl, result, context); + if (schemaDecl) { + yield schemaDecl; + } + } + } + + for (const definition of result.componentDefinitions) { + if (definition.declaration) { + const schemaDecl = getDeclarationForComponent(definition.declaration, result, context); + if (schemaDecl) { + yield schemaDecl; + } + } + } +} + +/** + * Returns declarations in an analyzer result + * @param result + * @param context + */ +export function* getDeclarationsFromResult( + result: AnalyzerResult, + context: TransformerContext +): IterableIterator { + yield* getExportedDeclarationsFromResult(result, context); + yield* getComponentDeclarationsFromResult(result, context); +} diff --git a/src/transformers/custom-elements-manifest/get-exports.ts b/src/transformers/custom-elements-manifest/get-exports.ts new file mode 100644 index 00000000..7c109756 --- /dev/null +++ b/src/transformers/custom-elements-manifest/get-exports.ts @@ -0,0 +1,58 @@ +import {AnalyzerResult} from "../../analyze/types/analyzer-result"; +import {TransformerContext} from "../transformer-context"; +import * as schema from "custom-elements-manifest/schema"; +import {getReferenceForNode} from "./utils"; + +function* getCustomElementExportsFromResult( + result: AnalyzerResult, + context: TransformerContext +): IterableIterator { + for (const definition of result.componentDefinitions) { + // 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) { + continue; + } + + yield { + kind: "custom-element-definition", + name: definition.tagName, + declaration: getReferenceForNode(definition.declaration.node, context) + }; + } +} + +function* getExportedNamesFromResult( + result: AnalyzerResult, + context: TransformerContext +): IterableIterator { + const symbol = context.checker.getSymbolAtLocation(result.sourceFile); + if (symbol == null) { + return; + } + + const exports = context.checker.getExportsOfModule(symbol); + + for (const exp of exports) { + if (exp.valueDeclaration) { + yield { + kind: "js", + name: exp.name, + declaration: getReferenceForNode(exp.valueDeclaration, context) + }; + } + } +} + +/** + * Returns exports in an analyzer result + * @param result + * @param context + */ +export function* getExportsFromResult( + result: AnalyzerResult, + context: TransformerContext +): IterableIterator { + yield* getCustomElementExportsFromResult(result, context); + yield* getExportedNamesFromResult(result, context); +} diff --git a/src/transformers/custom-elements-manifest/transformer.ts b/src/transformers/custom-elements-manifest/transformer.ts new file mode 100644 index 00000000..b30d4038 --- /dev/null +++ b/src/transformers/custom-elements-manifest/transformer.ts @@ -0,0 +1,113 @@ +import * as tsModule from "typescript"; +import {Program, SourceFile} from "typescript"; +import {AnalyzerResult} from "../../analyze/types/analyzer-result"; +import {ComponentDeclaration} from "../../analyze/types/component-declaration"; +import {visitAllHeritageClauses} from "../../analyze/util/component-declaration-util"; +import {TransformerConfig} from "../transformer-config"; +import {TransformerFunction} from "../transformer-function"; +import * as schema from "custom-elements-manifest/schema"; +import {TransformerContext} from "../transformer-context"; +import {getExportsFromResult} from "./get-exports"; +import {getDeclarationsFromResult} from "./get-declarations"; +import {getRelativePath} from "./utils"; + +/** + * Transforms results to a custom elements manifest + * @param results + * @param program + * @param config + */ +export const transformer: TransformerFunction = ( + results: AnalyzerResult[], + program: Program, + config: TransformerConfig +): string => { + const context: TransformerContext = { + config, + checker: program.getTypeChecker(), + program, + ts: tsModule + }; + + // 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) => resultToModule(result, context)); + + const manifest: schema.Package = { + schemaVersion: "1.0.0", + modules + }; + + return JSON.stringify(manifest, null, 2); +}; + +/** + * Transforms an analyzer result into a module + * @param result + * @param context + */ +function resultToModule( + result: AnalyzerResult, + context: TransformerContext +): schema.JavaScriptModule { + const exports = [...getExportsFromResult(result, context)]; + const declarations = [...getDeclarationsFromResult(result, context)]; + + return { + kind: "javascript-module", + path: getRelativePath(result.sourceFile.fileName, context), + declarations: declarations.length === 0 ? undefined : declarations, + exports: exports.length === 0 ? undefined : exports + }; +} + +/** + * Flatten all analyzer results with inherited declarations + * @param results + */ +function flattenAnalyzerResults(results: AnalyzerResult[]): AnalyzerResult[] { + // Keep track of declarations in each source file + const declarationMap = new Map>(); + + /** + * Add a declaration to the declaration map + * @param declaration + */ + function addDeclarationToMap(declaration: ComponentDeclaration) { + 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 + }; + }); +} diff --git a/src/transformers/custom-elements-manifest/utils.ts b/src/transformers/custom-elements-manifest/utils.ts new file mode 100644 index 00000000..df53580d --- /dev/null +++ b/src/transformers/custom-elements-manifest/utils.ts @@ -0,0 +1,191 @@ +import {basename, relative} from "path"; +import {SimpleType} from "ts-simple-type"; +import {Node, SourceFile, Type} from "typescript"; +import {TransformerContext} from "../transformer-context"; +import {JsDoc} from "../../analyze/types/js-doc"; +import * as schema from "custom-elements-manifest/schema"; +import {getNodeName, resolveDeclarations} from "../../analyze/util/ast-util"; +import { + ComponentDeclaration, + ComponentHeritageClause +} from "../../analyze/types/component-declaration"; +import {ComponentFeatureBase} from "../../analyze/types/features/component-feature"; +import {getTypeHintFromType} from "../../util/get-type-hint-from-type"; + +/** + * Returns a Reference to a node + * @param node + * @param context + */ +export function getReferenceForNode(node: Node, context: TransformerContext): schema.Reference { + const sourceFile = node.getSourceFile(); + const name = getNodeName(node, context) as string; + + // 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 + }; +} + +export function getInheritedFromReference( + onDeclaration: ComponentDeclaration, + feature: ComponentFeatureBase, + context: TransformerContext +): schema.Reference | undefined { + if (feature.declaration != null && feature.declaration !== onDeclaration) { + return getReferenceForNode(feature.declaration.node, context); + } + + return undefined; +} + +/** + * Returns a relative path based on "cwd" in the config + * @param fullPath + * @param context + */ +export function getRelativePath(fullPath: string, context: TransformerContext): string { + return context.config.cwd != null + ? `./${relative(context.config.cwd, fullPath)}` + : basename(fullPath); +} + +/** + * Returns the name of the package (if any) + * @param sourceFile + */ +export function getPackageName(sourceFile: SourceFile): string | undefined { + // 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\/(.*?)\//); + + return match?.[1]; +} + +/** + * Returns description and typeHint based on jsdoc for a specific parameter name + * @param name + * @param jsDoc + */ +export function getParameterFromJsDoc( + name: string, + jsDoc: JsDoc | undefined +): {description?: string; typeHint?: string} { + if (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 + */ +export function getReturnFromJsDoc(jsDoc: JsDoc | undefined): { + description?: string; + typeHint?: string; +} { + const tag = jsDoc?.tags?.find((tag) => tag.tag === "returns" || tag.tag === "return"); + + if (tag == null) { + return {}; + } + + const parsed = tag.parsed(); + return {description: parsed.description, typeHint: parsed.type}; +} + +/** + * Converts a typescript type to a schema type + * @param context + * @param type + */ +export function typeToSchemaType( + context: TransformerContext, + type: string | Type | SimpleType | undefined +): schema.Type | undefined { + const hint = getTypeHintFromType(type, context.checker, context.config); + + if (!hint) { + return undefined; + } + + return { + // TODO (43081j): specify type references here via the `references` property + text: hint + }; +} + +/** + * Returns the content of the summary jsdoc tag if any + * @param jsDoc + */ +export function getSummaryFromJsDoc(jsDoc: JsDoc | undefined): string | undefined { + const summaryTag = jsDoc?.tags?.find((tag) => tag.tag === "summary"); + + return summaryTag?.comment; +} + +/** + * Converts a heritage clause into a reference + * @param heritage + * @param context + */ +export function getReferenceFromHeritageClause( + heritage: ComponentHeritageClause, + context: TransformerContext +): schema.Reference | undefined { + const node = heritage.declaration?.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; +} diff --git a/src/transformers/index.ts b/src/transformers/index.ts index b2fa136c..7187f38a 100644 --- a/src/transformers/index.ts +++ b/src/transformers/index.ts @@ -1,3 +1,3 @@ -export * from "./transformer-config"; -export * from "./transformer-kind"; -export * from "./transform-analyzer-result"; +export * from './transformer-config'; +export * from './transformer-kind'; +export * from './transform-analyzer-result'; diff --git a/src/transformers/json2/json2-transformer.ts b/src/transformers/json2/json2-transformer.ts index 6c65bf1a..58e73c31 100644 --- a/src/transformers/json2/json2-transformer.ts +++ b/src/transformers/json2/json2-transformer.ts @@ -1,7 +1,7 @@ import { basename, relative } from "path"; import { isSimpleType, toSimpleType } from "ts-simple-type"; import * as tsModule from "typescript"; -import { Node, Program, SourceFile, Type, TypeChecker } from "typescript"; +import { Node, Program, SourceFile, Type } from "typescript"; import { AnalyzerResult } from "../../analyze/types/analyzer-result"; import { ComponentDeclaration, ComponentHeritageClause } from "../../analyze/types/component-declaration"; import { ComponentFeatureBase } from "../../analyze/types/features/component-feature"; @@ -36,13 +36,7 @@ import { SlotDoc, VariableDoc } from "./schema"; - -interface TransformerContext { - config: TransformerConfig; - checker: TypeChecker; - program: Program; - ts: typeof tsModule; -} +import { TransformerContext } from "../transformer-context"; /** * Transforms results to json using the schema found in the PR at https://github.com/webcomponents/custom-elements-json/pull/9 diff --git a/src/transformers/transform-analyzer-result.ts b/src/transformers/transform-analyzer-result.ts index d2b06b9d..38af7bc2 100644 --- a/src/transformers/transform-analyzer-result.ts +++ b/src/transformers/transform-analyzer-result.ts @@ -3,6 +3,7 @@ import { AnalyzerResult } from "../analyze/types/analyzer-result"; import { debugJsonTransformer } from "./debug/debug-json-transformer"; import { jsonTransformer } from "./json/json-transformer"; import { json2Transformer } from "./json2/json2-transformer"; +import { transformer as customElementsTransformer } from "./custom-elements-manifest/transformer"; import { markdownTransformer } from "./markdown/markdown-transformer"; import { TransformerConfig } from "./transformer-config"; import { TransformerFunction } from "./transformer-function"; @@ -15,7 +16,8 @@ const transformerFunctionMap: Record = { json2: json2Transformer, markdown: markdownTransformer, md: markdownTransformer, - vscode: vscodeTransformer + vscode: vscodeTransformer, + "custom-elements-manifest": customElementsTransformer }; /** diff --git a/src/transformers/transformer-context.ts b/src/transformers/transformer-context.ts new file mode 100644 index 00000000..5e453193 --- /dev/null +++ b/src/transformers/transformer-context.ts @@ -0,0 +1,9 @@ +import * as tsModule from "typescript"; +import { TransformerConfig } from "./transformer-config"; + +export interface TransformerContext { + config: TransformerConfig; + checker: tsModule.TypeChecker; + program: tsModule.Program; + ts: typeof tsModule; +} diff --git a/src/transformers/transformer-kind.ts b/src/transformers/transformer-kind.ts index c87a2f3a..4d88e2ed 100644 --- a/src/transformers/transformer-kind.ts +++ b/src/transformers/transformer-kind.ts @@ -1 +1 @@ -export type TransformerKind = "md" | "markdown" | "json" | "vscode" | "debug" | "json2"; +export type TransformerKind = "md" | "markdown" | "json" | "vscode" | "debug" | "json2" | "custom-elements-manifest";