import type { CallExpression, ClassDeclaration, GetAccessorDeclaration, Identifier, MethodDeclaration, Node, PropertyDeclaration, TypeChecker, } from 'typescript'; import ts from 'typescript'; import { isDecoratedClassMember, isDecoratorNamed, } from '../../utils/decorators.js'; import { getMemberName, isMemberPrivate } from '../../utils/members.js'; import { buildAnyMetaFromDocTags, buildMethodMetaFromDeclarationOrSignature, buildPropMetaFromDeclarationOrSignature, buildSlotMetaFromDocTags, buildSourceMetaFromNode, findDocTag, getDocTags, getDocumentation, RESERVED_PUBLIC_MEMBERS, } from '../../utils/meta.js'; import { escapeQuotes } from '../../utils/string.js'; import { CssPartMeta, CssPropMeta, DocTagMeta, MethodMeta, PropMeta, SlotMeta, } from '../ComponentMeta.js'; import { PluginBuilder } from '../Plugin.js'; export interface LitPropOptions { attribute?: string; reflect?: boolean; } export interface LitEventOptions { name?: string; bubbles?: boolean; composed?: boolean; } export const LIT_LIFECYCLE_METHODS = new Set([ 'createRenderRoot', 'connectedCallback', 'disconnectedCallback', 'performUpdate', 'shouldUpdate', 'willUpdate', 'update', 'render', 'firstUpdated', 'updated', ]); export type LitPluginConfig = Record; export const LIT_PLUGIN_DEFAULT_CONFIG: LitPluginConfig = {}; export async function normalizeLitPluginConfig( config: Partial, ): Promise { return { ...LIT_PLUGIN_DEFAULT_CONFIG, ...config, }; } /** * Discovers components that are built with Lit and builds their respective `ComponentMeta`. * This plugin will follow the complete heritage tree including mixins, subclasses and interfaces * and merge them together. This will run in the `discover` and `build` plugin lifecycle * steps. For more information on how to document your components see the project * [README.md](https://github.com/celement-js/cli#documenting-components). * * **IMPORTANT:** This plugin currently only supports TypeScript, so your web components * must be declared inside `.ts` files. * * @example * ```ts * // FILE: celement.config.ts * * import { litPlugin } from '@celement/cli'; * * export default [ * litPlugin({ * // Configuration options here. * }), * ]; * ``` */ export const litPlugin: PluginBuilder< Partial, ClassDeclaration > = () => { let checker: TypeChecker; return { name: '@celement/lit', async init(program) { checker = program.getTypeChecker(); }, async discover(sourceFile) { const discovered: ClassDeclaration[] = []; function visit(node: Node) { if (ts.isClassDeclaration(node) && findTagName(node).length > 0) { discovered.push(node); } } ts.forEachChild(sourceFile, visit); return discovered; }, async build(declaration) { const identifier = declaration.name as Identifier; const source = buildSourceMetaFromNode(declaration); const docTags = getDocTags(declaration); return { node: declaration, source, documentation: getDocumentation(checker, identifier), className: identifier.escapedText as string, props: findProps(checker, declaration), methods: findMethods(checker, declaration), events: [], tagName: findTagName(declaration), docTags, cssProps: findCssProps(docTags), cssParts: findCssParts(docTags), slots: findSlots(docTags), heritage: [], dependencies: [], dependents: [], }; }, }; }; function findTagName(declaration: ClassDeclaration): string { const customElDecorator = (declaration.decorators ?? []).find( isDecoratorNamed('customElement'), ); const tagName = ( customElDecorator?.expression as CallExpression )?.arguments?.[0].getText() ?? findDocTag(getDocTags(declaration), 'tagname')?.text; return escapeQuotes(tagName ?? ''); } function findProps( checker: TypeChecker, declaration: ClassDeclaration, ): PropMeta[] { return declaration.members .filter( node => (ts.isPropertyDeclaration(node) && isDecoratedClassMember(node) && node.decorators!.find(isDecoratorNamed('property'))) || (ts.isGetAccessor(node) && !isMemberPrivate(node)), ) .map(node => buildPropMetaFromDeclarationOrSignature( checker, node as PropertyDeclaration | GetAccessorDeclaration, 'property', 'internalProperty', opts => opts ?? {}, ), ); } function findMethods( checker: TypeChecker, declaration: ClassDeclaration, ): MethodMeta[] { return declaration.members .filter(node => ts.isMethodDeclaration(node)) .filter(node => !isMemberPrivate(node)) .filter( node => !RESERVED_PUBLIC_MEMBERS.has(getMemberName(checker, node) ?? '') && !LIT_LIFECYCLE_METHODS.has(getMemberName(checker, node)!), ) .map(node => buildMethodMetaFromDeclarationOrSignature( checker, node as MethodDeclaration, ), ); } function findCssProps(tags: DocTagMeta[]): CssPropMeta[] { return buildAnyMetaFromDocTags( tags, 'cssprop', '@cssprop --bg-color - The background color of this component.', ); } function findCssParts(tags: DocTagMeta[]): CssPartMeta[] { return buildAnyMetaFromDocTags( tags, 'csspart', '@csspart container - The root container of this component.', ); } function findSlots(tags: DocTagMeta[]): SlotMeta[] { return buildSlotMetaFromDocTags(tags); }