@cl-mcp/analyzer
v1.0.0
Published
Build-time component analysis tools for Angular component libraries
Maintainers
Readme
How @cl-mcp/analyzer Works — A Complete Walkthrough
This document explains, step by step, how the analyzer ingests an Angular component library source tree and produces component-metadata.json (schema v4.1 — additive bump from v4.0 adding StorybookExample.usedComponentRefs and optional ContentSlotAnalysis.selectorAlternates). It covers every configurable option, the full pipeline, each extraction stage, the semantic-relations model, and the output shape.
Scope: the analyzer package (
packages/analyzer/). The MCP server that consumes the JSON is out of scope.
1. High-Level Model
The analyzer is a build-time AST pipeline. It does not run Angular; it reads TypeScript sources with the TypeScript compiler API, combines that with pattern matching over templates, Storybook files, and JSDoc, and emits a single structured JSON file.
Angular lib sources (+ optional Storybook + optional docs)
│
▼
TypeScript compiler API (ts.Program + TypeChecker)
│
▼
Per-component extraction passes (AST + regex)
│
▼
Cross-component passes (import graph, co-occurrence, related)
│
▼
component-metadata.json (schema v4.1)The CLI entrypoint is src/cli/generate-metadata.ts. The Angular AST core is src/analyzers/angular/angular-analyzer.ts. Everything else under src/shared/ is support (cache, graph, validator, diagnostics, TS helpers).
2. CLI: Every Configurable Option
Command:
node packages/analyzer/dist/cli/generate-metadata.js \
--framework angular \
--path <library-source-path> \
[--package <name>] \
[--prefix <selector-prefix>] \
[--import-prefix <subpath-prefix>] \
[--storybook <path>] \
[--docs <path>] \
[--output <path>] \
[--allow-partial]| Flag | Required | Default | Purpose |
|------|----------|---------|---------|
| --framework | no | "angular" | Which framework analyzer to use. Only angular is implemented; anything else is rejected. |
| --path | yes | — | Absolute or relative path to the root of the component library source tree. Resolved to an absolute path. |
| --package | no | basename of --path | Package name used to synthesize the default --import-prefix. |
| --prefix | no | "" (empty) | CSS-selector prefix filter (e.g. mat-). Components whose selectors do not start with this prefix, or whose selectors start with test- / storybook-, are filtered from the selector map. |
| --import-prefix | no | <package>/ | Subpath import prefix used to detect sibling component imports when building the import graph (e.g. @angular/material/). |
| --storybook | no | unset | Path to a Storybook source tree. When provided, StorybookExtractor mines usage examples and powers the "often-used-with" relation. |
| --docs | no | unset | Path to a single markdown file. Its entire content is embedded as libraryDocumentation in the output. |
| --output | no | ./component-metadata.json | Where to write the JSON. Resolved to an absolute path; parent directory is created. |
| --allow-partial | no | false | If set, exit code is 0 even when error-severity diagnostics were collected. Without it, any error exits 1. |
Argument parsing is in src/cli/generate-metadata.ts:88 (parseArgs). Boolean flags (only --allow-partial) do not consume the next token; value flags take the next token verbatim.
2.1 How Defaults Cascade
packageName←--package??path.basename(libraryPath)importPrefix←--import-prefix??${packageName}/selectorPrefix←--prefix??""outputPath←--output??./component-metadata.json
2.2 What You Cannot Configure (Currently)
- Framework: only Angular.
FrameworkAnalyzer(seesrc/analyzers/analyzer.interface.ts) is designed for other frameworks, but no implementations exist. - Component discovery rules: hard-coded walk of the library path.
- Selector-filter exclusions:
test-andstorybook-prefix filters are hard-coded. - "Non-component" subpackage list for the import graph is hard-coded:
core,cdk,theming,prebuilt-themes,schematics,testing(seesrc/shared/import-graph.ts:22). - Semantic relations: there are no keyword maps, synonym lists, or tunable similarity thresholds. Relatedness is computed from imports + Storybook co-occurrence only (see §7).
3. Pipeline: What Happens in Order
The main() function in src/cli/generate-metadata.ts:683 orchestrates everything. Phases below match that function's structure.
Phase 1 — Setup
- Parse CLI args.
- Validate
framework === "angular", otherwise fail. - Instantiate
DiagnosticsCollector(errors/warnings aggregator). - Instantiate
AngularAstAnalyzer. - Discover component directories under
--path. - Recursively collect every
.tsfile (excluding*.spec.*). - Create
ts.Programwith ES2022 target so theTypeCheckeris available for literal-union resolution. - Hand the program to the analyzer (
setTypeChecker(program)).
Phase 2 — Source file cache
SourceFileCache (src/shared/source-file-cache.ts) is pre-populated with every ts.SourceFile from the program. All downstream passes reuse these cached nodes rather than re-parsing from disk.
Phase 3 — Library-level metadata
- If
--docsis set and the file exists, its contents are embedded underlibraryDocumentation. - If
--storybookis set and the directory exists, aStorybookExtractoris constructed (used per component in Phase 5).
Phase 4 — Import graph (first cross-component pass)
buildImportGraph() (src/shared/import-graph.ts:39) walks every component directory, reads imports from each .ts file, and builds two indexes:
imports[component] = [...otherComponents]— what this component pulls in.importedBy[component] = [...otherComponents]— reverse index.
An import counts as a sibling-component import if either:
- its module specifier starts with
--import-prefix(e.g.@angular/material/button), or - it is a relative path that resolves into another component directory under the library root.
Phase 5 — Per-component analysis
For each discovered component directory, analyzeComponent() runs these sub-steps in order (src/cli/generate-metadata.ts:245):
- AST extraction —
analyzer.analyzeFile()for every non-spec.ts. See §4. - Inheritance —
analyzeInheritance()per class. See §4.5. - Content projection —
analyzeContentProjection()per file. See §4.6. - Deprecation —
extractDeprecation()on the main component class. See §4.7. - Config tokens —
resolveConfigToken()on files matching-config.or.token.. See §4.8. - Path normalization — absolute paths in the analysis are rewritten to relative paths before serialization.
- Storybook examples —
storybookExtractor.extractStories(componentName)if enabled. See §5. - Import dependencies —
resolveDependencies()turns graph entries into{ required, optional, providers }. - README — if
README.mdexists in the component directory, its content is embedded. - LLM summary —
generateLLMSummary()produces a short human-readable blurb (heuristic, not ML). - Common patterns —
extractCommonPatterns()lists detected structural patterns (FormControl, content slots, etc.).
Each sub-step is guarded: failures are appended to the DiagnosticsCollector and do not abort the pipeline.
Phase 6 — Related components (second cross-component pass)
buildStorybookCooccurrences()— walks every Storybook example template, records which prefixed selectors appear together, producesMap<component, coOccurringComponents>.findRelatedComponents()— per component, combines import-based "requires" edges with co-occurrence-based "often-used-with" edges.
Phase 7 — Selector map
generateSelectorMap() (src/cli/generate-metadata.ts:629) builds a flat Record<selector, SelectorQuickInfo> for fast lookup by the MCP server. It applies the prefix filters described in §2.
Phase 8 — Write + exit
- Create the output directory if missing.
- Attach
diagnosticsarray to the metadata object. - Write JSON.
- Exit
1ifcollector.hasErrors()and--allow-partialis not set; otherwise0.
4. Angular AST Core (angular-analyzer.ts)
4.1 AST walk strategy
AngularAstAnalyzer.analyzeFile(filePath) (src/analyzers/angular/angular-analyzer.ts:136):
- Prefer the
SourceFilefrom the sharedts.Program(needed for theTypeChecker). - If absent, fall back to a fresh
ts.createSourceFile()parse (type-checker-dependent features will degrade gracefully). - Walk depth-first with
ts.forEachChild, dispatching by node kind:ClassDeclaration→analyzeClass()(routes to component/directive/service/pipe by decorator)- exported
InterfaceDeclaration/TypeAliasDeclaration/EnumDeclaration/FunctionDeclaration→ their own analyzers
The analyzer mutates a single FileAnalysis object during the walk and returns it.
4.2 Component / directive extraction
analyzeComponentOrDirective() handles @Component and @Directive. Per class, it extracts:
- Decorator metadata (
ComponentMetadata):selector, inlinetemplateXORtemplateUrl,styleUrls,standalone,changeDetection,encapsulation,exportAs,host,hostDirectives, etc. Decorator arguments are evaluated byevaluateExpression(src/shared/ts-util.ts:85) — a hardened literal evaluator (strings, numbers, booleans, arrays, plain object literals; complex expressions fall back to.getText()). - Inputs: from
@Input()on property declarations, getters, setters, and methods; plus signal inputs (input(),input.required()). - Outputs: from
@Output()onEventEmitter<T>properties and signal outputs (output()). - Public methods: non-private, non-lifecycle methods with parameters and return type.
- Lifecycle hooks: matched against a hard-coded set (
ngOnInit,ngOnDestroy,ngAfterViewInit, etc.). - Constructor dependencies: parameter names, types, access modifiers, decorators (
@Inject,@Optional,@SkipSelf,@Self,@Host). - JSDoc description: the free-text block on the class.
4.3 Input extraction details
Each input becomes a discriminated union on required:
type InputProperty =
| { required: true; name; type; typeResolved; alias?; description?; transform?; resolvedValues }
| { required: false; name; type; typeResolved; alias?; description?; transform?; resolvedValues; defaultValue? };Rules:
required: trueis set only when the decorator options explicitly containrequired: true(or the signal factory isinput.required()). Type-annotation!alone is not enough.defaultValueis read from the initializer expression.typeResolvedistruewhen the type annotation exists and was parsed verbatim;falsewhen inference from initializer failed or type is unresolvable.- Getter/setter pairs with
@Input()on both are deduplicated — first occurrence wins, with a preference for the entry whosetypeResolvedistrue.
4.4 Literal-union resolution
When an input's type is a string/number literal union ('primary' | 'accent' | 'warn'), the analyzer attempts to resolve it to a concrete list:
- Requires the
TypeChecker(i.e. the file is part of thets.Program). - Calls
checker.getTypeFromTypeNode(typeNode)and enumerates union members. - Keeps string and number literals; ignores
undefined/null. - If any member is not a literal (e.g. a type reference), marks
resolvedValues.partial = true.
Failures emit diagnostics:
union-resolution-no-checker— analyzer has no program.union-resolution-not-in-program— the file was parsed outside the program (cache fallback).
4.5 Inheritance
analyzeInheritance(className, sourceFile, cache, diagnostics) (src/analyzers/angular/angular-analyzer.ts:1181):
- Finds the class; walks
heritageClausesforextends. - Resolves the base class's import specifier via
resolveImportPath():- Matches named imports in the current file.
- Resolves relative paths to on-disk files.
- Resolves subpath package imports against the library root +
--import-prefix. - Follows re-exports through barrel files (
index.ts), withMAX_DEPTH = 10to avoid cycles.
- Re-analyzes the base class's source file (via cache) and harvests its inputs/outputs.
- Also captures
implementsclauses asmixins: string[].
Output is stored under inheritance[ClassName] = { baseClass, baseClassPath, inheritedInputs, inheritedOutputs, mixins }. A top-level inheritanceResolved: boolean is set to false if any import could not be resolved (produces diagnostic inheritance-unresolved-import).
4.6 Content projection (ng-content)
analyzeContentProjection() reads the template (inline string or templateUrl from disk) and parses it with @angular/compiler's parseTemplate via the shared wrapper in src/shared/template-parser.ts. A per-run TemplateParseCache (keyed by sha1(template)+sourceUrl) guarantees each unique template is parsed exactly once across all three template-consuming call-sites (content projection, template validator, storybook used-component extraction).
The walker descends every Angular 17+ control-flow block (@if/@for/@switch/@defer) plus legacy *ngIf/*ngFor wrappers, so <ng-content> nodes nested inside control-flow are honored — unlike the previous regex scan.
For each <ng-content> slot:
name=ngProjectAsvalue if present, else theselectattribute value, else"default".selector= theselectvalue (undefined for the default slot).required=truewhen therequiredattribute is present on the node (honest: the regex implementation always reportedfalse).multiple: !selector— an unselected slot accepts any content.selectorAlternates?(v4.1, optional) — populated for compound selectors likeselect="[foo], [bar]"with the comma-split list.
Duplicate slots are deduped by primary selector string. Commented-out <!-- <ng-content /> --> are correctly ignored (the regex mis-counted them).
On parse failure with no recovered nodes, a template-parse-failed warn-severity diagnostic is emitted and an empty slot list is returned. When the parser reports errors but still produces nodes, the diagnostic is emitted and the walker proceeds over the recovered tree (soft failure). The existing MAX_TEMPLATE_LENGTH (100 KB) guard in the validator short-circuits before the parser; analyzeContentProjection relies on library-source templates being well below that bound.
4.7 Deprecation
extractDeprecation() reads @deprecated from:
- JSDoc tags via
ts.getJSDocTags(), or - Raw leading comment text (fallback for non-JSDoc-style comments).
The comment text is mined for four pieces with regex heuristics:
since—/since\s+v?(\d+\.\d+\.\d+)/iremoveIn—/(?:removed?\s+in|remove\s+(?:in|at))\s+v?(\d+\.\d+\.\d+)/ireplacement—/(?:use|replaced?\s+(?:by|with))\s+(\S+)\s+instead/ireason— full raw comment text
Missing fields are simply omitted.
4.8 Config tokens
resolveConfigToken() targets Angular's common config pattern:
export interface MatFooConfig { ... }
export const MAT_FOO_DEFAULT_OPTIONS = new InjectionToken<MatFooConfig>('...');
export const DEFAULT_MAT_FOO_CONFIG: MatFooConfig = { ... };Algorithm:
- Collect every interface in the file →
TypeMember[]with{ name, type, optional, jsDoc }. - Collect every
new InjectionToken<T>(...)binding and everyDEFAULT_*_CONFIG/*_DEFAULT_CONFIGconst. - Pair each token with the interface named in its type argument; pair with the default-values const by naming convention.
Only files matching *-config.* or *.token.* are scanned (from src/cli/generate-metadata.ts:357).
5. Storybook Extractor
StorybookExtractor.extractStories(componentName) (src/analyzers/angular/storybook-extractor.ts:57):
- Resolves the component's stories directory under
<storybook>/src/lib/<componentName>(with singular/plural and dash-stripped variants). - Wrapper stories: parses every
*.story.tsfile — these are@Componentwrapper classes whosetemplateembeds real usage. Templates are sanitized (customsbFs*attributes stripped), then parsed with the sharedTemplateParseCache(v4.1) to walk everyTmplAstElement(tag names) and its attributes (potential attribute-directive selectors). Only prefix-matching element tags flow intousedComponents; the full raw token set ({ elements, attributes }) is parked inStorybookExtractor.rawTemplateTokensfor the CLI post-pass. A regex-based fallback still runs when the parser is unavailable (e.g. older test harnesses that instantiate the extractor without a cache). - Main stories file: parses the
*.stories.tsfile for the CSFmetadefault export and each named story export, harvestingargs,argTypes, and — if present — therender: (args) => ({ template })template. - Matches story metadata to wrapper examples by normalized story name; unmatched stories become standalone examples.
Output per example:
{ storyName, filePath, template, args, argTypes?, usedComponents, usedComponentRefs? }usedComponents: string[]— flat list of library selectors referenced by the story (back-compat). As of v4.1 this includes attribute-directive selectors (e.g.matTooltip,cdkDrag) resolved against the library's selector map in a CLI post-pass, not just element tags.usedComponentRefs?: { selector, kind: 'element' | 'attribute' }[]— v4.1 additive field that preserves the element-vs-attribute distinction.often-used-withedge weighting is blind tokind, but downstream consumers (MCP server, docs tooling) can use it.
The CLI phase order is: per-component analysis → selector-map generation → storybook-token resolution pass → co-occurrence build → related-components. Token resolution runs against the full selector map, so attribute directives defined anywhere in the library correctly resolve even when their host story lives in a different component's directory.
5.1 Post-pass: attribute-directive resolution
Attribute-directive selectors (e.g. matTooltip, cdkDrag) cannot be identified during per-component analysis — they require the complete library selector map, which does not exist until every component has been analyzed. Consider a story template like:
<button mat-button matTooltip="Save" cdkDrag>Save</button>The parser sees one element (<button>) and four attribute tokens (mat-button, matTooltip, cdkDrag, plus the literal Save). Nothing in the parse output tells the extractor which attributes are library selectors and which are plain HTML. Resolving this requires a library-wide reverse lookup.
The pipeline handles it in three steps:
- Collect raw tokens — During Storybook extraction,
StorybookExtractorwalks every template with the sharedTemplateParseCacheand records{ elements, attributes }per example inrawTemplateTokens: Map<StorybookExample, RawTemplateTokens>(src/analyzers/angular/storybook-extractor.ts:70). At this stageusedComponentsis only seeded with element tags matching the configured selector prefix; attributes are parked unresolved. - Build the selector map — After every component is analyzed,
generateSelectorMap()(src/cli/generate-metadata.ts:629) produces the completeRecord<selector, SelectorQuickInfo>for the whole library. - Resolve tokens against the map —
resolveStorybookUsedComponents(examples, selectorMap, rawTokens)(src/cli/generate-metadata.ts:713, invoked in the orchestrator right after selector-map generation) iterates each example's raw attribute tokens and looks them up in the selector map. Matches are appended tousedComponents(flat, back-compat) and tousedComponentRefswithkind: "attribute". Element-tag matches are recorded withkind: "element". Compound selectors (select="[foo], [bar]") are comma-split before lookup so each alternate resolves independently.
Only after this post-pass can buildStorybookCooccurrences() and findRelatedComponents() produce correct edges — run earlier, they would silently miss every directive-based relationship (roughly 10× fewer edges on Angular-Material-class libraries). This is why the phase order in §3 places selector-map generation before co-occurrence, rather than alongside per-component writes.
6. Template Validation (template-validator.ts)
This is a separate capability (exposed to the MCP server, not part of metadata generation). Given a template string and the registered component APIs, it:
- Parses the template with
@angular/compiler.parseTemplate(via the sharedTemplateParseCache) and walks the R3 AST to collect per-element bindings:[input],[(twoWay)](deduped against its synthesizedChangeevent),(output),TextAttributes, and attribute-directive selector candidates. Angular 17+ control-flow (@if/@for/@switch/@defer) and legacy structural directives (*ngIf/*ngFor, surfaced asTmplAstTemplatehosts) are descended. - Checks each binding against the component's inputs/outputs and the registered directive inputs/outputs. Every diagnostic carries an optional
sourceSpan: { line, column, length }derived from the parser. - Produces fuzzy-match suggestions using Levenshtein distance ≤ 3.
- Enforces required inputs.
- Ignores a hard-coded allowlist of common HTML attributes (
class,id,style,href, …), Angular directives (ngIf,ngFor,formControl, …), and built-in pipes (async,date,currency, …). - Walks every
BindingPipenode in every attachedASTWithSourceexpression (interpolations, bound attributes, bound-text children) and checks built-in pipes against an arity table (e.g.slicetakes 1–2 args,jsontakes 0).
It guards against pathological inputs (> 100 KB templates are short-circuited upfront via the MAX_TEMPLATE_LENGTH constant). On parser failure with no recovered nodes, returns a single template-parse-failed warning and emits a matching diagnostic; on partial recovery, emits the diagnostic and validates against the recovered tree.
7. Semantic Relations — The Truth
A common misconception: the analyzer has a taxonomy of semantic relationships between components. It does not. There are no keyword maps, synonym lists, ontologies, embeddings, or tunable thresholds. All relatedness is derived from two structural signals:
7.1 requires — from imports
If component A imports component B (either via --import-prefix subpath import or a relative path that resolves into B's directory), an edge A requires B is emitted, with a reason string like "imported directly". The NON_COMPONENT_SUBPACKAGES allowlist suppresses internal subpackages (core, cdk, theming, prebuilt-themes, schematics, testing) so they never surface as "related".
7.2 often-used-with — from Storybook co-occurrence
Two components that appear in the same Storybook story template co-occur. The co-occurrence map is built once (Phase 6), then turned into often-used-with edges with a reason like "appears together in N Storybook example(s)".
As of v4.1 attribute directives contribute co-occurrence edges on equal footing with element components. A story template like <button mat-button matTooltip="hi">Save</button> now emits three edges (mat-button, matTooltip, and — if the story belongs to e.g. mat-dialog — the host component) rather than zero. This typically produces ~10× more edges on Angular-Material-class libraries than the pre-v4.1 regex implementation, which only saw element tags.
7.3 alternative-to — not implemented
The RelatedComponent union includes "alternative-to", but nothing in the current code produces edges of this kind. It is reserved for future use.
7.4 Other structural heuristics
These are not semantic relations but are worth knowing, because they shape the output:
- Selector-map prefix filter (
--prefix): selectors not matching the prefix — or starting withtest-/storybook-— are excluded fromselectorMap. - FormControl detection: exact-name check for inputs
formControlorformControlName. No fuzzy match. - LLM summary: counts of inputs/outputs/directives/pipes, base class names, content slots, config tokens. Pure structural synthesis.
8. Diagnostics
Every extraction pass collects non-fatal issues into DiagnosticsCollector (src/shared/diagnostics.ts). Each entry carries { severity, code, component?, file?, message, stack? }.
Codes you will see:
| Code | Meaning |
|------|---------|
| inheritance-failed | The inheritance pass threw. |
| inheritance-unresolved-import | Base class import could not be resolved. |
| content-projection-failed | ng-content parsing threw. |
| deprecation-failed | JSDoc deprecation extraction threw. |
| config-token-failed | InjectionToken resolution threw. |
| storybook-failed | Story extraction threw. |
| component-analysis-failed | Top-level per-component analysis threw. |
| union-resolution-no-checker | Literal-union resolution ran without a TypeChecker. |
| union-resolution-not-in-program | The source file was parsed outside the ts.Program. |
| anonymous-class-skipped | Component/directive/pipe/service without a class name. |
| empty-selector | Component/directive declared with no selector. |
| duplicate-selector | Two components declare the same selector. |
| template-parse-failed | @angular/compiler.parseTemplate reported errors on a template (content projection, template validator, or storybook extraction). Always severity: "warn" — never gates CI. When no nodes could be recovered, downstream extraction falls back to an empty result; when nodes are partially recovered, the walker still runs over them. |
Exit behavior:
- Any
severity: "error"→ exit1, unless--allow-partialis set. severity: "warn"→ never changes exit code.- The full diagnostics array is written into the JSON, so the MCP server/LLM can reason about gaps.
9. Output Shape (schema v4.1)
Top level:
interface ComponentMetadataFile {
version: "4.1.0";
generatedAt: string; // ISO timestamp
componentsPath: string; // relative to cwd at generation time
importPrefix?: string;
libraryDocumentation?: string; // full text of --docs file
components: Record<string, AnalyzedComponentEntry>;
selectorMap?: Record<string, SelectorQuickInfo>;
diagnostics?: readonly AnalyzerDiagnostic[];
}Per component:
interface AnalyzedComponentEntry {
kind: "analyzed" | "skipped" | "failed";
name: string;
exports: string[]; // symbols exported from the component's index.ts
files: string[]; // relative paths
readme?: string;
analysis: FileAnalysis[]; // per-file AST analysis
dependencies: {
required: string[]; // currently always []
optional: string[]; // from the import graph
providers: { required: []; optional: []; }; // currently always []
};
examples: { name; description; template?; typescript?; scss? }[]; // currently empty; Storybook examples live under storybookExamples
// Enhancement fields
inheritance?: Record<string, InheritanceInfo>;
contentProjection?: ContentSlotInfo[];
deprecation?: DeprecationInfo;
configTokens?: ConfigTokenInfo[];
storybookExamples?: StorybookExample[];
importsFrom?: string[];
relatedComponents?: RelatedComponent[];
llmSummary?: string;
commonPatterns?: string[];
inheritanceResolved: boolean;
}Per file (FileAnalysis) contains components[], directives[], pipes[], services[], exportedTypes[], exportedFunctions[].
Per component class (ComponentAnalysis) contains className, filePath, metadata, inputs[], outputs[], publicMethods[], dependencies[], lifecycleHooks[], exportedTypes[], jsDocDescription?.
SelectorQuickInfo is the compact lookup for the MCP server:
interface SelectorQuickInfo {
component: string;
type?: "component" | "directive";
mainInputs: string[]; // up to 5; required inputs get a leading '*'
mainOutputs: string[]; // up to 3
formControl?: boolean;
hasContentSlots?: boolean;
deprecated?: boolean;
configRequired?: string[];
}Types are branded (FilePath, ClassName, Selector) to prevent cross-mixing in consumers (src/types.ts).
10. Framework Abstraction
src/analyzers/analyzer.interface.ts declares:
interface FrameworkAnalyzer {
readonly framework: "angular";
analyze(libraryPath: string, options: AnalyzerOptions): Promise<ComponentMetadataFile>;
}This is the seam for future framework support. The current Angular pipeline is orchestrated directly in the CLI and does not implement this interface — it uses AngularAstAnalyzer as a low-level helper and stitches the stages itself. Adding React/Vue would mean implementing FrameworkAnalyzer for real and having the CLI dispatch by --framework.
11. Quick Reference — File → Responsibility
| File | Responsibility |
|------|----------------|
| src/cli/generate-metadata.ts | CLI parsing, pipeline orchestration, selector map, LLM summary, exit policy |
| src/analyzers/angular/angular-analyzer.ts | Core Angular AST extraction + inheritance / content-projection / deprecation / config-token helpers |
| src/analyzers/angular/storybook-extractor.ts | Parse *.story.ts wrappers and *.stories.ts CSF metadata |
| src/shared/import-graph.ts | Import graph, Storybook co-occurrence, relatedComponents |
| src/shared/template-validator.ts | Template validation for MCP validate_template tool |
| src/shared/diagnostics.ts | DiagnosticsCollector |
| src/shared/source-file-cache.ts | Shared ts.SourceFile cache |
| src/shared/ts-util.ts | Decorator-name, JSDoc, literal-eval, type-arg helpers |
| src/types.ts | Canonical metadata schema v4.1 (shared with the MCP server) |
| src/shared/template-parser.ts | @angular/compiler.parseTemplate wrapper, TemplateParseCache, and walkTemplate visitor used by all three template-consuming call-sites |
| src/analyzers/analyzer.interface.ts | Framework-agnostic analyzer interface (seam for future frameworks) |
12. Mental Model — Short Version
- Parse a library with the TS compiler API.
- For every component class: decorator metadata + inputs + outputs + methods + deps + lifecycle.
- Enhance each component with: inheritance, content slots, deprecation, config tokens, Storybook examples.
- Cross-component: import graph →
requires; Storybook co-occurrence →often-used-with. - Flatten to a selector map for fast MCP lookup.
- Write JSON. Diagnostics travel with the output. Non-zero exit on errors unless
--allow-partial.
No magic, no ML, no keyword ontologies — structural signals only, deliberately conservative.
13. End-to-End Examples
This section walks concrete inputs (Angular source) through the pipeline to concrete outputs (fragments of component-metadata.json). Every output snippet is shaped exactly as the analyzer emits it; fields not relevant to the example are elided with /* … */.
13.1 The full pipeline, one command
Say you have Angular Material checked out at /tmp/material/src/material and Storybook stories at /tmp/material-storybook/src. The canonical invocation is:
node packages/analyzer/dist/cli/generate-metadata.js \
--framework angular \
--path /tmp/material/src/material \
--package @angular/material \
--prefix mat- \
--import-prefix @angular/material/ \
--storybook /tmp/material-storybook \
--docs /tmp/material/README.md \
--output ./data/angular-material/component-metadata.jsonWhat happens:
- Discovers every component directory under
--path(e.g.button/,dialog/,form-field/). - Builds one
ts.Programcovering all non-spec.tsfiles so theTypeCheckercan resolve literal unions across files. - For each component: runs the 11 sub-steps from §3 Phase 5.
- Builds the import graph, resolves Storybook tokens against the finished selector map, then computes
relatedComponents. - Writes JSON; exits
0(or1on error unless--allow-partial).
The rest of this section shows what individual pieces of that output look like.
13.2 A minimal component
Input — button/button.component.ts:
import { Component, Input, Output, EventEmitter } from "@angular/core";
/** A styled push-button. */
@Component({
selector: "mat-button",
standalone: true,
template: `<button (click)="pressed.emit()"><ng-content /></button>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MatButton {
/** Visual variant. */
@Input() variant: "primary" | "accent" | "warn" = "primary";
/** Disable the button. */
@Input({ required: true }) disabled!: boolean;
/** Emitted on click. */
@Output() pressed = new EventEmitter<void>();
}Output — entry under components["button"] (elided):
{
"kind": "analyzed",
"name": "button",
"analysis": [{
"filePath": "button/button.component.ts",
"components": [{
"className": "MatButton",
"jsDocDescription": "A styled push-button.",
"metadata": {
"selector": "mat-button",
"standalone": true,
"changeDetection": "OnPush",
"template": "<button (click)=\"pressed.emit()\"><ng-content /></button>"
},
"inputs": [
{
"name": "variant",
"type": "'primary' | 'accent' | 'warn'",
"typeResolved": true,
"required": false,
"defaultValue": "'primary'",
"description": "Visual variant.",
"resolvedValues": { "values": ["primary", "accent", "warn"], "partial": false }
},
{
"name": "disabled",
"type": "boolean",
"typeResolved": true,
"required": true,
"description": "Disable the button."
}
],
"outputs": [
{ "name": "pressed", "type": "void", "typeResolved": true, "description": "Emitted on click." }
],
"lifecycleHooks": [],
"publicMethods": [],
"dependencies": []
}],
"directives": [], "pipes": [], "services": [],
"exportedTypes": [], "exportedFunctions": []
}],
"contentProjection": [
{ "name": "default", "multiple": true, "required": false }
],
"inheritanceResolved": true,
"dependencies": { "required": [], "optional": [], "providers": { "required": [], "optional": [] } }
}Key things to notice:
required: trueondisabledis set only because the decorator options said so; the!alone would not flip it (see §4.3).resolvedValues.valuescame from theTypeCheckerenumerating the literal union (§4.4). If the file had been outside thets.Program, you would see aunion-resolution-not-in-programdiagnostic andresolvedValues: null.- The default
<ng-content />becomes a single{ name: "default", multiple: true }slot (§4.6). jsDocDescriptionis the/** A styled push-button. */block above the class.
13.3 Signal inputs & content slots with selectors
Input — card/card.component.ts:
import { Component, input, output } from "@angular/core";
@Component({
selector: "mat-card",
standalone: true,
template: `
<header><ng-content select="[cardTitle]" /></header>
<section><ng-content /></section>
<footer><ng-content select="mat-card-actions, [cardFooter]" /></footer>
`,
})
export class MatCard {
elevation = input<number>(1);
appearance = input.required<"raised" | "outlined">();
click = output<MouseEvent>();
}Output fragments:
"inputs": [
{ "name": "elevation", "type": "number", "typeResolved": true, "required": false, "defaultValue": "1" },
{ "name": "appearance", "type": "'raised' | 'outlined'", "typeResolved": true, "required": true,
"resolvedValues": { "values": ["raised", "outlined"], "partial": false } }
],
"outputs": [
{ "name": "click", "type": "MouseEvent", "typeResolved": true }
],
"contentProjection": [
{ "name": "cardTitle", "selector": "[cardTitle]", "required": false, "multiple": false },
{ "name": "default", "required": false, "multiple": true },
{ "name": "cardFooter", "selector": "mat-card-actions, [cardFooter]", "required": false, "multiple": false,
"selectorAlternates": ["mat-card-actions", "[cardFooter]"] }
]The compound select="mat-card-actions, [cardFooter]" triggers the v4.1 selectorAlternates field (§4.6) — every comma-split alternate is preserved so downstream tools can resolve each one against the selector map.
13.4 Inheritance across packages
Input — _base/focusable.ts:
export abstract class Focusable {
@Input() tabIndex: number = 0;
@Output() focused = new EventEmitter<void>();
}Input — button/icon-button.component.ts:
import { Focusable } from "../_base/focusable";
@Component({ selector: "mat-icon-button", standalone: true, template: "..." })
export class MatIconButton extends Focusable implements OnInit {
@Input() icon!: string;
ngOnInit() {}
}Output — the inheritance map under components["button"]:
"inheritance": {
"MatIconButton": {
"baseClass": "Focusable",
"baseClassPath": "_base/focusable.ts",
"inheritedInputs": [{ "name": "tabIndex", "type": "number", "typeResolved": true, "defaultValue": "0", "required": false }],
"inheritedOutputs": [{ "name": "focused", "type": "void", "typeResolved": true }],
"mixins": ["OnInit"]
}
},
"inheritanceResolved": trueThe resolver (§4.5) followed the relative import, cached the base source file, and harvested its @Input/@Output declarations. implements OnInit is captured separately in mixins. If the import had been unresolvable (e.g. a subpath alias not configured), you would see inheritanceResolved: false plus a diagnostic:
{ "severity": "error", "code": "inheritance-unresolved-import",
"component": "button", "file": "button/icon-button.component.ts",
"message": "Could not resolve base class 'Focusable' from '../_base/focusable'" }13.5 Deprecation
Input:
/**
* @deprecated since v15.2.0. Use mat-button instead. Removed in v18.0.0.
* The flat variant was merged into the main button API.
*/
@Component({ selector: "mat-flat-button", /* … */ })
export class MatFlatButton {}Output:
"deprecation": {
"since": "15.2.0",
"removeIn": "18.0.0",
"replacement": "mat-button",
"reason": "since v15.2.0. Use mat-button instead. Removed in v18.0.0.\n The flat variant was merged into the main button API."
}All four fields come from regex heuristics over the raw comment (§4.7). If the comment only said @deprecated, you would get "deprecation": { "reason": "" } — every field except reason is optional.
13.6 Config tokens
Input — dialog/dialog-config.ts:
import { InjectionToken } from "@angular/core";
/** Options for MatDialog. */
export interface MatDialogConfig {
/** Whether clicking the backdrop closes the dialog. */
disableClose?: boolean;
/** Initial width, CSS length. */
width?: string;
}
export const MAT_DIALOG_DEFAULT_OPTIONS = new InjectionToken<MatDialogConfig>("MAT_DIALOG_DEFAULT_OPTIONS");
export const DEFAULT_MAT_DIALOG_CONFIG: MatDialogConfig = { disableClose: false, width: "80vw" };Output:
"configTokens": [{
"tokenName": "MAT_DIALOG_DEFAULT_OPTIONS",
"configInterface": "MatDialogConfig",
"defaultsConst": "DEFAULT_MAT_DIALOG_CONFIG",
"members": [
{ "name": "disableClose", "type": "boolean", "optional": true, "jsDoc": "Whether clicking the backdrop closes the dialog." },
{ "name": "width", "type": "string", "optional": true, "jsDoc": "Initial width, CSS length." }
]
}]Only files matching *-config.* or *.token.* are scanned (§4.8). The token, interface, and defaults const are paired by naming convention.
13.7 Storybook examples + attribute-directive resolution
Input — button/button.stories.ts:
export default { title: "Button", component: MatButton };
export const WithTooltip = { args: { variant: "primary" },
render: (args) => ({ template: `<button mat-button matTooltip="Save" [disabled]="false">Save</button>`, props: args })
};Output — entry under components["button"].storybookExamples:
[{
"storyName": "WithTooltip",
"filePath": "button/button.stories.ts",
"template": "<button mat-button matTooltip=\"Save\" [disabled]=\"false\">Save</button>",
"args": { "variant": "primary" },
"usedComponents": ["mat-button", "matTooltip"],
"usedComponentRefs": [
{ "selector": "mat-button", "kind": "attribute" },
{ "selector": "matTooltip", "kind": "attribute" }
]
}]Critically, matTooltip is not an element — it's an attribute. The extractor parks it in rawTemplateTokens during per-component analysis because the tooltip directive may be defined in an entirely different component directory. Only after the selector map is built does resolveStorybookUsedComponents look it up and attach it with kind: "attribute" (§5.1). Without this post-pass the tooltip edge would vanish.
13.8 Related components
Given:
button/button.component.tsimportsripple/ripple.directive.ts(relative path).- Three Storybook stories put
<mat-button>and<mat-icon>together. - Two stories put
<mat-button>withmatTooltip(a directive undertooltip/).
Output — under components["button"].relatedComponents:
[
{ "name": "ripple", "relation": "requires", "reason": "imported directly" },
{ "name": "icon", "relation": "often-used-with", "reason": "appears together in 3 Storybook example(s)" },
{ "name": "tooltip", "relation": "often-used-with", "reason": "appears together in 2 Storybook example(s)" }
]There is no semantic similarity scoring — these edges are pure structural signals (§7): import graph + Storybook co-occurrence. alternative-to is reserved in the type union but no code currently produces it.
13.9 Selector map
After all per-component analysis completes, generateSelectorMap() flattens everything into a lookup table keyed by selector. With --prefix mat-:
"selectorMap": {
"mat-button": {
"component": "button",
"type": "component",
"mainInputs": ["*disabled", "variant"],
"mainOutputs": ["pressed"],
"hasContentSlots": true
},
"mat-card": {
"component": "card",
"type": "component",
"mainInputs": ["*appearance", "elevation"],
"mainOutputs": ["click"],
"hasContentSlots": true
},
"mat-flat-button": {
"component": "button",
"type": "component",
"mainInputs": [],
"mainOutputs": [],
"deprecated": true
}
}Selectors not starting with mat-, or starting with test- / storybook-, are dropped. Required inputs get a leading * (*disabled). hasContentSlots is true whenever the component has any contentProjection entries. The MCP server uses this map as its primary lookup index.
13.10 Diagnostics
A realistic diagnostics array embedded in the output:
"diagnostics": [
{ "severity": "warn", "code": "union-resolution-not-in-program",
"component": "legacy-toggle", "file": "legacy-toggle/legacy-toggle.component.ts",
"message": "Literal union for input 'mode' could not be resolved: source file not in ts.Program." },
{ "severity": "warn", "code": "template-parse-failed",
"component": "chip", "file": "chip/chip.stories.ts",
"message": "@angular/compiler.parseTemplate reported 1 error; recovered 3 nodes." },
{ "severity": "error", "code": "inheritance-unresolved-import",
"component": "autocomplete", "file": "autocomplete/autocomplete.component.ts",
"message": "Could not resolve base class 'Overlay' from '@cdk/overlay-legacy'" }
]Behavior (§8):
- The two
warnentries do not affect the exit code. - The single
errorcauses the CLI to exit1— unless you passed--allow-partial, in which case it still exits0and consumers are expected to readdiagnosticsand decide.
13.11 Template validation (MCP-side, not part of generation)
Though template-validator.ts is invoked by the MCP server rather than during metadata generation, here is what it does given the already-generated metadata above.
Input template (from an LLM trying to use your library):
<mat-button [disabld]="true" (clickd)="go()">Save</mat-button>Output diagnostics (returned by the validate_template MCP tool):
[
{ "severity": "error", "code": "unknown-input", "selector": "mat-button",
"binding": "disabld", "message": "Unknown input 'disabld' on <mat-button>. Did you mean 'disabled'?",
"suggestion": "disabled", "sourceSpan": { "line": 0, "column": 12, "length": 7 } },
{ "severity": "error", "code": "unknown-output", "selector": "mat-button",
"binding": "clickd", "message": "Unknown output '(clickd)' on <mat-button>. Did you mean 'pressed'?",
"suggestion": "pressed", "sourceSpan": { "line": 0, "column": 28, "length": 6 } },
{ "severity": "error", "code": "missing-required-input", "selector": "mat-button",
"message": "Required input 'disabled' is not bound on <mat-button>." }
]Fuzzy suggestions come from a Levenshtein-≤ 3 search over the component's declared inputs/outputs (§6). Common HTML attributes (class, id, style, …) and built-in Angular directives are allowlisted so they never fire an "unknown" diagnostic.
13.12 End-user journey (why this exists)
To tie it together, here is the chain of causality from an LLM prompt to a correct code snippet:
- User asks the LLM: "Show me a Material card with a header and actions footer."
- MCP client calls
get_component("card")→ server readscomponents["card"]from the pre-generated JSON (§13.3 above). - LLM sees the
contentProjectionslots (cardTitle,default,cardFooterwithselectorAlternates), the requiredappearanceinput, and theelevationdefault. - LLM drafts:
<mat-card appearance="raised"> <span cardTitle>Report</span> <p>Summary…</p> <mat-card-actions><button mat-button>OK</button></mat-card-actions> </mat-card> - Client optionally calls
validate_template(§13.11) before returning the snippet — catches hallucinated inputs/outputs before they reach the user.
No runtime Angular, no fuzzy "it's probably this" — every claim the LLM makes is grounded in a structural fact the analyzer extracted at build time.
