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;