@brad-frost-web/eddie-brain 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -0
- package/dist/analyze/drift-detector.d.ts +30 -0
- package/dist/analyze/drift-detector.d.ts.map +1 -0
- package/dist/analyze/drift-detector.js +310 -0
- package/dist/analyze/drift-detector.js.map +1 -0
- package/dist/analyze/health-scorer.d.ts +71 -0
- package/dist/analyze/health-scorer.d.ts.map +1 -0
- package/dist/analyze/health-scorer.js +420 -0
- package/dist/analyze/health-scorer.js.map +1 -0
- package/dist/analyze/index.d.ts +11 -0
- package/dist/analyze/index.d.ts.map +1 -0
- package/dist/analyze/index.js +11 -0
- package/dist/analyze/index.js.map +1 -0
- package/dist/analyze/naming-validator.d.ts +99 -0
- package/dist/analyze/naming-validator.d.ts.map +1 -0
- package/dist/analyze/naming-validator.js +430 -0
- package/dist/analyze/naming-validator.js.map +1 -0
- package/dist/analyze/slot-contract-validator.d.ts +68 -0
- package/dist/analyze/slot-contract-validator.d.ts.map +1 -0
- package/dist/analyze/slot-contract-validator.js +232 -0
- package/dist/analyze/slot-contract-validator.js.map +1 -0
- package/dist/analyze/token-validator.d.ts +62 -0
- package/dist/analyze/token-validator.d.ts.map +1 -0
- package/dist/analyze/token-validator.js +348 -0
- package/dist/analyze/token-validator.js.map +1 -0
- package/dist/cli/brain.d.ts +12 -0
- package/dist/cli/brain.d.ts.map +1 -0
- package/dist/cli/brain.js +641 -0
- package/dist/cli/brain.js.map +1 -0
- package/dist/cli/formatters/json.d.ts +15 -0
- package/dist/cli/formatters/json.d.ts.map +1 -0
- package/dist/cli/formatters/json.js +18 -0
- package/dist/cli/formatters/json.js.map +1 -0
- package/dist/cli/formatters/terminal.d.ts +19 -0
- package/dist/cli/formatters/terminal.d.ts.map +1 -0
- package/dist/cli/formatters/terminal.js +125 -0
- package/dist/cli/formatters/terminal.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/data/governance-rules.json +94 -0
- package/dist/governance/audit-log.d.ts +17 -0
- package/dist/governance/audit-log.d.ts.map +1 -0
- package/dist/governance/audit-log.js +44 -0
- package/dist/governance/audit-log.js.map +1 -0
- package/dist/governance/index.d.ts +8 -0
- package/dist/governance/index.d.ts.map +1 -0
- package/dist/governance/index.js +8 -0
- package/dist/governance/index.js.map +1 -0
- package/dist/governance/permissions.d.ts +26 -0
- package/dist/governance/permissions.d.ts.map +1 -0
- package/dist/governance/permissions.js +75 -0
- package/dist/governance/permissions.js.map +1 -0
- package/dist/governance/rules-engine.d.ts +24 -0
- package/dist/governance/rules-engine.d.ts.map +1 -0
- package/dist/governance/rules-engine.js +111 -0
- package/dist/governance/rules-engine.js.map +1 -0
- package/dist/governance/trust-manager.d.ts +34 -0
- package/dist/governance/trust-manager.d.ts.map +1 -0
- package/dist/governance/trust-manager.js +148 -0
- package/dist/governance/trust-manager.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/knowledge-graph/component-index.d.ts +320 -0
- package/dist/knowledge-graph/component-index.d.ts.map +1 -0
- package/dist/knowledge-graph/component-index.js +1033 -0
- package/dist/knowledge-graph/component-index.js.map +1 -0
- package/dist/knowledge-graph/index.d.ts +134 -0
- package/dist/knowledge-graph/index.d.ts.map +1 -0
- package/dist/knowledge-graph/index.js +249 -0
- package/dist/knowledge-graph/index.js.map +1 -0
- package/dist/knowledge-graph/learning-history.d.ts +77 -0
- package/dist/knowledge-graph/learning-history.d.ts.map +1 -0
- package/dist/knowledge-graph/learning-history.js +187 -0
- package/dist/knowledge-graph/learning-history.js.map +1 -0
- package/dist/knowledge-graph/relationship-map.d.ts +55 -0
- package/dist/knowledge-graph/relationship-map.d.ts.map +1 -0
- package/dist/knowledge-graph/relationship-map.js +238 -0
- package/dist/knowledge-graph/relationship-map.js.map +1 -0
- package/dist/knowledge-graph/token-taxonomy.d.ts +127 -0
- package/dist/knowledge-graph/token-taxonomy.d.ts.map +1 -0
- package/dist/knowledge-graph/token-taxonomy.js +357 -0
- package/dist/knowledge-graph/token-taxonomy.js.map +1 -0
- package/dist/loop/fix-agent.d.ts +55 -0
- package/dist/loop/fix-agent.d.ts.map +1 -0
- package/dist/loop/fix-agent.js +344 -0
- package/dist/loop/fix-agent.js.map +1 -0
- package/dist/loop/index.d.ts +8 -0
- package/dist/loop/index.d.ts.map +1 -0
- package/dist/loop/index.js +8 -0
- package/dist/loop/index.js.map +1 -0
- package/dist/loop/issue-fetcher.d.ts +51 -0
- package/dist/loop/issue-fetcher.d.ts.map +1 -0
- package/dist/loop/issue-fetcher.js +188 -0
- package/dist/loop/issue-fetcher.js.map +1 -0
- package/dist/loop/observer.d.ts +42 -0
- package/dist/loop/observer.d.ts.map +1 -0
- package/dist/loop/observer.js +220 -0
- package/dist/loop/observer.js.map +1 -0
- package/dist/loop/pacer.d.ts +44 -0
- package/dist/loop/pacer.d.ts.map +1 -0
- package/dist/loop/pacer.js +90 -0
- package/dist/loop/pacer.js.map +1 -0
- package/dist/loop/reporter.d.ts +9 -0
- package/dist/loop/reporter.d.ts.map +1 -0
- package/dist/loop/reporter.js +119 -0
- package/dist/loop/reporter.js.map +1 -0
- package/dist/loop/runner.d.ts +57 -0
- package/dist/loop/runner.d.ts.map +1 -0
- package/dist/loop/runner.js +390 -0
- package/dist/loop/runner.js.map +1 -0
- package/dist/loop/types.d.ts +151 -0
- package/dist/loop/types.d.ts.map +1 -0
- package/dist/loop/types.js +22 -0
- package/dist/loop/types.js.map +1 -0
- package/dist/mcp/index.d.ts +7 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +7 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +12 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +618 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/pipeline/agent-runner.d.ts +34 -0
- package/dist/pipeline/agent-runner.d.ts.map +1 -0
- package/dist/pipeline/agent-runner.js +323 -0
- package/dist/pipeline/agent-runner.js.map +1 -0
- package/dist/pipeline/agents/accessibility-auditor.d.ts +10 -0
- package/dist/pipeline/agents/accessibility-auditor.d.ts.map +1 -0
- package/dist/pipeline/agents/accessibility-auditor.js +69 -0
- package/dist/pipeline/agents/accessibility-auditor.js.map +1 -0
- package/dist/pipeline/agents/code-reviewer.d.ts +10 -0
- package/dist/pipeline/agents/code-reviewer.d.ts.map +1 -0
- package/dist/pipeline/agents/code-reviewer.js +75 -0
- package/dist/pipeline/agents/code-reviewer.js.map +1 -0
- package/dist/pipeline/agents/code-writer.d.ts +10 -0
- package/dist/pipeline/agents/code-writer.d.ts.map +1 -0
- package/dist/pipeline/agents/code-writer.js +103 -0
- package/dist/pipeline/agents/code-writer.js.map +1 -0
- package/dist/pipeline/agents/component-architect.d.ts +13 -0
- package/dist/pipeline/agents/component-architect.d.ts.map +1 -0
- package/dist/pipeline/agents/component-architect.js +81 -0
- package/dist/pipeline/agents/component-architect.js.map +1 -0
- package/dist/pipeline/agents/index.d.ts +16 -0
- package/dist/pipeline/agents/index.d.ts.map +1 -0
- package/dist/pipeline/agents/index.js +24 -0
- package/dist/pipeline/agents/index.js.map +1 -0
- package/dist/pipeline/agents/library-researcher.d.ts +12 -0
- package/dist/pipeline/agents/library-researcher.d.ts.map +1 -0
- package/dist/pipeline/agents/library-researcher.js +85 -0
- package/dist/pipeline/agents/library-researcher.js.map +1 -0
- package/dist/pipeline/agents/quality-gate.d.ts +9 -0
- package/dist/pipeline/agents/quality-gate.d.ts.map +1 -0
- package/dist/pipeline/agents/quality-gate.js +71 -0
- package/dist/pipeline/agents/quality-gate.js.map +1 -0
- package/dist/pipeline/agents/spec-analyst.d.ts +10 -0
- package/dist/pipeline/agents/spec-analyst.d.ts.map +1 -0
- package/dist/pipeline/agents/spec-analyst.js +72 -0
- package/dist/pipeline/agents/spec-analyst.js.map +1 -0
- package/dist/pipeline/agents/story-author.d.ts +9 -0
- package/dist/pipeline/agents/story-author.d.ts.map +1 -0
- package/dist/pipeline/agents/story-author.js +65 -0
- package/dist/pipeline/agents/story-author.js.map +1 -0
- package/dist/pipeline/artifact-store.d.ts +27 -0
- package/dist/pipeline/artifact-store.d.ts.map +1 -0
- package/dist/pipeline/artifact-store.js +77 -0
- package/dist/pipeline/artifact-store.js.map +1 -0
- package/dist/pipeline/conversational-gate.d.ts +26 -0
- package/dist/pipeline/conversational-gate.d.ts.map +1 -0
- package/dist/pipeline/conversational-gate.js +122 -0
- package/dist/pipeline/conversational-gate.js.map +1 -0
- package/dist/pipeline/index.d.ts +14 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +17 -0
- package/dist/pipeline/index.js.map +1 -0
- package/dist/pipeline/iteration-tracker.d.ts +29 -0
- package/dist/pipeline/iteration-tracker.d.ts.map +1 -0
- package/dist/pipeline/iteration-tracker.js +102 -0
- package/dist/pipeline/iteration-tracker.js.map +1 -0
- package/dist/pipeline/learning-bridge.d.ts +37 -0
- package/dist/pipeline/learning-bridge.d.ts.map +1 -0
- package/dist/pipeline/learning-bridge.js +118 -0
- package/dist/pipeline/learning-bridge.js.map +1 -0
- package/dist/pipeline/orchestrator.d.ts +45 -0
- package/dist/pipeline/orchestrator.d.ts.map +1 -0
- package/dist/pipeline/orchestrator.js +473 -0
- package/dist/pipeline/orchestrator.js.map +1 -0
- package/dist/pipeline/templates/architecture.d.ts +27 -0
- package/dist/pipeline/templates/architecture.d.ts.map +1 -0
- package/dist/pipeline/templates/architecture.js +111 -0
- package/dist/pipeline/templates/architecture.js.map +1 -0
- package/dist/pipeline/templates/brief.d.ts +22 -0
- package/dist/pipeline/templates/brief.d.ts.map +1 -0
- package/dist/pipeline/templates/brief.js +121 -0
- package/dist/pipeline/templates/brief.js.map +1 -0
- package/dist/pipeline/templates/component-rules.d.ts +25 -0
- package/dist/pipeline/templates/component-rules.d.ts.map +1 -0
- package/dist/pipeline/templates/component-rules.js +93 -0
- package/dist/pipeline/templates/component-rules.js.map +1 -0
- package/dist/pipeline/templates/index.d.ts +9 -0
- package/dist/pipeline/templates/index.d.ts.map +1 -0
- package/dist/pipeline/templates/index.js +7 -0
- package/dist/pipeline/templates/index.js.map +1 -0
- package/dist/pipeline/tool-handler.d.ts +25 -0
- package/dist/pipeline/tool-handler.d.ts.map +1 -0
- package/dist/pipeline/tool-handler.js +392 -0
- package/dist/pipeline/tool-handler.js.map +1 -0
- package/dist/pipeline/types.d.ts +146 -0
- package/dist/pipeline/types.d.ts.map +1 -0
- package/dist/pipeline/types.js +27 -0
- package/dist/pipeline/types.js.map +1 -0
- package/dist/plan/action-types.d.ts +31 -0
- package/dist/plan/action-types.d.ts.map +1 -0
- package/dist/plan/action-types.js +83 -0
- package/dist/plan/action-types.js.map +1 -0
- package/dist/plan/decision-engine.d.ts +57 -0
- package/dist/plan/decision-engine.d.ts.map +1 -0
- package/dist/plan/decision-engine.js +162 -0
- package/dist/plan/decision-engine.js.map +1 -0
- package/dist/plan/index.d.ts +6 -0
- package/dist/plan/index.d.ts.map +1 -0
- package/dist/plan/index.js +6 -0
- package/dist/plan/index.js.map +1 -0
- package/dist/types.d.ts +351 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +26 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/anthropic.d.ts +15 -0
- package/dist/utils/anthropic.d.ts.map +1 -0
- package/dist/utils/anthropic.js +40 -0
- package/dist/utils/anthropic.js.map +1 -0
- package/dist/utils/id.d.ts +8 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +14 -0
- package/dist/utils/id.js.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,1033 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ComponentIndex — Auto-discover and catalog Eddie components
|
|
3
|
+
*
|
|
4
|
+
* Scans the Eddie monorepo to build a registry of all components,
|
|
5
|
+
* extracting metadata from TypeScript decorators and Storybook stories.
|
|
6
|
+
*/
|
|
7
|
+
import fg from 'fast-glob';
|
|
8
|
+
const { globSync } = fg;
|
|
9
|
+
import { existsSync, readFileSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
export class ComponentIndex {
|
|
12
|
+
components = new Map();
|
|
13
|
+
/**
|
|
14
|
+
* Build the component registry by scanning the Eddie codebase.
|
|
15
|
+
*
|
|
16
|
+
* Scans four packages:
|
|
17
|
+
*
|
|
18
|
+
* eddie-web-components — core Lit components (depth 1: <name>/<name>.ts)
|
|
19
|
+
* eddie-recipes — recipe components (depth 2: <project>/<name>/<name>.ts)
|
|
20
|
+
* eddie-pages — full-page templates (depth 2: <project>/<name>/<name>.ts)
|
|
21
|
+
* eddie-charts — chart recipe components (depth 2: <project>/<name>/<name>.ts)
|
|
22
|
+
*
|
|
23
|
+
* All four are walked by scanPackage, which handles depth-agnostic traversal
|
|
24
|
+
* and enforces the basename-matches-containing-dir convention. eddie-charts
|
|
25
|
+
* is a category recipe library: its components share the `ed-r-*` tag prefix
|
|
26
|
+
* and `ed-r-*` CSS class prefix with eddie-recipes, but live in a separate
|
|
27
|
+
* package so that the Chart.js runtime is opt-in for consumers who don't
|
|
28
|
+
* need chart surfaces.
|
|
29
|
+
*/
|
|
30
|
+
async build(rootDir) {
|
|
31
|
+
this.components.clear();
|
|
32
|
+
const webComponentsDir = join(rootDir, 'packages', 'eddie-web-components', 'components');
|
|
33
|
+
const recipesDir = join(rootDir, 'packages', 'eddie-recipes', 'recipes');
|
|
34
|
+
const pagesDir = join(rootDir, 'packages', 'eddie-pages', 'pages');
|
|
35
|
+
const chartsDir = join(rootDir, 'packages', 'eddie-charts', 'charts');
|
|
36
|
+
await this.scanPackage(webComponentsDir, 'eddie-web-components', rootDir);
|
|
37
|
+
await this.scanPackage(recipesDir, 'eddie-recipes', rootDir);
|
|
38
|
+
await this.scanPackage(pagesDir, 'eddie-pages', rootDir);
|
|
39
|
+
await this.scanPackage(chartsDir, 'eddie-charts', rootDir);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Scan a package directory for components.
|
|
43
|
+
*
|
|
44
|
+
* Walks any depth under the package root and picks up files that follow
|
|
45
|
+
* the Eddie component convention: every component file lives at
|
|
46
|
+
* `<component-dir>/<component-name>.ts` where the file's basename matches
|
|
47
|
+
* its containing directory's basename. That gives us:
|
|
48
|
+
*
|
|
49
|
+
* eddie-web-components: components/button/button.ts
|
|
50
|
+
* components/card/card.ts
|
|
51
|
+
* → depth 1 under pkgDir
|
|
52
|
+
*
|
|
53
|
+
* eddie-recipes: recipes/common/project-card/project-card.ts
|
|
54
|
+
* recipes/we-are-here/wah-logo/wah-logo.ts
|
|
55
|
+
* → depth 2 under pkgDir (extra project-name level)
|
|
56
|
+
*
|
|
57
|
+
* Historic bug: the previous glob was a two-level pattern (star slash star
|
|
58
|
+
* dot ts) which only matched depth 1, so the entire eddie-recipes catalog
|
|
59
|
+
* was invisible to the indexer. `ed-r-project-card` and 11 other real,
|
|
60
|
+
* shipping recipe components never made it into components.json. Fix: walk
|
|
61
|
+
* any depth via a double-star glob, then enforce the basename-matches-
|
|
62
|
+
* containing-directory convention to filter helpers, types, and barrel
|
|
63
|
+
* files that happen to sit next to real components.
|
|
64
|
+
*/
|
|
65
|
+
async scanPackage(pkgDir, pkgName, rootDir) {
|
|
66
|
+
// Walk any depth. Ignore base classes, test files, stories, and
|
|
67
|
+
// declaration files. Everything else is a candidate for being a component.
|
|
68
|
+
const tsFiles = globSync('**/*.ts', {
|
|
69
|
+
cwd: pkgDir,
|
|
70
|
+
ignore: [
|
|
71
|
+
'EdElement.ts',
|
|
72
|
+
'EdFormElement.ts',
|
|
73
|
+
'**/EdElement.ts',
|
|
74
|
+
'**/EdFormElement.ts',
|
|
75
|
+
'**/test/**',
|
|
76
|
+
'**/*.stories.ts',
|
|
77
|
+
'**/*.d.ts',
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
for (const relativeFile of tsFiles) {
|
|
81
|
+
// relativeFile is pkgDir-relative, e.g. "button/button.ts" or
|
|
82
|
+
// "common/project-card/project-card.ts"
|
|
83
|
+
const parts = relativeFile.split('/');
|
|
84
|
+
const fileName = parts[parts.length - 1];
|
|
85
|
+
const componentName = fileName.replace(/\.ts$/, '');
|
|
86
|
+
// Enforce the Eddie <name>/<name>.ts convention. If the containing
|
|
87
|
+
// directory's name doesn't match the file's name, this isn't a
|
|
88
|
+
// component — it's a helper, a shared type, or a barrel file.
|
|
89
|
+
// Skip silently.
|
|
90
|
+
if (parts.length < 2)
|
|
91
|
+
continue;
|
|
92
|
+
const dirName = parts[parts.length - 2];
|
|
93
|
+
if (dirName !== componentName)
|
|
94
|
+
continue;
|
|
95
|
+
// relativeDir is the path from pkgDir to the component's own directory,
|
|
96
|
+
// without the filename. E.g. "button" or "common/project-card".
|
|
97
|
+
const relativeDir = parts.slice(0, -1).join('/');
|
|
98
|
+
const absoluteComponentDir = join(pkgDir, relativeDir);
|
|
99
|
+
const absoluteFilePath = join(pkgDir, relativeFile);
|
|
100
|
+
try {
|
|
101
|
+
const entry = await this.parseComponent(componentName, absoluteFilePath, absoluteComponentDir, relativeDir, pkgName, rootDir);
|
|
102
|
+
if (entry) {
|
|
103
|
+
this.components.set(entry.tagName, entry);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
// Silently skip unparseable files
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Parse a single component file and extract metadata.
|
|
113
|
+
*
|
|
114
|
+
* @param componentName The component's base name (e.g. "button", "project-card")
|
|
115
|
+
* @param filePath Absolute path to the component's .ts file
|
|
116
|
+
* @param componentDir Absolute path to the component's own directory
|
|
117
|
+
* (contains the .ts, .scss, .stories.ts, and test/)
|
|
118
|
+
* @param relativeDir Path from the package root to the component dir,
|
|
119
|
+
* e.g. "button" (eddie-web-components) or
|
|
120
|
+
* "common/project-card" (eddie-recipes)
|
|
121
|
+
* @param pkgName Which package this component lives in
|
|
122
|
+
* @param rootDir Repo root (unused today but passed for future use)
|
|
123
|
+
*/
|
|
124
|
+
async parseComponent(componentName, filePath, componentDir, relativeDir, pkgName, rootDir) {
|
|
125
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
126
|
+
// Extract tag name from @customElement decorator or customElements.define().
|
|
127
|
+
//
|
|
128
|
+
// eddie-pages are structurally different: they're reusable Lit templates
|
|
129
|
+
// that extend LitElement directly, render into light DOM, and are NOT
|
|
130
|
+
// registered as custom elements. They have no `customElements.define()`
|
|
131
|
+
// call and no `@customElement` decorator. For pages, we synthesize an
|
|
132
|
+
// identifier of the form `ed-p-<name>` from the file name so they can
|
|
133
|
+
// live in the component index alongside real custom elements. Consumers
|
|
134
|
+
// can tell them apart by checking `atomicLevel === 'page'`.
|
|
135
|
+
const tagNameMatch = content.match(/@customElement\(['"]([^'"]+)['"]\)/) ||
|
|
136
|
+
content.match(/customElements\.define\(['"]([^'"]+)['"]/);
|
|
137
|
+
let tagName;
|
|
138
|
+
if (tagNameMatch) {
|
|
139
|
+
tagName = tagNameMatch[1];
|
|
140
|
+
}
|
|
141
|
+
else if (pkgName === 'eddie-pages') {
|
|
142
|
+
// Synthesize a stable identifier from the component name.
|
|
143
|
+
// e.g. "app-dashboard" → "ed-p-app-dashboard"
|
|
144
|
+
tagName = `ed-p-${componentName}`;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
return null; // Not a custom element and not a page
|
|
148
|
+
}
|
|
149
|
+
// Extract class name
|
|
150
|
+
const classMatch = content.match(/export class\s+(\w+)/);
|
|
151
|
+
if (!classMatch) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const className = classMatch[1];
|
|
155
|
+
const displayName = this.classNameToDisplayName(className);
|
|
156
|
+
// Determine CSS class name (follows BEM convention).
|
|
157
|
+
// The category letter distinguishes core components, recipes, and pages:
|
|
158
|
+
// core components: `ed-c-<name>` (c = component)
|
|
159
|
+
// recipes: `ed-r-<name>` (r = recipe)
|
|
160
|
+
// pages: `ed-p-<name>` (p = page)
|
|
161
|
+
//
|
|
162
|
+
// Historic bug: the previous derivation emitted `ed-r-c-<name>` and
|
|
163
|
+
// `ed-p-c-<name>` (from PRs #497 and #499), double-encoding the
|
|
164
|
+
// category with an extra `-c-`. The recipe source files also use
|
|
165
|
+
// `ed-r-c-*` today (tracked as #505 for a source-side rename). Once
|
|
166
|
+
// that rename lands, the source and the indexer will agree. In the
|
|
167
|
+
// meantime, the indexer reflects the correct target convention, not
|
|
168
|
+
// the temporary source state.
|
|
169
|
+
const nameAsClass = componentName
|
|
170
|
+
.replace(/([A-Z])/g, '-$1')
|
|
171
|
+
.toLowerCase()
|
|
172
|
+
.replace(/^-/, '');
|
|
173
|
+
let cssClassName;
|
|
174
|
+
if (pkgName === 'eddie-recipes' || pkgName === 'eddie-charts') {
|
|
175
|
+
// eddie-charts components are recipes in every sense except their
|
|
176
|
+
// runtime dep on Chart.js — they use the same `ed-r-*` tag and class
|
|
177
|
+
// prefix as eddie-recipes.
|
|
178
|
+
cssClassName = `ed-r-${nameAsClass}`;
|
|
179
|
+
}
|
|
180
|
+
else if (pkgName === 'eddie-pages') {
|
|
181
|
+
cssClassName = `ed-p-${nameAsClass}`;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
cssClassName = `ed-c-${nameAsClass}`;
|
|
185
|
+
}
|
|
186
|
+
// Extract base class
|
|
187
|
+
const baseClassMatch = content.match(/extends\s+(EdElement|EdFormElement)/);
|
|
188
|
+
const baseClass = baseClassMatch ? baseClassMatch[1] : 'EdElement';
|
|
189
|
+
// Extract properties
|
|
190
|
+
const properties = this.extractProperties(content);
|
|
191
|
+
// Extract slots
|
|
192
|
+
const slots = this.extractSlots(content);
|
|
193
|
+
// Extract events
|
|
194
|
+
const events = this.extractEvents(content);
|
|
195
|
+
// Extract CSS custom properties used
|
|
196
|
+
const cssCustomProperties = this.extractCssCustomProperties(content);
|
|
197
|
+
// Determine atomic level from stories file (uses componentDir directly —
|
|
198
|
+
// previously this constructed the stories path from a hardcoded
|
|
199
|
+
// "components/<name>" template which broke for eddie-recipes).
|
|
200
|
+
const atomicLevel = await this.getAtomicLevel(componentName, componentDir);
|
|
201
|
+
// Extract component intent from JSDoc
|
|
202
|
+
const intent = this.extractIntent(content);
|
|
203
|
+
// Extract guidelines (use / dontUse / accessibility) from JSDoc tags
|
|
204
|
+
const guidelines = this.extractGuidelines(content);
|
|
205
|
+
// Determine parent/child relationships
|
|
206
|
+
const { parentComponent, childComponents } = this.determineCompoundRelationship(tagName, componentName);
|
|
207
|
+
// Relative paths for cross-referencing. Previously hardcoded to
|
|
208
|
+
// `components/<name>/...` which only worked for eddie-web-components.
|
|
209
|
+
// Now derived from the component's actual relative directory within
|
|
210
|
+
// its package, with a package-specific subdir:
|
|
211
|
+
// eddie-web-components → components/<relativeDir>/...
|
|
212
|
+
// eddie-recipes → recipes/<relativeDir>/...
|
|
213
|
+
// eddie-pages → pages/<relativeDir>/...
|
|
214
|
+
//
|
|
215
|
+
// Each sibling-file path (styles, stories, test) is filesystem-verified
|
|
216
|
+
// before being emitted — the previous implementation emitted all four
|
|
217
|
+
// unconditionally, so consumers querying eddie-brain saw paths like
|
|
218
|
+
// `pages/common/homepage/homepage.scss` for components that have no
|
|
219
|
+
// .scss file at all. The `component` path is always emitted because we
|
|
220
|
+
// just read it.
|
|
221
|
+
let pkgSubdir;
|
|
222
|
+
if (pkgName === 'eddie-recipes') {
|
|
223
|
+
pkgSubdir = 'recipes';
|
|
224
|
+
}
|
|
225
|
+
else if (pkgName === 'eddie-pages') {
|
|
226
|
+
pkgSubdir = 'pages';
|
|
227
|
+
}
|
|
228
|
+
else if (pkgName === 'eddie-charts') {
|
|
229
|
+
pkgSubdir = 'charts';
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
pkgSubdir = 'components';
|
|
233
|
+
}
|
|
234
|
+
const paths = {
|
|
235
|
+
component: `${pkgSubdir}/${relativeDir}/${componentName}.ts`,
|
|
236
|
+
};
|
|
237
|
+
const stylesAbs = join(componentDir, `${componentName}.scss`);
|
|
238
|
+
if (existsSync(stylesAbs)) {
|
|
239
|
+
paths.styles = `${pkgSubdir}/${relativeDir}/${componentName}.scss`;
|
|
240
|
+
}
|
|
241
|
+
const storiesAbs = join(componentDir, `${componentName}.stories.ts`);
|
|
242
|
+
if (existsSync(storiesAbs)) {
|
|
243
|
+
paths.stories = `${pkgSubdir}/${relativeDir}/${componentName}.stories.ts`;
|
|
244
|
+
}
|
|
245
|
+
const testAbs = join(componentDir, 'test', `${componentName}.test.ts`);
|
|
246
|
+
if (existsSync(testAbs)) {
|
|
247
|
+
paths.test = `${pkgSubdir}/${relativeDir}/test/${componentName}.test.ts`;
|
|
248
|
+
}
|
|
249
|
+
// Extract composesWith from import statements in the source. This turns
|
|
250
|
+
// the previously-empty array into a real list of `<ed-*>` / `<ed-r-*>` /
|
|
251
|
+
// `<ed-p-*>` tag names that the component or template imports — the
|
|
252
|
+
// catalog answer to "what does this thing combine?". Imports that don't
|
|
253
|
+
// resolve to an Eddie component path are ignored.
|
|
254
|
+
const composesWith = this.extractComposesWith(content);
|
|
255
|
+
// Extract the component's customization surface (slots/props marked
|
|
256
|
+
// `@overridableSlot` / `@overridableProp`) and its canonical invocation
|
|
257
|
+
// (prose from `@canonicalUsage` + markup from the Default story). Both
|
|
258
|
+
// are optional — components that don't declare these tags simply get
|
|
259
|
+
// `undefined` for the corresponding field. See #642.
|
|
260
|
+
const overridableSurface = this.extractOverridableSurface(content);
|
|
261
|
+
const canonicalUsage = this.extractCanonicalUsage(content, componentDir, componentName);
|
|
262
|
+
const contentApi = this.extractContentApi(content);
|
|
263
|
+
return {
|
|
264
|
+
tagName,
|
|
265
|
+
displayName,
|
|
266
|
+
cssClassName,
|
|
267
|
+
atomicLevel,
|
|
268
|
+
package: pkgName,
|
|
269
|
+
intent,
|
|
270
|
+
baseClass,
|
|
271
|
+
properties,
|
|
272
|
+
slots,
|
|
273
|
+
cssCustomProperties,
|
|
274
|
+
events,
|
|
275
|
+
composesWith,
|
|
276
|
+
parentComponent,
|
|
277
|
+
childComponents,
|
|
278
|
+
guidelines,
|
|
279
|
+
overridableSurface,
|
|
280
|
+
canonicalUsage,
|
|
281
|
+
contentApi,
|
|
282
|
+
paths,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Extract the list of Eddie component tag names referenced via `import`
|
|
287
|
+
* statements in the source.
|
|
288
|
+
*
|
|
289
|
+
* Recognizes the canonical Eddie import paths:
|
|
290
|
+
*
|
|
291
|
+
* import '@brad-frost-web/eddie-web-components/components/button/button';
|
|
292
|
+
* → ed-button
|
|
293
|
+
* import '@brad-frost-web/eddie-recipes/recipes/common/site-header/site-header';
|
|
294
|
+
* → ed-r-site-header
|
|
295
|
+
* import '@brad-frost-web/eddie-pages/pages/common/homepage/homepage';
|
|
296
|
+
* → ed-p-homepage
|
|
297
|
+
*
|
|
298
|
+
* The trailing `/<name>/<name>` segment is the convention enforced by
|
|
299
|
+
* scanPackage (component basename matches its containing directory). We
|
|
300
|
+
* map the basename to a tag name using the same package → prefix table
|
|
301
|
+
* the indexer uses for cssClassName.
|
|
302
|
+
*
|
|
303
|
+
* Imports that don't match this shape (e.g. `import { html } from 'lit'`,
|
|
304
|
+
* `import styles from './foo.scss?inline'`, EdElement base-class imports)
|
|
305
|
+
* are ignored. Side-effect imports without a `from` (the most common form
|
|
306
|
+
* for Eddie component registration) and named/default imports are both
|
|
307
|
+
* matched.
|
|
308
|
+
*
|
|
309
|
+
* The resulting list is deduplicated and sorted for stable output across
|
|
310
|
+
* builds (eliminates a class of dirty-diff in the catalog snapshot).
|
|
311
|
+
*/
|
|
312
|
+
extractComposesWith(content) {
|
|
313
|
+
const tagNames = new Set();
|
|
314
|
+
// Match the bare-specifier import form used everywhere in the codebase:
|
|
315
|
+
// import '@brad-frost-web/<pkg>/<path>';
|
|
316
|
+
// import { x } from '@brad-frost-web/<pkg>/<path>';
|
|
317
|
+
// import x from '@brad-frost-web/<pkg>/<path>';
|
|
318
|
+
const importRegex = /import\s+(?:[^'"`;]+\s+from\s+)?['"]@brad-frost-web\/([^'"`]+)['"]/g;
|
|
319
|
+
let match;
|
|
320
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
321
|
+
const importPath = match[1];
|
|
322
|
+
const tagName = this.importPathToTagName(importPath);
|
|
323
|
+
if (tagName)
|
|
324
|
+
tagNames.add(tagName);
|
|
325
|
+
}
|
|
326
|
+
return Array.from(tagNames).sort();
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Map a `@brad-frost-web/<pkg>/<path>` import specifier to its Eddie tag
|
|
330
|
+
* name, or return null if the specifier doesn't resolve to a component.
|
|
331
|
+
*
|
|
332
|
+
* Handles the three component-bearing packages:
|
|
333
|
+
*
|
|
334
|
+
* eddie-web-components/components/<X>/<X> → ed-<X>
|
|
335
|
+
* eddie-recipes/recipes/<scope>/<X>/<X> → ed-r-<X>
|
|
336
|
+
* eddie-pages/pages/<scope>/<X>/<X> → ed-p-<X>
|
|
337
|
+
*
|
|
338
|
+
* Returns null for non-component imports such as the `EdElement` base
|
|
339
|
+
* class, theme/token CSS, asset paths, or any other shape we don't
|
|
340
|
+
* recognize as a registered Eddie tag.
|
|
341
|
+
*/
|
|
342
|
+
importPathToTagName(importPath) {
|
|
343
|
+
const segments = importPath.split('/');
|
|
344
|
+
if (segments.length < 3)
|
|
345
|
+
return null;
|
|
346
|
+
const [pkg, ...rest] = segments;
|
|
347
|
+
// Convention: the last two path segments are `<name>/<name>` for any
|
|
348
|
+
// valid Eddie component registration import. If they don't match, it's
|
|
349
|
+
// not a component import.
|
|
350
|
+
const last = rest[rest.length - 1];
|
|
351
|
+
const beforeLast = rest[rest.length - 2];
|
|
352
|
+
if (!last || !beforeLast || last !== beforeLast)
|
|
353
|
+
return null;
|
|
354
|
+
// Skip the EdElement base class explicitly; its path is
|
|
355
|
+
// `eddie-web-components/components/EdElement` which doesn't fit the
|
|
356
|
+
// <name>/<name> convention anyway, so the check above usually catches it,
|
|
357
|
+
// but be defensive.
|
|
358
|
+
if (last === 'EdElement' || last === 'EdFormElement')
|
|
359
|
+
return null;
|
|
360
|
+
if (pkg === 'eddie-web-components' && rest[0] === 'components') {
|
|
361
|
+
return `ed-${last}`;
|
|
362
|
+
}
|
|
363
|
+
if (pkg === 'eddie-recipes' && rest[0] === 'recipes') {
|
|
364
|
+
return `ed-r-${last}`;
|
|
365
|
+
}
|
|
366
|
+
if (pkg === 'eddie-pages' && rest[0] === 'pages') {
|
|
367
|
+
return `ed-p-${last}`;
|
|
368
|
+
}
|
|
369
|
+
if (pkg === 'eddie-charts' && rest[0] === 'charts') {
|
|
370
|
+
// eddie-charts shares the `ed-r-*` tag prefix with eddie-recipes.
|
|
371
|
+
return `ed-r-${last}`;
|
|
372
|
+
}
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Extract `use` / `dontUse` / `accessibility` guidelines from the class-level
|
|
377
|
+
* JSDoc. Recognizes three custom tag forms:
|
|
378
|
+
*
|
|
379
|
+
* @use <text> → adds one entry to guidelines.use
|
|
380
|
+
* @dontuse <text> → adds one entry to guidelines.dontUse
|
|
381
|
+
* @a11y <text> → adds one entry to guidelines.accessibility
|
|
382
|
+
*
|
|
383
|
+
* Tags can appear multiple times in the same JSDoc block, with each
|
|
384
|
+
* occurrence becoming a separate bullet in the corresponding array. The
|
|
385
|
+
* tag text is everything from after the tag name to the end of the line.
|
|
386
|
+
* Trailing-line continuations are not currently supported — each guideline
|
|
387
|
+
* must fit on one line.
|
|
388
|
+
*
|
|
389
|
+
* This is the counterpart to authoring-side guidelines in the component
|
|
390
|
+
* source files. Today the Eddie catalog is sparse on guidelines; this
|
|
391
|
+
* parser is the mechanism that lets new guidelines actually land in
|
|
392
|
+
* eddie-brain's output.
|
|
393
|
+
*/
|
|
394
|
+
extractGuidelines(content) {
|
|
395
|
+
const jsDocMatch = content.match(/\/\*\*[\s\S]*?\*\/\s*export class/);
|
|
396
|
+
if (!jsDocMatch) {
|
|
397
|
+
return { use: [], dontUse: [], accessibility: [] };
|
|
398
|
+
}
|
|
399
|
+
const jsDocText = jsDocMatch[0];
|
|
400
|
+
const extractTag = (tag) => {
|
|
401
|
+
// Case-insensitive to tolerate @DontUse / @dontUse variations
|
|
402
|
+
const regex = new RegExp(`@${tag}\\s+([^\\n]+)`, 'gi');
|
|
403
|
+
const items = [];
|
|
404
|
+
let m;
|
|
405
|
+
while ((m = regex.exec(jsDocText)) !== null) {
|
|
406
|
+
const text = m[1].trim();
|
|
407
|
+
if (text.length > 0)
|
|
408
|
+
items.push(text);
|
|
409
|
+
}
|
|
410
|
+
return items;
|
|
411
|
+
};
|
|
412
|
+
return {
|
|
413
|
+
use: extractTag('use'),
|
|
414
|
+
dontUse: extractTag('dontuse'),
|
|
415
|
+
accessibility: extractTag('a11y'),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Extract `@overridableSlot` and `@overridableProp` tags from class-level
|
|
420
|
+
* JSDoc. These mark the specific slots/props a consumer is expected to use
|
|
421
|
+
* to customize the component — distinct from `slots` (which lists every
|
|
422
|
+
* slot that exists) and `properties` (which lists everything the class
|
|
423
|
+
* declares). See #642 for why we need both.
|
|
424
|
+
*
|
|
425
|
+
* Tag shapes:
|
|
426
|
+
* `@overridableSlot name — purpose`
|
|
427
|
+
* `@overridableSlot `name` purpose`
|
|
428
|
+
* `@overridableProp name — purpose`
|
|
429
|
+
* (any of em-dash, en-dash, or hyphen between name and purpose; optional)
|
|
430
|
+
*/
|
|
431
|
+
extractOverridableSurface(content) {
|
|
432
|
+
const jsDocMatch = content.match(/\/\*\*[\s\S]*?\*\/\s*export class/);
|
|
433
|
+
if (!jsDocMatch)
|
|
434
|
+
return undefined;
|
|
435
|
+
const jsDocText = jsDocMatch[0];
|
|
436
|
+
const slots = [];
|
|
437
|
+
const props = [];
|
|
438
|
+
const slotRegex = /@overridableSlot\s+`?([\w-]+)`?\s*[-—–]?\s*([^\n]*)/gi;
|
|
439
|
+
let m;
|
|
440
|
+
while ((m = slotRegex.exec(jsDocText)) !== null) {
|
|
441
|
+
const name = m[1];
|
|
442
|
+
const purpose = m[2].trim();
|
|
443
|
+
if (name)
|
|
444
|
+
slots.push({ name, purpose });
|
|
445
|
+
}
|
|
446
|
+
const propRegex = /@overridableProp\s+`?([\w-]+)`?\s*[-—–]?\s*([^\n]*)/gi;
|
|
447
|
+
while ((m = propRegex.exec(jsDocText)) !== null) {
|
|
448
|
+
const name = m[1];
|
|
449
|
+
const purpose = m[2].trim();
|
|
450
|
+
if (name)
|
|
451
|
+
props.push({ name, purpose });
|
|
452
|
+
}
|
|
453
|
+
if (slots.length === 0 && props.length === 0)
|
|
454
|
+
return undefined;
|
|
455
|
+
return { slots, props };
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Extract the canonical invocation of the component.
|
|
459
|
+
*
|
|
460
|
+
* Sourced from two places:
|
|
461
|
+
* - Optional `@canonicalUsage` JSDoc tag on the class → prose `note`
|
|
462
|
+
* - The `Default` export in the component's stories file → `default`
|
|
463
|
+
* markup (only when the story uses `html\`...\`` — imperative
|
|
464
|
+
* `document.createElement` stories are skipped because they have no
|
|
465
|
+
* static markup to extract)
|
|
466
|
+
*
|
|
467
|
+
* The goal (per #642): when a consumer asks "use the default site header",
|
|
468
|
+
* an agent can call `eddie_get_component` and get back the exact markup to
|
|
469
|
+
* paste. No interpretation, no guessing.
|
|
470
|
+
*/
|
|
471
|
+
extractCanonicalUsage(content, componentDir, componentName) {
|
|
472
|
+
let note;
|
|
473
|
+
const jsDocMatch = content.match(/\/\*\*[\s\S]*?\*\/\s*export class/);
|
|
474
|
+
if (jsDocMatch) {
|
|
475
|
+
const tagMatch = jsDocMatch[0].match(/@canonicalUsage\s+([^\n]+)/i);
|
|
476
|
+
if (tagMatch)
|
|
477
|
+
note = tagMatch[1].trim();
|
|
478
|
+
}
|
|
479
|
+
const storiesPath = join(componentDir, `${componentName}.stories.ts`);
|
|
480
|
+
const markup = this.extractDefaultStoryMarkup(storiesPath);
|
|
481
|
+
if (!note && !markup)
|
|
482
|
+
return undefined;
|
|
483
|
+
return { default: markup, note };
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Extract a `@contentApi { ... }` JSDoc tag from the class-level comment.
|
|
487
|
+
*
|
|
488
|
+
* The tag value is a JSON-ish object literal (single quotes or double quotes
|
|
489
|
+
* accepted, trailing commas tolerated). Three keys are recognized:
|
|
490
|
+
*
|
|
491
|
+
* contentVia — string description of the primary content path
|
|
492
|
+
* labelVia — "prop" | "slot" | "none"
|
|
493
|
+
* labelVisibility — "visible" | "hidden" | "accessible-only"
|
|
494
|
+
*
|
|
495
|
+
* Components that don't declare the tag get `undefined`. See #627: this is
|
|
496
|
+
* the structural answer to "where does this component's content come from?",
|
|
497
|
+
* letting consumers and validators detect prop-vs-slot mismatches before
|
|
498
|
+
* they render as silent drops.
|
|
499
|
+
*/
|
|
500
|
+
extractContentApi(content) {
|
|
501
|
+
const jsDocMatch = content.match(/\/\*\*[\s\S]*?\*\/\s*export class/);
|
|
502
|
+
if (!jsDocMatch)
|
|
503
|
+
return undefined;
|
|
504
|
+
const tagMatch = jsDocMatch[0].match(/@contentApi\s*(\{[\s\S]*?\})/i);
|
|
505
|
+
if (!tagMatch)
|
|
506
|
+
return undefined;
|
|
507
|
+
// The tag body is JSON-ish but lenient: bare keys, single or double
|
|
508
|
+
// quotes, and trailing commas are all acceptable. Pull out each known
|
|
509
|
+
// field by name with a tolerant per-key regex rather than relying on
|
|
510
|
+
// JSON.parse — the JSDoc continuation asterisks make the body messy
|
|
511
|
+
// enough that strict parsing isn't worth it.
|
|
512
|
+
const body = tagMatch[1].replace(/^\s*\*\s?/gm, '');
|
|
513
|
+
const result = {};
|
|
514
|
+
const contentVia = body.match(/\bcontentVia\s*:\s*['"]([^'"]+)['"]/);
|
|
515
|
+
if (contentVia)
|
|
516
|
+
result.contentVia = contentVia[1];
|
|
517
|
+
const labelVia = body.match(/\blabelVia\s*:\s*['"](prop|slot|none)['"]/);
|
|
518
|
+
if (labelVia)
|
|
519
|
+
result.labelVia = labelVia[1];
|
|
520
|
+
const labelVisibility = body.match(/\blabelVisibility\s*:\s*['"](visible|hidden|accessible-only)['"]/);
|
|
521
|
+
if (labelVisibility) {
|
|
522
|
+
result.labelVisibility = labelVisibility[1];
|
|
523
|
+
}
|
|
524
|
+
if (Object.keys(result).length === 0)
|
|
525
|
+
return undefined;
|
|
526
|
+
return result;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Parse the `Default` story template literal out of a stories file.
|
|
530
|
+
*
|
|
531
|
+
* Recognizes the common Eddie story shape:
|
|
532
|
+
*
|
|
533
|
+
* export const Default = () => html`<ed-foo></ed-foo>`;
|
|
534
|
+
* export const Default = (args: Args) => html`<ed-foo>...</ed-foo>`;
|
|
535
|
+
* export const Default = () => html`
|
|
536
|
+
* <ed-foo>
|
|
537
|
+
* ...
|
|
538
|
+
* </ed-foo>
|
|
539
|
+
* `;
|
|
540
|
+
*
|
|
541
|
+
* Returns undefined for imperative stories (`document.createElement(...)`)
|
|
542
|
+
* which have no extractable markup — consumers of those recipes interact
|
|
543
|
+
* via JS properties rather than markup and don't benefit from a canonical
|
|
544
|
+
* markup string.
|
|
545
|
+
*/
|
|
546
|
+
extractDefaultStoryMarkup(storiesPath) {
|
|
547
|
+
if (!existsSync(storiesPath))
|
|
548
|
+
return undefined;
|
|
549
|
+
let content;
|
|
550
|
+
try {
|
|
551
|
+
content = readFileSync(storiesPath, 'utf-8');
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
return undefined;
|
|
555
|
+
}
|
|
556
|
+
const match = content.match(/export\s+const\s+Default[^=]*=\s*(?:\([^)]*\)\s*=>\s*)?(?:\{\s*return\s+)?html`([^`]*)`/);
|
|
557
|
+
if (!match)
|
|
558
|
+
return undefined;
|
|
559
|
+
return match[1].trim();
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Extract @property decorated properties
|
|
563
|
+
*/
|
|
564
|
+
extractProperties(content) {
|
|
565
|
+
const properties = [];
|
|
566
|
+
// Match @property decorators and following property declarations.
|
|
567
|
+
//
|
|
568
|
+
// IMPORTANT: `[^=;\n]*` (not `[^=]*`) constrains the between-name-and-equals
|
|
569
|
+
// match so it cannot cross statement terminators. Previously the regex used
|
|
570
|
+
// `[^=]*` which is greedy and newline-permissive, so after matching a
|
|
571
|
+
// property declaration with no default (e.g. `variant?: 'bare';`) the
|
|
572
|
+
// engine would continue across newlines looking for the next `=` sign
|
|
573
|
+
// anywhere in the file — typically landing inside the render method on
|
|
574
|
+
// something like `const componentClassNames = this.componentClassNames(...)`
|
|
575
|
+
// and capturing truncated method-call text as the "default value". The
|
|
576
|
+
// tighter character class prevents that cross-statement leakage.
|
|
577
|
+
const propRegex = /@property\s*\(\s*({[^}]*})?\s*\)\s*(?:declare\s+)?(\w+)[^=;\n]*(?:=\s*([^;\n]+))?/g;
|
|
578
|
+
let match;
|
|
579
|
+
while ((match = propRegex.exec(content)) !== null) {
|
|
580
|
+
const decoratorConfig = match[1] || '';
|
|
581
|
+
const propName = match[2];
|
|
582
|
+
const rawDefault = match[3]?.trim();
|
|
583
|
+
// Sanity-check the captured default: if it has unbalanced parens/braces
|
|
584
|
+
// it's a truncated expression (multi-line method call, template literal,
|
|
585
|
+
// object initializer that extends past the line) and should not be
|
|
586
|
+
// emitted as a "default value". Emit nothing in that case.
|
|
587
|
+
const defaultValue = rawDefault && this.hasBalancedDelimiters(rawDefault)
|
|
588
|
+
? rawDefault
|
|
589
|
+
: undefined;
|
|
590
|
+
// Extract type from property context
|
|
591
|
+
const contextStart = Math.max(0, match.index - 500);
|
|
592
|
+
const contextEnd = match.index + 300;
|
|
593
|
+
const context = content.substring(contextStart, contextEnd);
|
|
594
|
+
const typeMatch = context.match(new RegExp(`${propName}\\s*:\\s*([^;=\n]+)`));
|
|
595
|
+
const propType = typeMatch ? typeMatch[1].trim().split(/[{|}]/)[0].trim() : 'string';
|
|
596
|
+
// Check if required
|
|
597
|
+
const required = !decoratorConfig.includes('type: Boolean') && !defaultValue;
|
|
598
|
+
// Extract the JSDoc comment IMMEDIATELY preceding this @property decorator.
|
|
599
|
+
//
|
|
600
|
+
// Historic bug (#642): the previous implementation used a forward regex
|
|
601
|
+
// `/\/\*\*[\s\S]*?\*\/\s*@property/` on a 500-char-back context window.
|
|
602
|
+
// Non-greedy matching starts at the earliest `/**` in the window, so
|
|
603
|
+
// when property A's JSDoc + declaration fits into the window preceding
|
|
604
|
+
// property B, the regex finds A's `/** ... */ @property a: ...` and
|
|
605
|
+
// returns A's description for B. Reproducible on `ed-logo`: the
|
|
606
|
+
// `accent` property was getting `href`'s description.
|
|
607
|
+
//
|
|
608
|
+
// Fix: walk backward from the @property match to find the closest `*/`,
|
|
609
|
+
// verify there's only whitespace between that `*/` and the @property
|
|
610
|
+
// (so we know this JSDoc actually belongs to THIS property), then find
|
|
611
|
+
// the matching `/**`.
|
|
612
|
+
const before = content.substring(0, match.index);
|
|
613
|
+
const jsDocEnd = before.lastIndexOf('*/');
|
|
614
|
+
let description = '';
|
|
615
|
+
if (jsDocEnd !== -1) {
|
|
616
|
+
const between = before.substring(jsDocEnd + 2);
|
|
617
|
+
if (/^\s*$/.test(between)) {
|
|
618
|
+
const jsDocStart = before.lastIndexOf('/**', jsDocEnd);
|
|
619
|
+
if (jsDocStart !== -1) {
|
|
620
|
+
description = before
|
|
621
|
+
.substring(jsDocStart, jsDocEnd + 2)
|
|
622
|
+
.replace(/\/\*\*/, '')
|
|
623
|
+
.replace(/\*\//, '')
|
|
624
|
+
.split('\n')
|
|
625
|
+
.map((line) => line.replace(/^\s*\*\s?/, '').trim())
|
|
626
|
+
.filter((line) => line.length > 0)
|
|
627
|
+
.join(' ');
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// Extract enum options if present
|
|
632
|
+
const options = this.extractEnumOptions(propType);
|
|
633
|
+
properties.push({
|
|
634
|
+
name: propName,
|
|
635
|
+
type: propType.split('|')[0].trim(),
|
|
636
|
+
default: defaultValue,
|
|
637
|
+
description,
|
|
638
|
+
options,
|
|
639
|
+
required,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
return properties;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Extract enum options from union types
|
|
646
|
+
*/
|
|
647
|
+
extractEnumOptions(type) {
|
|
648
|
+
const unionMatch = type.match(/['"]([^'"]+)['"]\s*\|\s*['"]([^'"]+)['"]/g);
|
|
649
|
+
if (unionMatch) {
|
|
650
|
+
return unionMatch.map((opt) => opt.replace(/['"]/g, ''));
|
|
651
|
+
}
|
|
652
|
+
return undefined;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Extract slot definitions from JSDoc comments.
|
|
656
|
+
*
|
|
657
|
+
* Parses the Eddie JSDoc slot convention, which comes in these forms:
|
|
658
|
+
*
|
|
659
|
+
* @slot - The default slot description. (default slot, leading dash)
|
|
660
|
+
* @slot header - Optional header content (named slot, name before dash)
|
|
661
|
+
* @slot `header` Optional header content (named slot, backticked name)
|
|
662
|
+
* @slot Just a default description (default slot, no dash, bare prose)
|
|
663
|
+
*
|
|
664
|
+
* Also looks for `<slot name="x">` in the render template as a secondary
|
|
665
|
+
* source for slots that weren't explicitly documented in JSDoc.
|
|
666
|
+
*
|
|
667
|
+
* Historic bug: the previous regex `/@slot\s*(?:-\s*)?([^\n]*)/` consumed
|
|
668
|
+
* the optional leading `- ` and then an inner regex `/^`?(\w+)`?/` grabbed
|
|
669
|
+
* the first word of the remaining text as the slot name. For a default slot
|
|
670
|
+
* like `@slot - The grid items`, this produced slot name "The" (the first
|
|
671
|
+
* word of "The grid items") instead of "default". Bug fix: distinguish the
|
|
672
|
+
* four cases above explicitly.
|
|
673
|
+
*/
|
|
674
|
+
extractSlots(content) {
|
|
675
|
+
const slots = [];
|
|
676
|
+
// Grab each @slot JSDoc line individually
|
|
677
|
+
const slotLineRegex = /@slot\s+([^\n]*)/g;
|
|
678
|
+
let match;
|
|
679
|
+
while ((match = slotLineRegex.exec(content)) !== null) {
|
|
680
|
+
const rest = match[1].trim();
|
|
681
|
+
let name;
|
|
682
|
+
let description;
|
|
683
|
+
if (rest.startsWith('-')) {
|
|
684
|
+
// Form: "@slot - description" → default slot
|
|
685
|
+
name = 'default';
|
|
686
|
+
description = rest.slice(1).trim();
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
const backtickMatch = rest.match(/^`(\w+)`\s*-?\s*(.*)$/);
|
|
690
|
+
const dashMatch = rest.match(/^(\w+)\s+-\s+(.*)$/);
|
|
691
|
+
if (backtickMatch) {
|
|
692
|
+
// Form: "@slot `name` description" → named slot, backticked
|
|
693
|
+
name = backtickMatch[1];
|
|
694
|
+
description = backtickMatch[2].trim();
|
|
695
|
+
}
|
|
696
|
+
else if (dashMatch) {
|
|
697
|
+
// Form: "@slot name - description" → named slot, dash-separated
|
|
698
|
+
name = dashMatch[1];
|
|
699
|
+
description = dashMatch[2].trim();
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
// Form: "@slot Just a description" → default slot, bare prose.
|
|
703
|
+
// We do NOT pluck the first word as a name — that was the historic
|
|
704
|
+
// bug. If the author wanted a named slot they would have used one
|
|
705
|
+
// of the explicit forms above.
|
|
706
|
+
name = 'default';
|
|
707
|
+
description = rest;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
slots.push({ name, description });
|
|
711
|
+
}
|
|
712
|
+
// Supplement with any slots declared in the render template that weren't
|
|
713
|
+
// caught by JSDoc (e.g., when the author named a slot but didn't document
|
|
714
|
+
// it with @slot).
|
|
715
|
+
const renderSlotRegex = /<slot\s+name=["']([^"']+)["']/g;
|
|
716
|
+
let renderMatch;
|
|
717
|
+
while ((renderMatch = renderSlotRegex.exec(content)) !== null) {
|
|
718
|
+
const slotName = renderMatch[1];
|
|
719
|
+
if (!slots.find((s) => s.name === slotName)) {
|
|
720
|
+
slots.push({ name: slotName, description: '' });
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return slots;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Returns true if the given string has balanced parens, braces, and
|
|
727
|
+
* brackets. Used to reject truncated expressions (like multi-line method
|
|
728
|
+
* calls that span past the regex capture boundary) from being emitted as
|
|
729
|
+
* property default values.
|
|
730
|
+
*/
|
|
731
|
+
hasBalancedDelimiters(s) {
|
|
732
|
+
let paren = 0;
|
|
733
|
+
let brace = 0;
|
|
734
|
+
let bracket = 0;
|
|
735
|
+
for (const c of s) {
|
|
736
|
+
if (c === '(')
|
|
737
|
+
paren++;
|
|
738
|
+
else if (c === ')')
|
|
739
|
+
paren--;
|
|
740
|
+
else if (c === '{')
|
|
741
|
+
brace++;
|
|
742
|
+
else if (c === '}')
|
|
743
|
+
brace--;
|
|
744
|
+
else if (c === '[')
|
|
745
|
+
bracket++;
|
|
746
|
+
else if (c === ']')
|
|
747
|
+
bracket--;
|
|
748
|
+
if (paren < 0 || brace < 0 || bracket < 0)
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
return paren === 0 && brace === 0 && bracket === 0;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Extract custom events from dispatch calls
|
|
755
|
+
*/
|
|
756
|
+
extractEvents(content) {
|
|
757
|
+
const events = [];
|
|
758
|
+
// Look for dispatchEvent or dispatch calls
|
|
759
|
+
const dispatchRegex = /dispatch\(['"]([^'"]+)['"]/g;
|
|
760
|
+
let match;
|
|
761
|
+
while ((match = dispatchRegex.exec(content)) !== null) {
|
|
762
|
+
const eventName = match[1];
|
|
763
|
+
if (!events.find((e) => e.name === eventName)) {
|
|
764
|
+
events.push({
|
|
765
|
+
name: eventName,
|
|
766
|
+
description: '',
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return events;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Extract CSS custom properties referenced
|
|
774
|
+
*/
|
|
775
|
+
extractCssCustomProperties(content) {
|
|
776
|
+
const props = new Set();
|
|
777
|
+
// Look for var(--ed-*) in SCSS content
|
|
778
|
+
const varRegex = /var\((--ed[^)]+)\)/g;
|
|
779
|
+
let match;
|
|
780
|
+
while ((match = varRegex.exec(content)) !== null) {
|
|
781
|
+
props.add(match[1]);
|
|
782
|
+
}
|
|
783
|
+
return Array.from(props).sort();
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Extract intent/purpose from JSDoc comment.
|
|
787
|
+
*
|
|
788
|
+
* Parses the class-level JSDoc preceding `export class` and returns the
|
|
789
|
+
* first real description paragraph. A "paragraph" is a sequence of
|
|
790
|
+
* consecutive non-blank, non-@tag lines joined with spaces.
|
|
791
|
+
*
|
|
792
|
+
* Heading detection: Eddie components sometimes begin their JSDoc with a
|
|
793
|
+
* one-word title line (e.g. "Grid", "Button") on its own, separated from
|
|
794
|
+
* the actual description by a blank line. In that case the first paragraph
|
|
795
|
+
* is just the single heading word, which is not a useful intent. If the
|
|
796
|
+
* first paragraph is a short heading-like line AND a second paragraph
|
|
797
|
+
* exists, return the second paragraph instead.
|
|
798
|
+
*
|
|
799
|
+
* Historic bug: the previous implementation used `.slice(0, 1)` on the
|
|
800
|
+
* filtered lines, which returned only the first non-blank non-@ line
|
|
801
|
+
* regardless of context — so `ed-grid`'s intent came back as literally the
|
|
802
|
+
* single word "Grid" while its real description lived on the next
|
|
803
|
+
* paragraph. Bug fix: paragraph-aware extraction.
|
|
804
|
+
*/
|
|
805
|
+
extractIntent(content) {
|
|
806
|
+
const jsDocMatch = content.match(/\/\*\*[\s\S]*?\*\/\s*export class/);
|
|
807
|
+
if (!jsDocMatch) {
|
|
808
|
+
return '';
|
|
809
|
+
}
|
|
810
|
+
const lines = jsDocMatch[0]
|
|
811
|
+
.replace(/\/\*\*/, '')
|
|
812
|
+
.replace(/\*\//, '')
|
|
813
|
+
.split('\n')
|
|
814
|
+
.map((line) => line.replace(/^\s*\*\s?/, '').trim());
|
|
815
|
+
// Group lines into paragraphs. Blank lines and @tag lines act as
|
|
816
|
+
// paragraph separators.
|
|
817
|
+
const paragraphs = [];
|
|
818
|
+
let current = [];
|
|
819
|
+
for (const line of lines) {
|
|
820
|
+
if (line.length === 0 || line.startsWith('@')) {
|
|
821
|
+
if (current.length > 0) {
|
|
822
|
+
paragraphs.push(current);
|
|
823
|
+
current = [];
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
current.push(line);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (current.length > 0)
|
|
831
|
+
paragraphs.push(current);
|
|
832
|
+
if (paragraphs.length === 0)
|
|
833
|
+
return '';
|
|
834
|
+
// Heading-detection heuristic: if the first paragraph is a single line
|
|
835
|
+
// of at most 3 words, treat it as a heading and prefer the next
|
|
836
|
+
// paragraph (if one exists). This handles components whose JSDoc starts
|
|
837
|
+
// with a title like "Grid" before the real description.
|
|
838
|
+
const firstParagraph = paragraphs[0].join(' ');
|
|
839
|
+
const looksLikeHeading = paragraphs[0].length === 1 && firstParagraph.split(/\s+/).filter(Boolean).length <= 3;
|
|
840
|
+
if (looksLikeHeading && paragraphs.length > 1) {
|
|
841
|
+
return paragraphs[1].join(' ');
|
|
842
|
+
}
|
|
843
|
+
return firstParagraph;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Get atomic level from the component's own stories file.
|
|
847
|
+
*
|
|
848
|
+
* `componentDir` is the component's actual directory (e.g.
|
|
849
|
+
* `packages/eddie-web-components/components/button` or
|
|
850
|
+
* `packages/eddie-recipes/recipes/common/project-card`). The stories file
|
|
851
|
+
* lives directly inside it as `<componentName>.stories.ts`.
|
|
852
|
+
*/
|
|
853
|
+
async getAtomicLevel(componentName, componentDir) {
|
|
854
|
+
try {
|
|
855
|
+
const storiesPath = join(componentDir, `${componentName}.stories.ts`);
|
|
856
|
+
const stories = readFileSync(storiesPath, 'utf-8');
|
|
857
|
+
// Extract title from default export
|
|
858
|
+
const titleMatch = stories.match(/title:\s*['"]([^'"]+)['"]/);
|
|
859
|
+
if (!titleMatch) {
|
|
860
|
+
return 'atom';
|
|
861
|
+
}
|
|
862
|
+
const title = titleMatch[1].toLowerCase();
|
|
863
|
+
if (title.includes('atoms'))
|
|
864
|
+
return 'atom';
|
|
865
|
+
if (title.includes('molecules'))
|
|
866
|
+
return 'molecule';
|
|
867
|
+
if (title.includes('organisms'))
|
|
868
|
+
return 'organism';
|
|
869
|
+
if (title.includes('recipes'))
|
|
870
|
+
return 'recipe';
|
|
871
|
+
if (title.includes('charts'))
|
|
872
|
+
return 'recipe';
|
|
873
|
+
if (title.includes('pages'))
|
|
874
|
+
return 'page';
|
|
875
|
+
return 'atom';
|
|
876
|
+
}
|
|
877
|
+
catch {
|
|
878
|
+
return 'atom';
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Determine parent/child relationships for compound components
|
|
883
|
+
*/
|
|
884
|
+
determineCompoundRelationship(tagName, componentName) {
|
|
885
|
+
// Check if this is a child component (starts with a parent name)
|
|
886
|
+
const parentMatch = Array.from(this.components.values()).find((comp) => tagName !== comp.tagName && tagName.startsWith(comp.tagName + '-'));
|
|
887
|
+
if (parentMatch) {
|
|
888
|
+
return {
|
|
889
|
+
parentComponent: parentMatch.tagName,
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
// Check if this is a parent with children
|
|
893
|
+
const children = Array.from(this.components.values())
|
|
894
|
+
.filter((comp) => comp.tagName.startsWith(tagName + '-'))
|
|
895
|
+
.map((comp) => comp.tagName);
|
|
896
|
+
if (children.length > 0) {
|
|
897
|
+
return {
|
|
898
|
+
childComponents: children,
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
return {};
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Convert class name to display name
|
|
905
|
+
*/
|
|
906
|
+
classNameToDisplayName(className) {
|
|
907
|
+
return className
|
|
908
|
+
.replace(/^Ed/, '')
|
|
909
|
+
.replace(/([A-Z])/g, ' $1')
|
|
910
|
+
.trim();
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Get a component by tag name
|
|
914
|
+
*/
|
|
915
|
+
getComponent(tagName) {
|
|
916
|
+
return this.components.get(tagName);
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Get all components
|
|
920
|
+
*/
|
|
921
|
+
getAll() {
|
|
922
|
+
return Array.from(this.components.values());
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Search components by query string
|
|
926
|
+
*/
|
|
927
|
+
/**
|
|
928
|
+
* Search components by natural-language query.
|
|
929
|
+
*
|
|
930
|
+
* Historic bug (#610): used naive `.includes()` substring matching against
|
|
931
|
+
* only tagName / displayName / intent. A multi-word query like "marketing
|
|
932
|
+
* homepage" never matched `ed-p-marketing-homepage` because the literal
|
|
933
|
+
* substring "marketing homepage" (with a space) is not anywhere in the
|
|
934
|
+
* tagName, displayName, or single-sentence intent. Pages were effectively
|
|
935
|
+
* invisible to any query an agent would actually type.
|
|
936
|
+
*
|
|
937
|
+
* Fix: tokenize the query, drop stopwords and very short terms, and score
|
|
938
|
+
* each candidate against the full set of fields that carry meaning —
|
|
939
|
+
* tagName, displayName, intent, guidelines.use, property names and
|
|
940
|
+
* descriptions, slot names and descriptions. Pages get a ranking boost
|
|
941
|
+
* when the query contains page-intent keywords ("page", "template",
|
|
942
|
+
* "dashboard", "homepage", "login", "article", etc.) because in that
|
|
943
|
+
* case the agent is looking for a full composition starting point, not
|
|
944
|
+
* a primitive.
|
|
945
|
+
*/
|
|
946
|
+
search(query) {
|
|
947
|
+
const STOPWORDS = new Set([
|
|
948
|
+
'a', 'an', 'the', 'of', 'in', 'on', 'at', 'to', 'for', 'by',
|
|
949
|
+
'is', 'it', 'as', 'or', 'and', 'with', 'that', 'this', 'from',
|
|
950
|
+
'how', 'what', 'which', 'use', 'using', 'build', 'make',
|
|
951
|
+
]);
|
|
952
|
+
const PAGE_KEYWORDS = new Set([
|
|
953
|
+
'page', 'pages', 'template', 'starter', 'shell', 'layout',
|
|
954
|
+
'homepage', 'dashboard', 'article', 'login', 'authentication',
|
|
955
|
+
'auth', 'checkout', 'form', 'grid',
|
|
956
|
+
]);
|
|
957
|
+
const terms = query
|
|
958
|
+
.toLowerCase()
|
|
959
|
+
.split(/\s+/)
|
|
960
|
+
.map((t) => t.replace(/[^a-z0-9-]/g, ''))
|
|
961
|
+
.filter((t) => t.length > 2 && !STOPWORDS.has(t));
|
|
962
|
+
if (terms.length === 0)
|
|
963
|
+
return [];
|
|
964
|
+
const queryWantsPage = terms.some((t) => PAGE_KEYWORDS.has(t));
|
|
965
|
+
const scoreEntry = (comp) => {
|
|
966
|
+
const haystack = {
|
|
967
|
+
tagName: comp.tagName.toLowerCase(),
|
|
968
|
+
displayName: comp.displayName.toLowerCase(),
|
|
969
|
+
intent: comp.intent.toLowerCase(),
|
|
970
|
+
useGuidelines: comp.guidelines.use.join(' ').toLowerCase(),
|
|
971
|
+
propNames: comp.properties.map((p) => p.name.toLowerCase()).join(' '),
|
|
972
|
+
propDescriptions: comp.properties.map((p) => p.description.toLowerCase()).join(' '),
|
|
973
|
+
slotNames: comp.slots.map((s) => s.name.toLowerCase()).join(' '),
|
|
974
|
+
slotDescriptions: comp.slots.map((s) => s.description.toLowerCase()).join(' '),
|
|
975
|
+
};
|
|
976
|
+
let score = 0;
|
|
977
|
+
for (const t of terms) {
|
|
978
|
+
if (haystack.tagName.includes(t))
|
|
979
|
+
score += 5;
|
|
980
|
+
if (haystack.displayName.includes(t))
|
|
981
|
+
score += 3;
|
|
982
|
+
if (haystack.intent.includes(t))
|
|
983
|
+
score += 3;
|
|
984
|
+
if (haystack.useGuidelines.includes(t))
|
|
985
|
+
score += 2;
|
|
986
|
+
if (haystack.slotNames.includes(t))
|
|
987
|
+
score += 2;
|
|
988
|
+
if (haystack.propNames.includes(t))
|
|
989
|
+
score += 1;
|
|
990
|
+
if (haystack.slotDescriptions.includes(t))
|
|
991
|
+
score += 1;
|
|
992
|
+
if (haystack.propDescriptions.includes(t))
|
|
993
|
+
score += 1;
|
|
994
|
+
}
|
|
995
|
+
// Boost pages when the query reads like it wants a page template.
|
|
996
|
+
if (queryWantsPage && comp.atomicLevel === 'page')
|
|
997
|
+
score += 6;
|
|
998
|
+
return score;
|
|
999
|
+
};
|
|
1000
|
+
return this.getAll()
|
|
1001
|
+
.map((comp) => ({ comp, score: scoreEntry(comp) }))
|
|
1002
|
+
.filter((r) => r.score > 0)
|
|
1003
|
+
.sort((a, b) => b.score - a.score)
|
|
1004
|
+
.map((r) => r.comp);
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Get components by atomic level
|
|
1008
|
+
*/
|
|
1009
|
+
getByAtomicLevel(level) {
|
|
1010
|
+
return this.getAll().filter((comp) => comp.atomicLevel === level);
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Serialize to JSON
|
|
1014
|
+
*/
|
|
1015
|
+
toJSON() {
|
|
1016
|
+
const obj = {};
|
|
1017
|
+
for (const [tagName, entry] of this.components) {
|
|
1018
|
+
obj[tagName] = entry;
|
|
1019
|
+
}
|
|
1020
|
+
return obj;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Deserialize from JSON
|
|
1024
|
+
*/
|
|
1025
|
+
static fromJSON(obj) {
|
|
1026
|
+
const index = new ComponentIndex();
|
|
1027
|
+
for (const [tagName, entry] of Object.entries(obj)) {
|
|
1028
|
+
index.components.set(tagName, entry);
|
|
1029
|
+
}
|
|
1030
|
+
return index;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
//# sourceMappingURL=component-index.js.map
|