create-caspian-app 1.0.0 → 1.0.2

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.
@@ -1,274 +1,274 @@
1
- """Template lint: reject JSX and unknown directives in PulsePoint markup.
2
-
3
- Why this exists
4
- ---------------
5
- `npm run check` validated Python only. A route template could contain JSX --
6
- `{users.map(user => (<tr/>))}`, `class={...}`, `className` -- and the gate stayed
7
- green, because nothing in the toolchain reads `.html` files. The failure then
8
- surfaced only in the browser, and the worst variant surfaced nowhere at all: an
9
- unquoted brace attribute is invalid HTML, so the parser shreds the element, the
10
- component root never compiles, the runtime's reveal step never clears
11
- `<body style="opacity: 0">`, and the route serves a blank page with **no console
12
- error**.
13
-
14
- PulsePoint borrows React's hook API inside `<script>` and React's component
15
- decomposition. It borrows none of React's markup syntax. This module enforces
16
- that boundary at authoring time, where the fix is cheap.
17
-
18
- Scope
19
- -----
20
- Scans authored markup under `src/`:
21
-
22
- - `**/*.html` -- route, layout, and component templates
23
- - `**/*.py` -- single-file components that embed markup via `html(\"\"\"...\"\"\")`
24
-
25
- Regions that legitimately contain JavaScript or sample code are removed before
26
- matching, so a real `array.map(...)` in a component script and a JSX snippet in
27
- a docs `<pre>` block are both ignored:
28
-
29
- - `<script>...</script>` (the component script is JS, `.map()` is correct there)
30
- - `<pre>` / `<code>` (documentation examples)
31
- - `<!-- ... -->` (commented-out markup)
32
-
33
- Usage:
34
-
35
- python settings/check_templates.py # run standalone
36
- npm run check # runs as part of the gate
37
- """
38
-
39
- from __future__ import annotations
40
-
41
- import re
42
- from dataclasses import dataclass
43
- from pathlib import Path
44
-
45
- PROJECT_ROOT = Path(__file__).resolve().parents[1]
46
- SCAN_ROOT = PROJECT_ROOT / "src"
47
-
48
- # Generated or vendored trees that are not hand-authored markup.
49
- EXCLUDED_PARTS = {"__pycache__", "node_modules", ".venv", "prisma"}
50
-
51
-
52
- @dataclass
53
- class TemplateIssue:
54
- path: str
55
- line: int
56
- column: int
57
- code: str
58
- message: str
59
-
60
-
61
- @dataclass
62
- class Rule:
63
- code: str
64
- pattern: re.Pattern[str]
65
- message: str
66
-
67
-
68
- # Each rule names the JSX/unsupported construct and the PulsePoint replacement,
69
- # so the report is directly actionable without opening the docs.
70
- RULES: list[Rule] = [
71
- Rule(
72
- "jsx-map",
73
- # `{items.map(item => (` and `{items.map((item, i) => (` -- the trailing
74
- # `(` is what distinguishes returning markup from a normal value map.
75
- re.compile(r"\{[^{}\n]*?\.map\s*\(\s*\(?[\w\s,]*\)?\s*=>\s*\(", re.MULTILINE),
76
- "JSX .map() returning markup. Use <template pp-for=\"item in items\"> "
77
- 'with key="{item.id}".',
78
- ),
79
- Rule(
80
- "jsx-logical",
81
- # `{cond && (<div` -- element after a logical AND.
82
- re.compile(r"\{[^{}]*?&&\s*\(\s*<", re.DOTALL),
83
- 'JSX `{cond && (<element/>)}`. Use hidden="{!cond}" on the element.',
84
- ),
85
- Rule(
86
- "jsx-ternary-element",
87
- # `{cond ? <A` -- element directly after a ternary branch.
88
- re.compile(r"\{[^{}]*?\?\s*\(?\s*<[a-zA-Z]", re.DOTALL),
89
- "JSX `{cond ? <A/> : <B/>}`. Use two elements with complementary "
90
- 'hidden="{...}" bindings.',
91
- ),
92
- Rule(
93
- "unquoted-brace-attr",
94
- # `class={...}` / `selected={...}` -- invalid HTML, silently blanks the page.
95
- re.compile(r"\s[\w:.\-]+=\{"),
96
- "Unquoted brace attribute. This is invalid HTML: the parser splits the "
97
- "value on spaces, the component root never compiles, and the page "
98
- 'renders blank with no console error. Quote it: attr="{expr}".',
99
- ),
100
- Rule(
101
- "react-attribute",
102
- re.compile(r"\s(className|htmlFor|dangerouslySetInnerHTML)\s*="),
103
- "React DOM property. Use class=, for=, or server-rendered markup.",
104
- ),
105
- Rule(
106
- "camelcase-event",
107
- # `onClick="…"` / `onClick={…}` in attribute position. The value-start
108
- # class and the declaration lookbehinds keep ordinary JavaScript out:
109
- # `const onPointerEnter = () => {` is a valid handler name in a component
110
- # script or an injected snippet, not a JSX prop.
111
- re.compile(
112
- r"(?<!\bconst )(?<!\blet )(?<!\bvar )(?<!\bfunction )"
113
- r"\son[A-Z][a-zA-Z]*\s*=\s*[\"'{]"
114
- ),
115
- "camelCase event prop. PulsePoint binds native lowercase event "
116
- 'attributes: onclick="{handler()}". A component prop uses kebab-case '
117
- '(on-click), which arrives as pp.props.onClick.',
118
- ),
119
- Rule(
120
- "jsx-fragment",
121
- re.compile(r"<>|</>"),
122
- "JSX fragment. A template needs exactly one real root element.",
123
- ),
124
- Rule(
125
- "style-object",
126
- re.compile(r"style\s*=\s*\{\{"),
127
- "JSX style object. pp-style takes a CSS *string*: "
128
- "pp-style=\"{'color: red'}\".",
129
- ),
130
- Rule(
131
- "unknown-directive",
132
- re.compile(r"\spp-(if|show|else|elif|key|class|text|html|model|bind|on)\s*[=>\s]"),
133
- "Directive does not exist in PulsePoint. Conditionals use "
134
- 'hidden="{...}", lists use <template pp-for>, keys use plain key="{...}".',
135
- ),
136
- ]
137
-
138
- # `pp-for` is valid only on <template>. Matching the opening tag it sits in is
139
- # enough: the attribute cannot appear before its own tag name.
140
- PP_FOR_TAG = re.compile(r"<\s*([a-zA-Z][\w:-]*)([^>]*?\spp-for\s*=)", re.DOTALL)
141
-
142
- SCRIPT_BLOCK = re.compile(r"<script\b.*?</script\s*>", re.IGNORECASE | re.DOTALL)
143
- PRE_BLOCK = re.compile(r"<(pre|code)\b.*?</\1\s*>", re.IGNORECASE | re.DOTALL)
144
- HTML_COMMENT = re.compile(r"<!--.*?-->", re.DOTALL)
145
-
146
- # In a single-file component the markup lives in a triple-quoted string handed to
147
- # `html(...)`. Surrounding Python must not be matched: `onValueChange=...` is an
148
- # ordinary keyword argument, and flagging it as a camelCase event prop would make
149
- # the gate unusable for every generated maddex component.
150
- TRIPLE_QUOTED = re.compile(r'"""(?:.|\n)*?"""|\'\'\'(?:.|\n)*?\'\'\'')
151
-
152
-
153
- def _blank_out(text: str, pattern: re.Pattern[str]) -> str:
154
- """Replace matched regions with same-length whitespace.
155
-
156
- Offsets stay valid, so reported line/column numbers still point at the real
157
- location in the original file.
158
- """
159
-
160
- def replace(match: re.Match[str]) -> str:
161
- return "".join("\n" if ch == "\n" else " " for ch in match.group(0))
162
-
163
- return pattern.sub(replace, text)
164
-
165
-
166
- def _keep_only(text: str, pattern: re.Pattern[str]) -> str:
167
- """Inverse of `_blank_out`: blank everything *outside* the matched regions."""
168
- kept = ["\n" if ch == "\n" else " " for ch in text]
169
- for match in pattern.finditer(text):
170
- for index in range(match.start(), match.end()):
171
- kept[index] = text[index]
172
- return "".join(kept)
173
-
174
-
175
- def _markup_only(text: str, *, is_python: bool) -> str:
176
- """Reduce a source file to just the markup a template rule may match."""
177
- if is_python:
178
- # Only triple-quoted regions can hold markup in a single-file component.
179
- text = _keep_only(text, TRIPLE_QUOTED)
180
- for pattern in (SCRIPT_BLOCK, PRE_BLOCK, HTML_COMMENT):
181
- text = _blank_out(text, pattern)
182
- return text
183
-
184
-
185
- def _position(text: str, offset: int) -> tuple[int, int]:
186
- line = text.count("\n", 0, offset) + 1
187
- line_start = text.rfind("\n", 0, offset) + 1
188
- return line, offset - line_start + 1
189
-
190
-
191
- def lint_text(text: str, rel_path: str, *, is_python: bool = False) -> list[TemplateIssue]:
192
- """Return every template issue found in one file's contents."""
193
- markup = _markup_only(text, is_python=is_python)
194
- issues: list[TemplateIssue] = []
195
-
196
- for rule in RULES:
197
- for match in rule.pattern.finditer(markup):
198
- line, column = _position(markup, match.start())
199
- issues.append(
200
- TemplateIssue(rel_path, line, column, rule.code, rule.message)
201
- )
202
-
203
- for match in PP_FOR_TAG.finditer(markup):
204
- tag = match.group(1).lower()
205
- if tag == "template":
206
- continue
207
- line, column = _position(markup, match.start())
208
- issues.append(
209
- TemplateIssue(
210
- rel_path,
211
- line,
212
- column,
213
- "pp-for-placement",
214
- f"pp-for on <{tag}>. It belongs only on <template>: "
215
- f'<template pp-for="item in items"><{tag} key="{{item.id}}">…',
216
- )
217
- )
218
-
219
- return issues
220
-
221
-
222
- def _iter_files() -> list[Path]:
223
- if not SCAN_ROOT.exists():
224
- return []
225
- files: list[Path] = []
226
- for pattern in ("**/*.html", "**/*.py"):
227
- for path in SCAN_ROOT.glob(pattern):
228
- if EXCLUDED_PARTS.intersection(path.parts):
229
- continue
230
- files.append(path)
231
- return sorted(files)
232
-
233
-
234
- def lint_templates() -> list[TemplateIssue]:
235
- """Lint every authored template under `src/`."""
236
- issues: list[TemplateIssue] = []
237
- for path in _iter_files():
238
- try:
239
- text = path.read_text(encoding="utf-8")
240
- except (OSError, UnicodeDecodeError):
241
- continue
242
- # Cheap pre-filter: a file with no brace expression and no angle-bracket
243
- # markup cannot trip any rule.
244
- if "{" not in text and "<" not in text:
245
- continue
246
- rel = path.relative_to(PROJECT_ROOT).as_posix()
247
- issues.extend(lint_text(text, rel, is_python=path.suffix == ".py"))
248
- return issues
249
-
250
-
251
- def main() -> int:
252
- issues = lint_templates()
253
- if not issues:
254
- print("templates: no JSX or unknown directives found.")
255
- return 0
256
-
257
- by_file: dict[str, list[TemplateIssue]] = {}
258
- for issue in issues:
259
- by_file.setdefault(issue.path, []).append(issue)
260
-
261
- for path in sorted(by_file):
262
- print(path)
263
- for issue in sorted(by_file[path], key=lambda i: (i.line, i.column)):
264
- print(
265
- f" {issue.line}:{issue.column} "
266
- f"[templates:{issue.code}] {issue.message}"
267
- )
268
-
269
- print(f"\n{len(issues)} template issue(s) found.")
270
- return 1
271
-
272
-
273
- if __name__ == "__main__":
274
- raise SystemExit(main())
1
+ """Template lint: reject JSX and unknown directives in PulsePoint markup.
2
+
3
+ Why this exists
4
+ ---------------
5
+ `npm run check` validated Python only. A route template could contain JSX --
6
+ `{users.map(user => (<tr/>))}`, `class={...}`, `className` -- and the gate stayed
7
+ green, because nothing in the toolchain reads `.html` files. The failure then
8
+ surfaced only in the browser, and the worst variant surfaced nowhere at all: an
9
+ unquoted brace attribute is invalid HTML, so the parser shreds the element, the
10
+ component root never compiles, the runtime's reveal step never clears
11
+ `<body style="opacity: 0">`, and the route serves a blank page with **no console
12
+ error**.
13
+
14
+ PulsePoint borrows React's hook API inside `<script>` and React's component
15
+ decomposition. It borrows none of React's markup syntax. This module enforces
16
+ that boundary at authoring time, where the fix is cheap.
17
+
18
+ Scope
19
+ -----
20
+ Scans authored markup under `src/`:
21
+
22
+ - `**/*.html` -- route, layout, and component templates
23
+ - `**/*.py` -- single-file components that embed markup via `html(\"\"\"...\"\"\")`
24
+
25
+ Regions that legitimately contain JavaScript or sample code are removed before
26
+ matching, so a real `array.map(...)` in a component script and a JSX snippet in
27
+ a docs `<pre>` block are both ignored:
28
+
29
+ - `<script>...</script>` (the component script is JS, `.map()` is correct there)
30
+ - `<pre>` / `<code>` (documentation examples)
31
+ - `<!-- ... -->` (commented-out markup)
32
+
33
+ Usage:
34
+
35
+ python settings/check_templates.py # run standalone
36
+ npm run check # runs as part of the gate
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import re
42
+ from dataclasses import dataclass
43
+ from pathlib import Path
44
+
45
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
46
+ SCAN_ROOT = PROJECT_ROOT / "src"
47
+
48
+ # Generated or vendored trees that are not hand-authored markup.
49
+ EXCLUDED_PARTS = {"__pycache__", "node_modules", ".venv", "prisma"}
50
+
51
+
52
+ @dataclass
53
+ class TemplateIssue:
54
+ path: str
55
+ line: int
56
+ column: int
57
+ code: str
58
+ message: str
59
+
60
+
61
+ @dataclass
62
+ class Rule:
63
+ code: str
64
+ pattern: re.Pattern[str]
65
+ message: str
66
+
67
+
68
+ # Each rule names the JSX/unsupported construct and the PulsePoint replacement,
69
+ # so the report is directly actionable without opening the docs.
70
+ RULES: list[Rule] = [
71
+ Rule(
72
+ "jsx-map",
73
+ # `{items.map(item => (` and `{items.map((item, i) => (` -- the trailing
74
+ # `(` is what distinguishes returning markup from a normal value map.
75
+ re.compile(r"\{[^{}\n]*?\.map\s*\(\s*\(?[\w\s,]*\)?\s*=>\s*\(", re.MULTILINE),
76
+ "JSX .map() returning markup. Use <template pp-for=\"item in items\"> "
77
+ 'with key="{item.id}".',
78
+ ),
79
+ Rule(
80
+ "jsx-logical",
81
+ # `{cond && (<div` -- element after a logical AND.
82
+ re.compile(r"\{[^{}]*?&&\s*\(\s*<", re.DOTALL),
83
+ 'JSX `{cond && (<element/>)}`. Use hidden="{!cond}" on the element.',
84
+ ),
85
+ Rule(
86
+ "jsx-ternary-element",
87
+ # `{cond ? <A` -- element directly after a ternary branch.
88
+ re.compile(r"\{[^{}]*?\?\s*\(?\s*<[a-zA-Z]", re.DOTALL),
89
+ "JSX `{cond ? <A/> : <B/>}`. Use two elements with complementary "
90
+ 'hidden="{...}" bindings.',
91
+ ),
92
+ Rule(
93
+ "unquoted-brace-attr",
94
+ # `class={...}` / `selected={...}` -- invalid HTML, silently blanks the page.
95
+ re.compile(r"\s[\w:.\-]+=\{"),
96
+ "Unquoted brace attribute. This is invalid HTML: the parser splits the "
97
+ "value on spaces, the component root never compiles, and the page "
98
+ 'renders blank with no console error. Quote it: attr="{expr}".',
99
+ ),
100
+ Rule(
101
+ "react-attribute",
102
+ re.compile(r"\s(className|htmlFor|dangerouslySetInnerHTML)\s*="),
103
+ "React DOM property. Use class=, for=, or server-rendered markup.",
104
+ ),
105
+ Rule(
106
+ "camelcase-event",
107
+ # `onClick="…"` / `onClick={…}` in attribute position. The value-start
108
+ # class and the declaration lookbehinds keep ordinary JavaScript out:
109
+ # `const onPointerEnter = () => {` is a valid handler name in a component
110
+ # script or an injected snippet, not a JSX prop.
111
+ re.compile(
112
+ r"(?<!\bconst )(?<!\blet )(?<!\bvar )(?<!\bfunction )"
113
+ r"\son[A-Z][a-zA-Z]*\s*=\s*[\"'{]"
114
+ ),
115
+ "camelCase event prop. PulsePoint binds native lowercase event "
116
+ 'attributes: onclick="{handler()}". A component prop uses kebab-case '
117
+ '(on-click), which arrives as pp.props.onClick.',
118
+ ),
119
+ Rule(
120
+ "jsx-fragment",
121
+ re.compile(r"<>|</>"),
122
+ "JSX fragment. A template needs exactly one real root element.",
123
+ ),
124
+ Rule(
125
+ "style-object",
126
+ re.compile(r"style\s*=\s*\{\{"),
127
+ "JSX style object. pp-style takes a CSS *string*: "
128
+ "pp-style=\"{'color: red'}\".",
129
+ ),
130
+ Rule(
131
+ "unknown-directive",
132
+ re.compile(r"\spp-(if|show|else|elif|key|class|text|html|model|bind|on)\s*[=>\s]"),
133
+ "Directive does not exist in PulsePoint. Conditionals use "
134
+ 'hidden="{...}", lists use <template pp-for>, keys use plain key="{...}".',
135
+ ),
136
+ ]
137
+
138
+ # `pp-for` is valid only on <template>. Matching the opening tag it sits in is
139
+ # enough: the attribute cannot appear before its own tag name.
140
+ PP_FOR_TAG = re.compile(r"<\s*([a-zA-Z][\w:-]*)([^>]*?\spp-for\s*=)", re.DOTALL)
141
+
142
+ SCRIPT_BLOCK = re.compile(r"<script\b.*?</script\s*>", re.IGNORECASE | re.DOTALL)
143
+ PRE_BLOCK = re.compile(r"<(pre|code)\b.*?</\1\s*>", re.IGNORECASE | re.DOTALL)
144
+ HTML_COMMENT = re.compile(r"<!--.*?-->", re.DOTALL)
145
+
146
+ # In a single-file component the markup lives in a triple-quoted string handed to
147
+ # `html(...)`. Surrounding Python must not be matched: `onValueChange=...` is an
148
+ # ordinary keyword argument, and flagging it as a camelCase event prop would make
149
+ # the gate unusable for every generated maddex component.
150
+ TRIPLE_QUOTED = re.compile(r'"""(?:.|\n)*?"""|\'\'\'(?:.|\n)*?\'\'\'')
151
+
152
+
153
+ def _blank_out(text: str, pattern: re.Pattern[str]) -> str:
154
+ """Replace matched regions with same-length whitespace.
155
+
156
+ Offsets stay valid, so reported line/column numbers still point at the real
157
+ location in the original file.
158
+ """
159
+
160
+ def replace(match: re.Match[str]) -> str:
161
+ return "".join("\n" if ch == "\n" else " " for ch in match.group(0))
162
+
163
+ return pattern.sub(replace, text)
164
+
165
+
166
+ def _keep_only(text: str, pattern: re.Pattern[str]) -> str:
167
+ """Inverse of `_blank_out`: blank everything *outside* the matched regions."""
168
+ kept = ["\n" if ch == "\n" else " " for ch in text]
169
+ for match in pattern.finditer(text):
170
+ for index in range(match.start(), match.end()):
171
+ kept[index] = text[index]
172
+ return "".join(kept)
173
+
174
+
175
+ def _markup_only(text: str, *, is_python: bool) -> str:
176
+ """Reduce a source file to just the markup a template rule may match."""
177
+ if is_python:
178
+ # Only triple-quoted regions can hold markup in a single-file component.
179
+ text = _keep_only(text, TRIPLE_QUOTED)
180
+ for pattern in (SCRIPT_BLOCK, PRE_BLOCK, HTML_COMMENT):
181
+ text = _blank_out(text, pattern)
182
+ return text
183
+
184
+
185
+ def _position(text: str, offset: int) -> tuple[int, int]:
186
+ line = text.count("\n", 0, offset) + 1
187
+ line_start = text.rfind("\n", 0, offset) + 1
188
+ return line, offset - line_start + 1
189
+
190
+
191
+ def lint_text(text: str, rel_path: str, *, is_python: bool = False) -> list[TemplateIssue]:
192
+ """Return every template issue found in one file's contents."""
193
+ markup = _markup_only(text, is_python=is_python)
194
+ issues: list[TemplateIssue] = []
195
+
196
+ for rule in RULES:
197
+ for match in rule.pattern.finditer(markup):
198
+ line, column = _position(markup, match.start())
199
+ issues.append(
200
+ TemplateIssue(rel_path, line, column, rule.code, rule.message)
201
+ )
202
+
203
+ for match in PP_FOR_TAG.finditer(markup):
204
+ tag = match.group(1).lower()
205
+ if tag == "template":
206
+ continue
207
+ line, column = _position(markup, match.start())
208
+ issues.append(
209
+ TemplateIssue(
210
+ rel_path,
211
+ line,
212
+ column,
213
+ "pp-for-placement",
214
+ f"pp-for on <{tag}>. It belongs only on <template>: "
215
+ f'<template pp-for="item in items"><{tag} key="{{item.id}}">…',
216
+ )
217
+ )
218
+
219
+ return issues
220
+
221
+
222
+ def _iter_files() -> list[Path]:
223
+ if not SCAN_ROOT.exists():
224
+ return []
225
+ files: list[Path] = []
226
+ for pattern in ("**/*.html", "**/*.py"):
227
+ for path in SCAN_ROOT.glob(pattern):
228
+ if EXCLUDED_PARTS.intersection(path.parts):
229
+ continue
230
+ files.append(path)
231
+ return sorted(files)
232
+
233
+
234
+ def lint_templates() -> list[TemplateIssue]:
235
+ """Lint every authored template under `src/`."""
236
+ issues: list[TemplateIssue] = []
237
+ for path in _iter_files():
238
+ try:
239
+ text = path.read_text(encoding="utf-8")
240
+ except (OSError, UnicodeDecodeError):
241
+ continue
242
+ # Cheap pre-filter: a file with no brace expression and no angle-bracket
243
+ # markup cannot trip any rule.
244
+ if "{" not in text and "<" not in text:
245
+ continue
246
+ rel = path.relative_to(PROJECT_ROOT).as_posix()
247
+ issues.extend(lint_text(text, rel, is_python=path.suffix == ".py"))
248
+ return issues
249
+
250
+
251
+ def main() -> int:
252
+ issues = lint_templates()
253
+ if not issues:
254
+ print("templates: no JSX or unknown directives found.")
255
+ return 0
256
+
257
+ by_file: dict[str, list[TemplateIssue]] = {}
258
+ for issue in issues:
259
+ by_file.setdefault(issue.path, []).append(issue)
260
+
261
+ for path in sorted(by_file):
262
+ print(path)
263
+ for issue in sorted(by_file[path], key=lambda i: (i.line, i.column)):
264
+ print(
265
+ f" {issue.line}:{issue.column} "
266
+ f"[templates:{issue.code}] {issue.message}"
267
+ )
268
+
269
+ print(f"\n{len(issues)} template issue(s) found.")
270
+ return 1
271
+
272
+
273
+ if __name__ == "__main__":
274
+ raise SystemExit(main())
@@ -981,7 +981,7 @@ function analyzeFile(filePath: string, rootDir: string): ComponentMetadata[] {
981
981
 
982
982
  function loadConfig(): CaspianConfig {
983
983
  if (!fs.existsSync(CONFIG_PATH)) {
984
- console.error(`Error: Configuration file not found at: ${CONFIG_PATH}`);
984
+ console.error(`Error: Configuration file not found at: ${CONFIG_PATH}`);
985
985
  process.exit(1);
986
986
  }
987
987
  return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
@@ -1024,7 +1024,7 @@ function walkDirectory(
1024
1024
  */
1025
1025
 
1026
1026
  export async function componentMap() {
1027
- console.log("Starting Component Analysis (Lezer AST-first)...");
1027
+ console.log("Starting Component Analysis (Lezer AST-first)...");
1028
1028
  const config = loadConfig();
1029
1029
  let allFiles: string[] = [];
1030
1030
 
@@ -1036,7 +1036,7 @@ export async function componentMap() {
1036
1036
  );
1037
1037
  });
1038
1038
 
1039
- console.log(`Found ${allFiles.length} Python files.`);
1039
+ console.log(`Found ${allFiles.length} Python files.`);
1040
1040
  const componentRegistry: ComponentMetadata[] = [];
1041
1041
 
1042
1042
  allFiles.forEach((file) => {
@@ -1044,15 +1044,15 @@ export async function componentMap() {
1044
1044
  const foundComponents = analyzeFile(file, PROJECT_ROOT);
1045
1045
  componentRegistry.push(...foundComponents);
1046
1046
  } catch (e) {
1047
- console.warn(`Warning: Failed to parse ${file}:`, e);
1047
+ console.warn(`Warning: Failed to parse ${file}:`, e);
1048
1048
  }
1049
1049
  });
1050
1050
 
1051
- console.log(`Discovered ${componentRegistry.length} Components.`);
1051
+ console.log(`Discovered ${componentRegistry.length} Components.`);
1052
1052
 
1053
1053
  const outputPath = path.join(__dirname, "component-map.json");
1054
1054
  if (componentRegistry.length > 0 || !fs.existsSync(outputPath)) {
1055
1055
  fs.writeFileSync(outputPath, JSON.stringify(componentRegistry, null, 2));
1056
- console.log(`Component map written to: ${outputPath}`);
1056
+ console.log(`Component map written to: ${outputPath}`);
1057
1057
  }
1058
1058
  }