@zeix/cem-plugin-le-truc
v0.2.1
Published
Custom Elements Manifest analyzer plugin for @zeix/le-truc factory components
Readme
@zeix/cem-plugin-le-truc
A @custom-elements-manifest/analyzer plugin that generates standards-compliant custom-elements.json manifests for components built with @zeix/le-truc.
Le Truc uses a factory pattern (defineComponent<Props>(tagName, factory)) rather than class declarations. This plugin bridges the gap so the full CEM ecosystem (editor LSP, AI coding agents, design system tooling) works out of the box.
The plugin runs within @custom-elements-manifest/analyzer (cem analyze) and does not require @pwrs/cem. The two packages are complementary, not competing — see Complementary tooling below.
Installation
bun add -D @zeix/cem-plugin-le-trucUsage
Add to custom-elements-manifest.config.mjs:
import { leTrucPlugin } from '@zeix/cem-plugin-le-truc'
import ts from 'typescript'
let typeChecker
export default {
globs: ['src/**/*.ts'],
exclude: ['**/*.test.ts'],
overrideModuleCreation({ ts, globs }) {
const program = ts.createProgram(globs, { strict: true })
typeChecker = program.getTypeChecker()
return program.getSourceFiles().filter(sf => !sf.isDeclarationFile)
},
plugins: [leTrucPlugin(() => typeChecker)],
}Run the analyzer:
npx cem analyzeThis generates custom-elements.json from your Le Truc components.
What gets extracted
| CEM field | Source |
|---|---|
| tagName | First argument of defineComponent(tagName, …) |
| name | PascalCase from tagName (basic-counter → BasicCounter) |
| description | JSDoc above the export default defineComponent(…) |
| members | Properties of the Props type argument via TypeScript type checker |
| attributes | Properties in expose({}) whose initializer is an as* call from @zeix/le-truc (imported by name or relative path), plus @attribute/@attr JSDoc tags |
| slots, events, cssParts, cssProperties | @slot, @fires, @csspart, @cssprop JSDoc tags |
| demos | @demo {url} description JSDoc tags |
The plugin also fixes up three schema-compliance gaps in the default analyzer's output: it links the default export to the synthesised declaration name (required by Reference.name), adds package: "global:" to built-in superclass references (e.g. HTMLElement stubs), and relativizes module paths against the working directory — the overrideModuleCreation boilerplate feeds ts.createProgram source files whose names are absolute, which would otherwise leak local (or CI-runner) paths into the published manifest.
JSDoc contract
/**
* A counter that increments on click. Use it for demonstrating reactive
* property updates. The host element should contain a `<button>` and a
* `<span>`; the value must be a non-negative integer.
* @slot - Default slot for button label
* @fires count-changed - Fired when the count changes
* @csspart counter - The counter container
* @cssprop --counter-color - Text color
* @demo {./examples/basic-counter.html} Interactive counter demo
*/
export default defineComponent<CounterProps>('basic-counter', ({ expose }) => {
expose({ count: asInteger() })
return []
})Property descriptions go on the Props type:
export type CounterProps = {
/** Current count value. Read from the `count` attribute at connect time. */
count: number
}Description quality
The description is free-form JSDoc, but a few conventions improve the manifest's usefulness across the CEM ecosystem — and score well in documentation-health tools like cem health:
- Explain purpose and context. Use words like use, for, when, provides to describe what the component is for, not just what it is.
- State constraints with RFC 2119 keywords. Words like must, should, avoid clarify requirements (e.g. "the
valuemust be a non-negative integer"). - Mention accessibility. If the component provides ARIA semantics, keyboard interaction, or screen-reader support, note it.
- Mention keyboard interaction. If the component handles keyboard events (Enter, Space, Arrow keys, focus management), document it.
These are recommendations, not requirements — a clear one-liner is better than keyword-stuffed prose.
@attribute annotations (connect-time attributes)
Attributes that a component reads once via host.getAttribute() at connect time — server-side configuration of stable client-side behavior — never appear in expose({}), so they cannot be detected statically. Declare them with @attribute (alias @attr), the same tag names the stock analyzer supports for class-based components:
/**
* A resizable split view.
* @attribute {'horizontal'|'vertical'} [orientation=horizontal] - Layout direction. Read once at connect time.
*/Syntax: @attribute {type} name - description, all parts except the name optional; [name=default] declares a default value. The emitted entry has no fieldName — the CEM schema's way of saying the attribute is not backed by a property. If the name collides with an expose()-derived attribute, the entries merge: the Props type remains the source of truth for type and fieldName, while the JSDoc tag contributes description and default.
@demo annotations
/**
* @demo {./examples/basic-counter.html} Interactive counter demo
* @demo {https://myapp.com/demos/counter} Hosted production demo
*/The URL in braces identifies the demo page; the trailing text is a markdown description. A demo with both a URL and a description scores full marks in cem health. The URL may be relative (published with the package) or absolute (hosted).
Complementary tooling (@pwrs/cem)
This plugin generates custom-elements.json via @custom-elements-manifest/analyzer (cem analyze). For additional tooling — consuming the manifest rather than generating it — install @pwrs/cem globally:
bun add -g @pwrs/cem@pwrs/cem provides tools that read the generated manifest:
| Command | Purpose |
|---|---|
| cem validate | Validate custom-elements.json against the CEM schema |
| cem health | Score documentation quality (descriptions, attributes, demos, etc.) |
| cem lsp | Language Server for editor autocomplete, hover docs, and diagnostics |
| cem mcp | MCP server for AI coding agents (Claude Code, etc.) |
| cem list | Query the manifest (tags, attributes, slots, events, etc.) |
| cem serve | Development server with live reload for component demos |
Note:
@pwrs/cemand@custom-elements-manifest/analyzerboth provide acemCLI but share no overlapping commands. The analyzer providescem analyze(generation);@pwrs/cemprovidesvalidate,lsp,mcp,serve, etc. (consumption). They are complementary —bun runresolves the local analyzer forcem analyze, while barecem <command>resolves the global@pwrs/cem.
