From c568bf17d9cf39c5e49d3c6a16a19da257f8bdea Mon Sep 17 00:00:00 2001 From: Johan Groth Date: Fri, 15 Jul 2022 16:53:25 +0200 Subject: [PATCH] feat(compiler): copy dock block from component to generated types The dock block from the component itself is now copied to the generated components.d.ts file. This makes it possible for other tools to get the documentation and use it, e.g. an IDE can display the documentation for a component when it's being used --- src/compiler/types/generate-app-types.ts | 22 +++++--- .../types/generate-component-types.ts | 10 ++-- src/utils/test/util.spec.ts | 51 +++++++++++++++++++ src/utils/util.ts | 50 ++++++++++++++++++ test/end-to-end/src/components.d.ts | 32 ++++++++++++ test/karma/test-app/components.d.ts | 12 +++++ 6 files changed, 168 insertions(+), 9 deletions(-) diff --git a/src/compiler/types/generate-app-types.ts b/src/compiler/types/generate-app-types.ts index f45f86804c12..c243c8ca1810 100644 --- a/src/compiler/types/generate-app-types.ts +++ b/src/compiler/types/generate-app-types.ts @@ -1,4 +1,4 @@ -import { normalizePath } from '@utils'; +import { addDocBlock, normalizePath } from '@utils'; import { isAbsolute, relative, resolve } from 'path'; import type * as d from '../../declarations'; @@ -133,7 +133,12 @@ const generateComponentTypesFile = (config: d.Config, buildCtx: d.BuildCtx, areT c.push(`}`); c.push(`declare namespace LocalJSX {`); - c.push(...modules.map((m) => ` ${m.jsx}`)); + c.push( + ...modules.map((m) => { + const docs = components.find((c) => c.tagName === m.tagName).docs; + return addDocBlock(` ${m.jsx}`, docs, 4); + }) + ); c.push(` interface IntrinsicElements {`); c.push(...modules.map((m) => ` "${m.tagName}": ${m.tagNameAsPascal};`)); @@ -147,10 +152,15 @@ const generateComponentTypesFile = (config: d.Config, buildCtx: d.BuildCtx, areT c.push(` export namespace JSX {`); c.push(` interface IntrinsicElements {`); c.push( - ...modules.map( - (m) => - ` "${m.tagName}": LocalJSX.${m.tagNameAsPascal} & JSXBase.HTMLAttributes<${m.htmlElementName}>;` - ) + ...modules.map((m) => { + const docs = components.find((c) => c.tagName === m.tagName).docs; + + return addDocBlock( + ` "${m.tagName}": LocalJSX.${m.tagNameAsPascal} & JSXBase.HTMLAttributes<${m.htmlElementName}>;`, + docs, + 12 + ); + }) ); c.push(` }`); c.push(` }`); diff --git a/src/compiler/types/generate-component-types.ts b/src/compiler/types/generate-component-types.ts index 1f39ad44913d..9e582625dc54 100644 --- a/src/compiler/types/generate-component-types.ts +++ b/src/compiler/types/generate-component-types.ts @@ -1,4 +1,4 @@ -import { dashToPascalCase, sortBy } from '@utils'; +import { addDocBlock, dashToPascalCase, sortBy } from '@utils'; import type * as d from '../../declarations'; import { generateEventTypes } from './generate-event-types'; @@ -34,7 +34,11 @@ export const generateComponentTypes = ( const jsxAttributes = attributesToMultiLineString([...propAttributes, ...eventAttributes], true, areTypesInternal); const element = [ - ` interface ${htmlElementName} extends Components.${tagNameAsPascal}, HTMLStencilElement {`, + addDocBlock( + ` interface ${htmlElementName} extends Components.${tagNameAsPascal}, HTMLStencilElement {`, + cmp.docs, + 4 + ), ` }`, ` var ${htmlElementName}: {`, ` prototype: ${htmlElementName};`, @@ -46,7 +50,7 @@ export const generateComponentTypes = ( tagName, tagNameAsPascal, htmlElementName, - component: ` interface ${tagNameAsPascal} {\n${componentAttributes} }`, + component: addDocBlock(` interface ${tagNameAsPascal} {\n${componentAttributes} }`, cmp.docs, 4), jsx: ` interface ${tagNameAsPascal} {\n${jsxAttributes} }`, element: element.join(`\n`), }; diff --git a/src/utils/test/util.spec.ts b/src/utils/test/util.spec.ts index cdc6fe702c9f..6db49ba439af 100644 --- a/src/utils/test/util.spec.ts +++ b/src/utils/test/util.spec.ts @@ -200,4 +200,55 @@ describe('util', () => { }); }); }); + + describe('addDocBlock', () => { + let str: string; + let docs: d.CompilerJsDoc; + + beforeEach(() => { + str = 'interface Foo extends Components.Foo, HTMLStencilElement {'; + docs = { + tags: [{ name: 'deprecated', text: 'only for testing' }], + text: 'Lorem ipsum', + }; + }); + + it('adds a doc block to the string', () => { + expect(util.addDocBlock(str, docs)).toEqual(`/** + * Lorem ipsum + * @deprecated only for testing + */ +interface Foo extends Components.Foo, HTMLStencilElement {`); + }); + + it('indents the doc block correctly', () => { + str = ' ' + str; + expect(util.addDocBlock(str, docs, 4)).toEqual(` /** + * Lorem ipsum + * @deprecated only for testing + */ + interface Foo extends Components.Foo, HTMLStencilElement {`); + }); + + it('excludes the @internal tag', () => { + docs.tags.push({ name: 'internal' }); + expect(util.addDocBlock(str, docs).includes('@internal')).toBeFalsy(); + }); + + it('excludes empty lines', () => { + docs.text = ''; + str = ' ' + str; + expect(util.addDocBlock(str, docs, 4)).toEqual(` /** + * @deprecated only for testing + */ + interface Foo extends Components.Foo, HTMLStencilElement {`); + }); + + it.each([[null], [undefined], [{ tags: [], text: '' }]])( + 'does not add a doc block when docs are empty (%j)', + (docs) => { + expect(util.addDocBlock(str, docs)).toEqual(str); + } + ); + }); }); diff --git a/src/utils/util.ts b/src/utils/util.ts index ba5ae151cb39..69c1b1ec570a 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -2,6 +2,8 @@ import type * as d from '../declarations'; import { dashToPascalCase, isString, toDashCase } from './helpers'; import { buildError } from './message-utils'; +const SUPPRESSED_JSDOC_TAGS: string[] = ['internal']; + export const createJsVarName = (fileName: string) => { if (isString(fileName)) { fileName = fileName.split('?')[0]; @@ -72,6 +74,54 @@ ${docs.tags .join('\n')}`.trim(); } +/** + * Adds a doc block to a string + * @param str the string to add a doc block to + * @param docs the compiled JS docs + * @param indentation number of spaces to indent the block with + * @returns the doc block + */ +export function addDocBlock(str: string, docs?: d.CompilerJsDoc, indentation: number = 0): string { + if (!docs) { + return str; + } + + return [formatDocBlock(docs, indentation), str].filter(Boolean).join(`\n`); +} + +/** + * Formats the given compiled docs to a JavaScript doc block + * @param docs the compiled JS docs + * @param indentation number of spaces to indent the block with + * @returns the formatted doc block + */ +function formatDocBlock(docs: d.CompilerJsDoc, indentation: number = 0): string { + const textDocs = getDocBlockLines(docs); + if (!textDocs.filter(Boolean).length) { + return ''; + } + + const spaces = new Array(indentation + 1).join(' '); + + return [spaces + '/**', ...textDocs.map((line) => spaces + ` * ${line}`), spaces + ' */'].join(`\n`); +} + +/** + * Get all lines part of the doc block + * @param docs the compiled JS docs + * @returns list of lines part of the doc block + */ +function getDocBlockLines(docs: d.CompilerJsDoc): string[] { + return [ + ...docs.text.split(lineBreakRegex), + ...docs.tags + .filter((tag) => !SUPPRESSED_JSDOC_TAGS.includes(tag.name)) + .map((tag) => `@${tag.name} ${tag.text || ''}`.split(lineBreakRegex)), + ] + .flat() + .filter(Boolean); +} + /** * Retrieve a project's dependencies from the current build context * @param buildCtx the current build context to query for a specific package diff --git a/test/end-to-end/src/components.d.ts b/test/end-to-end/src/components.d.ts index ff6b2623e32f..6852676297be 100644 --- a/test/end-to-end/src/components.d.ts +++ b/test/end-to-end/src/components.d.ts @@ -14,6 +14,11 @@ export namespace Components { interface CarDetail { "car": CarData; } + /** + * Component that helps display a list of cars + * @slot header - The slot for the header content. + * @part car - The shadow part to target to style the car. + */ interface CarList { "cars": CarData[]; "selected": CarData; @@ -47,6 +52,9 @@ export namespace Components { } interface PrerenderCmp { } + /** + * @virtualProp mode - Mode + */ interface PropCmp { "first": string; "lastName": string; @@ -92,6 +100,11 @@ declare global { prototype: HTMLCarDetailElement; new (): HTMLCarDetailElement; }; + /** + * Component that helps display a list of cars + * @slot header - The slot for the header content. + * @part car - The shadow part to target to style the car. + */ interface HTMLCarListElement extends Components.CarList, HTMLStencilElement { } var HTMLCarListElement: { @@ -164,6 +177,9 @@ declare global { prototype: HTMLPrerenderCmpElement; new (): HTMLPrerenderCmpElement; }; + /** + * @virtualProp mode - Mode + */ interface HTMLPropCmpElement extends Components.PropCmp, HTMLStencilElement { } var HTMLPropCmpElement: { @@ -225,6 +241,11 @@ declare namespace LocalJSX { interface CarDetail { "car"?: CarData; } + /** + * Component that helps display a list of cars + * @slot header - The slot for the header content. + * @part car - The shadow part to target to style the car. + */ interface CarList { "cars"?: CarData[]; "onCarSelected"?: (event: CarListCustomEvent) => void; @@ -257,6 +278,9 @@ declare namespace LocalJSX { } interface PrerenderCmp { } + /** + * @virtualProp mode - Mode + */ interface PropCmp { "first"?: string; "lastName"?: string; @@ -304,6 +328,11 @@ declare module "@stencil/core" { "app-root": LocalJSX.AppRoot & JSXBase.HTMLAttributes; "build-data": LocalJSX.BuildData & JSXBase.HTMLAttributes; "car-detail": LocalJSX.CarDetail & JSXBase.HTMLAttributes; + /** + * Component that helps display a list of cars + * @slot header - The slot for the header content. + * @part car - The shadow part to target to style the car. + */ "car-list": LocalJSX.CarList & JSXBase.HTMLAttributes; "dom-api": LocalJSX.DomApi & JSXBase.HTMLAttributes; "dom-interaction": LocalJSX.DomInteraction & JSXBase.HTMLAttributes; @@ -316,6 +345,9 @@ declare module "@stencil/core" { "method-cmp": LocalJSX.MethodCmp & JSXBase.HTMLAttributes; "path-alias-cmp": LocalJSX.PathAliasCmp & JSXBase.HTMLAttributes; "prerender-cmp": LocalJSX.PrerenderCmp & JSXBase.HTMLAttributes; + /** + * @virtualProp mode - Mode + */ "prop-cmp": LocalJSX.PropCmp & JSXBase.HTMLAttributes; "slot-cmp": LocalJSX.SlotCmp & JSXBase.HTMLAttributes; "slot-cmp-container": LocalJSX.SlotCmpContainer & JSXBase.HTMLAttributes; diff --git a/test/karma/test-app/components.d.ts b/test/karma/test-app/components.d.ts index d9b069a775ac..0ea66ad381c8 100644 --- a/test/karma/test-app/components.d.ts +++ b/test/karma/test-app/components.d.ts @@ -226,6 +226,9 @@ export namespace Components { } interface ShadowDomBasicRoot { } + /** + * @virtualProp {string} colormode - The mode determines which platform styles to use. + */ interface ShadowDomMode { /** * The mode determines which platform styles to use. @@ -868,6 +871,9 @@ declare global { prototype: HTMLShadowDomBasicRootElement; new (): HTMLShadowDomBasicRootElement; }; + /** + * @virtualProp {string} colormode - The mode determines which platform styles to use. + */ interface HTMLShadowDomModeElement extends Components.ShadowDomMode, HTMLStencilElement { } var HTMLShadowDomModeElement: { @@ -1490,6 +1496,9 @@ declare namespace LocalJSX { } interface ShadowDomBasicRoot { } + /** + * @virtualProp {string} colormode - The mode determines which platform styles to use. + */ interface ShadowDomMode { /** * The mode determines which platform styles to use. @@ -1813,6 +1822,9 @@ declare module "@stencil/core" { "shadow-dom-array-root": LocalJSX.ShadowDomArrayRoot & JSXBase.HTMLAttributes; "shadow-dom-basic": LocalJSX.ShadowDomBasic & JSXBase.HTMLAttributes; "shadow-dom-basic-root": LocalJSX.ShadowDomBasicRoot & JSXBase.HTMLAttributes; + /** + * @virtualProp {string} colormode - The mode determines which platform styles to use. + */ "shadow-dom-mode": LocalJSX.ShadowDomMode & JSXBase.HTMLAttributes; "shadow-dom-mode-root": LocalJSX.ShadowDomModeRoot & JSXBase.HTMLAttributes; "shadow-dom-slot-basic": LocalJSX.ShadowDomSlotBasic & JSXBase.HTMLAttributes;