From 2acfb6d663e03313699bfa2572ac9bdd2c66f117 Mon Sep 17 00:00:00 2001 From: Justin Fagnani Date: Fri, 27 Mar 2020 10:37:58 -0700 Subject: [PATCH] Add an experimental "json2" output format --- package-lock.json | 12 +- src/cli/cli.ts | 2 +- src/transformers/json2/json2-transformer.ts | 198 +++++++++++++ src/transformers/json2/schema.ts | 269 ++++++++++++++++++ src/transformers/transform-analyzer-result.ts | 2 + src/transformers/transformer-kind.ts | 2 +- 6 files changed, 477 insertions(+), 8 deletions(-) create mode 100644 src/transformers/json2/json2-transformer.ts create mode 100644 src/transformers/json2/schema.ts diff --git a/package-lock.json b/package-lock.json index 4c1c8350..18acf0f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8551,9 +8551,9 @@ "dev": true }, "typescript-3.6": { - "version": "npm:typescript@3.6.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", - "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==", + "version": "npm:typescript@3.6.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.5.tgz", + "integrity": "sha512-BEjlc0Z06ORZKbtcxGrIvvwYs5hAnuo6TKdNFL55frVDlB+na3z5bsLhFaIxmT+dPWgBIjMo6aNnTOgHHmHgiQ==", "dev": true }, "typescript-3.7": { @@ -8563,9 +8563,9 @@ "dev": true }, "typescript-3.8": { - "version": "npm:typescript@3.8.0-dev.20200117", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.0-dev.20200117.tgz", - "integrity": "sha512-80flPGfPkB9agERB/XHIt1C9F27EYBt3alVUS8VVjblPvigLjQbva/tkAhGacsbrRYzL8qWIEwRVTOrtiT2CaQ==", + "version": "npm:typescript@3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", "dev": true }, "ua-parser-js": { diff --git a/src/cli/cli.ts b/src/cli/cli.ts index a87a28f4..412855b6 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", "vscode"], + choices: ["md", "markdown", "json", "json2", "vscode"], nargs: 1, alias: "f" }) diff --git a/src/transformers/json2/json2-transformer.ts b/src/transformers/json2/json2-transformer.ts new file mode 100644 index 00000000..4dfc34e4 --- /dev/null +++ b/src/transformers/json2/json2-transformer.ts @@ -0,0 +1,198 @@ +import * as path from "path"; +import { Program } from "typescript"; +import ts from 'typescript'; + +// import { Program, TypeChecker } from "typescript"; +import { AnalyzerResult } from "../../analyze/types/analyzer-result"; +// import { ComponentDefinition } from "../../analyze/types/component-definition"; +// import { ComponentCssPart } from "../../analyze/types/features/component-css-part"; +// import { ComponentCssProperty } from "../../analyze/types/features/component-css-property"; +// import { ComponentEvent } from "../../analyze/types/features/component-event"; +// import { ComponentMember } from "../../analyze/types/features/component-member"; +// import { ComponentSlot } from "../../analyze/types/features/component-slot"; +import { JsDoc } from "../../analyze/types/js-doc"; +// import { arrayDefined } from "../../util/array-util"; +import { getTypeHintFromType } from "../../util/get-type-hint-from-type"; +import { filterVisibility } from "../../util/model-util"; +// import { getFirst } from "../../util/set-util"; +import { TransformerConfig } from "../transformer-config"; +import { TransformerFunction } from "../transformer-function"; +// import { +// HtmlData, +// HtmlDataAttribute, +// HtmlDataCssPart, +// HtmlDataCssProperty, +// HtmlDataEvent, +// HtmlDataProperty, +// HtmlDataSlot, +// HtmlDataTag +// } from "./custom-elements-json-data"; + +import {PackageDoc, ModuleDoc, CustomElementDoc, ClassMember, Parameter, AttributeDoc, EventDoc, CSSPropertyDoc, CSSPartDoc} from './schema.js'; +import { toTypeString } from "ts-simple-type"; + +/** + * Transforms results to json. + * @param results + * @param program + * @param config + */ +export const json2Transformer: TransformerFunction = (results: AnalyzerResult[], program: Program, config: TransformerConfig): string => { + const checker = program.getTypeChecker(); + const cwd = config?.cwd ?? process.cwd(); + + const modules: Map = new Map(); + + // Get all modules + for (const result of results) { + const {sourceFile} = result; + console.log('AA', `\n${sourceFile.fileName}\n${cwd}`); + // console.log(result); + + // TODO: calculate actual JS output path + const modulePath = path.basename(sourceFile.fileName); + let module = modules.get(modulePath); + if (module === undefined) { + modules.set(modulePath, module = { + path: modulePath, + }); + } + + for (const def of result.componentDefinitions) { + module.exports = module.exports ?? []; + const declaration = def.declaration(); + + let className: string | undefined; + for (const node of declaration.declarationNodes) { + if (ts.isClassDeclaration(node)) { + className = node.name?.getText(); + break; + } + } + + let members: Array | undefined; + const visibleMembers = filterVisibility(config.visibility, declaration.members); + if (visibleMembers.length > 0) { + members = members ?? []; + for (const member of visibleMembers) { + const type = member.type?.(); + if (member.kind === 'property') { + console.log('property', member.propName, member.default); + members.push({ + kind: 'field', + name: member.propName, + description: getDescriptionFromJsDoc(member.jsDoc), + default: member.default as any, + privacy: member.visibility, + type: type === undefined ? type : toTypeString(type, checker), + }); + } + } + } + + const visibleMethods = filterVisibility(config.visibility, declaration.methods); + if (visibleMethods.length > 0) { + members = members ?? []; + for (const method of visibleMethods) { + const parameters: Array = []; + const node = method.node; + if (node !== undefined && ts.isMethodDeclaration(node)) { + for (const param of node.parameters) { + const name = param.name.getText(); + parameters.push({ + name: name, + type: param.type && toTypeString(checker.getTypeAtLocation(param.type), checker), + description: getParameterDescriptionFromJsDoc(name, method.jsDoc), + }); + } + } + + members.push({ + kind: 'method', + name: method.name, + description: getDescriptionFromJsDoc(method.jsDoc), + privacy: method.visibility, + parameters, + }); + } + } + + const attributes: Array = + filterVisibility(config.visibility, declaration.members).map((d) => { + // const type = d.type?.(); + return { + name: d.attrName!, + fieldName: d.propName, + description: getDescriptionFromJsDoc(d.jsDoc), + type: getTypeHintFromType(d.typeHint ?? d.type?.(), checker, config), + default: d.default != null ? JSON.stringify(d.default) : undefined, + }; + }); + + const events: Array = + filterVisibility(config.visibility, declaration.events).map(e => { + return { + name: e.name!, + description: getDescriptionFromJsDoc(e.jsDoc), + type: getTypeHintFromType(e.typeHint ?? e.type?.(), checker, config), + }; + }); + + // const slots = arrayDefined(declaration.slots.map(e => componentSlotToHtmlDataSlot(e, checker))); + + const cssProperties: Array = declaration.cssProperties.map(p => { + return { + name: p.name, + description: getDescriptionFromJsDoc(p.jsDoc), + type: getTypeHintFromType(p.typeHint, checker, config), + default: String(p.default), + }; + }); + + const cssParts: Array = declaration.cssParts.map(p => { + return { + name: p.name, + description: getDescriptionFromJsDoc(p.jsDoc), + }; + }); + + + let el: CustomElementDoc = { + kind: 'class', + name: className, + description: getDescriptionFromJsDoc(declaration.jsDoc), + tagName: def.tagName, + members, + attributes: attributes.length === 0 ? undefined : attributes, + events: events.length === 0 ? undefined : events, + // slots: slots.length === 0 ? undefined : slots, + cssProperties: cssProperties.length === 0 ? undefined : cssProperties, + cssParts: cssParts.length === 0 ? undefined : cssParts, + }; + module.exports.push(el); + } + } + + const packageDoc: PackageDoc = { + version: "experimental", + modules: Array.from(modules.values()), + }; + + return JSON.stringify(packageDoc, null, 2); +}; + +function getDescriptionFromJsDoc(jsDoc: JsDoc | undefined): string | undefined { + return jsDoc?.description; +} + +function getParameterDescriptionFromJsDoc(name: string, jsDoc: JsDoc | undefined): string | undefined { + if (jsDoc?.tags === undefined) { + return undefined; + } + for (const tag of jsDoc.tags) { + const parsed = tag.parsed(); + if (parsed.tag === 'param' && parsed.name === name) { + return parsed.description; + } + } +} diff --git a/src/transformers/json2/schema.ts b/src/transformers/json2/schema.ts new file mode 100644 index 00000000..999ab0cf --- /dev/null +++ b/src/transformers/json2/schema.ts @@ -0,0 +1,269 @@ +/** + * The top-level interface of a custom-elements.json file. + * + * custom-elements.json documents all the elements in a single npm package, + * across all modules within the package. Elements may be exported from multiple + * modules with re-exports, but as a rule, elements in this file should be + * included once in the "canonical" module that they're exported from. + */ +export interface PackageDoc { + version: string; + + /** + * An array of the modules this package contains. + */ + modules: Array; +} + +export interface ModuleDoc { + path: string; + + /** + * A markdown summary suitable for display in a listing. + */ + summary?: string; + + /** + * A markdown description of the module. + */ + description?: string; + + exports?: Array; +} + +export type ExportDoc = ClassDoc|FunctionDoc|VariableDoc; + +/** + * A reference to an export of a module. + * + * All references are required to be publically accessible, so the canonical + * representation of a refernce it the export it's available from. + */ +export interface Reference { + name: string; + package?: string; + module?: string; +} + +export interface CustomElementDoc extends ClassDoc { + tagName: string; + /** + * The attributes that this element is known to understand. + */ + attributes?: AttributeDoc[]; + + /** The events that this element fires. */ + events?: EventDoc[]; + + /** + * The shadow dom content slots that this element accepts. + */ + slots?: SlotDoc[]; + + cssProperties?: CSSPropertyDoc[]; + + cssParts?: CSSPartDoc[]; + + demos?: Demo[]; +} + +export interface AttributeDoc { + name: string; + + /** + * A markdown description for the attribute. + */ + description?: string; + + /** + * The type that the attribute will be serialized/deserialized as. + */ + type?: string; + + /** + * The default value of the attribute, if any. + * + * As attributes are always strings, this is the actual value, not a human + * readable description. + */ + defaultValue?: string; + + /** + * The name of the field this attribute is associated with, if any. + */ + fieldName?: string; +} + +export interface EventDoc { + name: string; + + /** + * A markdown description of the event. + */ + description?: string; + + /** + * The type of the event object that's fired. + * + * If the event type is built-in, this is a string, e.g. `Event`, + * `CustomEvent`, `KeyboardEvent`. If the event type is an event class defined + * in a module, the reference to it. + */ + type?: Reference|string; + + /** + * If the event is a CustomEvent, the type of `detail` field. + */ + detailType?: string; +} + +export interface SlotDoc { + /** + * The slot name, or the empty string for an unnamed slot. + */ + name: string; + + /** + * A markdown description of the slot. + */ + description?: string; +} + +export interface CSSPropertyDoc { + name: string; + description?: string; + type?: string; + default?: string; +} + +export interface CSSPartDoc { + name: string; + description?: string; +} + +export interface ClassDoc { + kind: 'class'; + + /** + * The class name, or `undefined` if the class is anonymous. + */ + name?: string; + + /** + * A markdown summary suitable for display in a listing. + * TODO: restrictions on markdown/markup. ie, no headings, only inline + * formatting? + */ + summary?: string; + + /** + * A markdown description of the class. + */ + description?: string; + superclass?: Reference; + mixins?: Array; + members?: Array; +} + +export type ClassMember = FieldDoc|MethodDoc; + +export interface FieldDoc { + kind: 'field'; + name: string; + static?: boolean; + + /** + * A markdown summary suitable for display in a listing. + * TODO: restrictions on markdown/markup. ie, no headings, only inline + * formatting? + */ + summary?: string; + + /** + * A markdown description of the field. + */ + description?: string; + default?: string; // TODO: make this a Type type or a Reference + privacy?: Privacy; + type?: string; +} + +export interface MethodDoc extends FunctionLike { + kind: 'method'; + + static?: boolean; +} + +/** + * TODO: tighter definition of mixin: + * - Should it only accept a single argument? + * - Should it not extend ClassDoc so it doesn't has a superclass? + * - What's TypeScript's exact definition? + */ +export interface MixinDoc extends ClassDoc {} + +export interface VariableDoc { + kind: 'variable'; + + name: string; + + /** + * A markdown summary suitable for display in a listing. + */ + summary?: string; + + /** + * A markdown description of the class. + */ + description?: string; + type?: string; +} + +export interface FunctionDoc extends FunctionLike { + kind: 'function'; +} + +export interface Parameter { + name: string, + type?: string, + description?: string, +} + +export interface FunctionLike { + name: string; + + /** + * A markdown summary suitable for display in a listing. + */ + summary?: string; + + /** + * A markdown description of the class. + */ + description?: string; + + parameters?: Array; + + return?: { + type?: string, + description?: string, + }; + + privacy?: Privacy; + type?: string; +} + +export type Privacy = 'public'|'private'|'protected'; + +export interface Demo { + /** + * A markdown description of the demo. + */ + description?: string; + + /** + * Relative URL of the demo if it's published with the package. Absolute URL + * if it's hosted. + */ + url: string; +} diff --git a/src/transformers/transform-analyzer-result.ts b/src/transformers/transform-analyzer-result.ts index 9c630bd3..d2b06b9d 100644 --- a/src/transformers/transform-analyzer-result.ts +++ b/src/transformers/transform-analyzer-result.ts @@ -2,6 +2,7 @@ import { Program } from "typescript"; 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 { markdownTransformer } from "./markdown/markdown-transformer"; import { TransformerConfig } from "./transformer-config"; import { TransformerFunction } from "./transformer-function"; @@ -11,6 +12,7 @@ import { vscodeTransformer } from "./vscode/vscode-transformer"; const transformerFunctionMap: Record = { debug: debugJsonTransformer, json: jsonTransformer, + json2: json2Transformer, markdown: markdownTransformer, md: markdownTransformer, vscode: vscodeTransformer diff --git a/src/transformers/transformer-kind.ts b/src/transformers/transformer-kind.ts index 64e0847d..728b616d 100644 --- a/src/transformers/transformer-kind.ts +++ b/src/transformers/transformer-kind.ts @@ -1 +1 @@ -export type TransformerKind = "md" | "markdown" | "json" | "vscode" | "debug"; +export type TransformerKind = "md" | "markdown" | "json" | "json2" | "vscode" | "debug";