@celement/cli
Table of Contents
Introduction
This is a lightweight CLI tool that parses your JavaScript/TypeScript files and collects
metadata about your Web Components via plugins, which can then
be used again via plugins to transform it into whatever output you require such as JSON or
Markdown.
Metadata includes the properties, methods, events, cssprops, cssparts, slots and more about each
component. This tool works completely off plugins so there's not much underlying logic out of the
box. Pick, use and create what you need. See below on how to create your own plugin.
The following are some plugins available out of the box:
- Lit: Discovers metadata about your Lit components (only TS support atm).
Follows complete heritage tree (mixins/subclasses/interfaces).
- JSON: Transforms component metadata into JSON format.
- Markdown: Transforms component metadata into markdown.
- VSCode: Transforms component metadata into VSCode Custom Data.
Install
$: npm install @celement/cli -D
Usage
First create a celement.config.ts
file at the root your project directory and include some plugins...
import {
litPlugin,
jsonPlugin,
markdownPlugin,
vscodeHtmlDataPlugin,
} from '@celement/cli';
export default [
litPlugin(),
jsonPlugin(),
markdownPlugin(),
vscodeHmlDataPlugin(),
];
Next simply run the analyze
command...
$: celement analyze src/**/*.ts
For more information call celement analyze -h
to see what arguments are available.
Documenting Components
Here's an example of how you can document a component when using the litPlugin
...
export class MyElement extends LitElement {
@property({ type: Boolean }) hidden = false;
@property({ attribute: 'size' }) sizing: 'small' | 'big' = 'small';
get currentSize(): 'small' | 'big' {
return this.size;
}
onShow() {
}
protected internalMethod() {
}
}
Custom Metadata
You might've noticed that some information such as events were missing, and there may
potentially be other information you'd like to include in the final output. In these cases it'd be
best to create your own plugin and extract the information you need. Depending on what you're
gathering the postbuild
and postlink
plugin lifecycle steps are generally the best time to do
this. See the next section for more information on how you can go about achieving this with
custom plugins.
Plugins
export interface Plugin<ComponentRootNodeType extends Node = Node> {
name: string;
init?(program: Program): Promise<void>;
discover?(sourceFile: SourceFile): Promise<ComponentRootNodeType[]>;
build?(node: ComponentRootNodeType): Promise<ComponentMeta>;
postbuild?(
components: ComponentMeta[],
sourceFiles: SourceFile[],
): Promise<ComponentMeta[]>;
link?(
component: ComponentMeta,
heritage: HeritageMeta,
): Promise<ComponentMeta>;
postlink?(
components: ComponentMeta[],
sourceFiles: SourceFile[],
): Promise<ComponentMeta[]>;
transform?(components: ComponentMeta[], fs: PluginFs): Promise<void>;
destroy?(): Promise<void>;
}
Custom Plugin
Assume you're registering your component's in a separate .ts
file so that when someone
imports my-library/button/my-button.ts
it'll register the MyButton
custom element in the
Window registry under the tag name my-button
.
import '../theme/my-theme.js';
import { MyButton } from './MyButton';
window.customElements.define('my-button', MyButton);
This plugin will discover component dependencies by looking at the import declarations at
the top of said file, and seeing if they reference other component registration files. In the
example above, the imports listed directly under the comment // Dependencies
will be discovered.
import {
litPlugin,
markdownPlugin,
Plugin,
ComponentMeta,
} from '@celement/cli';
import { escapeQuotes, isUndefined } from '@celement/cli/dist/utils';
import { SourceFile, isImportDeclaration } from 'typescript';
export default [litPlugin(), dependencyDiscoveryPlugin(), markdownPlugin()];
function dependencyDiscoveryPlugin(): Plugin {
return {
name: 'deps-discovery',
async postbuild(components, sourceFiles) {
sourceFiles.forEach(sourceFile => {
const path = sourceFile.fileName;
components.forEach(component => {
const definitionFile = `${component.tagName!}.ts`;
if (path.endsWith(definitionFile)) {
const deps = findDependencies(components, sourceFile);
component.dependencies.push(...deps);
deps.forEach(dep => {
const notFound = !dep.dependents.some(
c => c.tagName === component.tagName,
);
if (notFound) dep.dependents.push(component);
});
}
});
});
return components;
},
};
}
function findDependencies(
components: ComponentMeta[],
sourceFile: SourceFile,
): ComponentMeta[] {
const deps: ComponentMeta[] = [];
sourceFile.forEachChild(node => {
if (isImportDeclaration(node)) {
const importPath = escapeQuotes(node.moduleSpecifier.getText());
if (importPath.startsWith('../.js')) {
const dep = components.find(c => importPath.includes(c.tagName!));
if (!isUndefined(dep)) deps.push(dep);
}
}
});
return deps;
}
Prettier
Here's an example of running Prettier on the markdown files generated by
the markdownPlugin
...
import { litPlugin, markdownPlugin } from '@celement/cli';
import prettier from 'prettier';
export default [
litPlugin(),
markdownPlugin({
async transformContent(_, content) {
return prettier.format(content);
},
}),
];