feat: update component migration orchestrator and add deletion manifest script- Simplify COMPONENT_DIRS to process all TSX components under src/components, excluding specific files.- Introduce EXCLUDED_FILENAMES to filter out non-relevant TSX files.- Enhance documentation for component registry entry format and JSON return structure.- Add a new script to generate a deletion manifest for legacy TSX components with JSON equivalents.

This commit is contained in:
2026-01-20 01:11:50 +00:00
parent f7683ff19b
commit 02a47b8a93
2 changed files with 176 additions and 11 deletions

View File

@@ -32,11 +32,9 @@ ROOT = Path(__file__).resolve().parents[1]
DEFAULT_MODEL = os.getenv("CODEX_MODEL") or os.getenv("OPENAI_MODEL") or "gpt-4o-mini" DEFAULT_MODEL = os.getenv("CODEX_MODEL") or os.getenv("OPENAI_MODEL") or "gpt-4o-mini"
API_CALL_DELAY_SECONDS = 2.0 API_CALL_DELAY_SECONDS = 2.0
API_CALL_TIMEOUT_SECONDS = 120.0 API_CALL_TIMEOUT_SECONDS = 120.0
COMPONENT_DIRS = [ # Process all TSX components under src/components, excluding App, index, and test/story files.
ROOT / "src" / "components" / "atoms", COMPONENT_DIRS = [ROOT / "src" / "components"]
ROOT / "src" / "components" / "molecules", EXCLUDED_FILENAMES = {"app.tsx", "app.jsx", "index.tsx", "index.ts"}
ROOT / "src" / "components" / "organisms",
]
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -139,16 +137,17 @@ JSON definition schema (src/components/json-definitions/<component>.json):
"children": "text" | [{{ ... }}, "..."] "children": "text" | [{{ ... }}, "..."]
}} }}
- Use nested UIComponents in children. - Use nested UIComponents in children.
- For load.path use the component's folder under src/components: "{component_rel_dir}" (omit the subpath if empty).
Registry entry format (json-components-registry.json) for the component if missing: Registry entry format (json-components-registry.json) for the component if missing:
{{ {{
"type": "ComponentName", "type": "ComponentName",
"name": "ComponentName", "name": "ComponentName",
"category": "layout|input|display|navigation|feedback|data|custom", "category": "layout|input|display|navigation|feedback|data|custom",
"canHaveChildren": true|false, "canHaveChildren": true|false,
"description": "Short description", "description": "Short description",
"status": "supported", "status": "supported",
"source": "atoms|molecules|organisms|ui|custom", "source": "atoms|molecules|organisms|ui|custom|misc",
"jsonCompatible": true|false, "jsonCompatible": true|false,
"wrapperRequired": true|false, "wrapperRequired": true|false,
"load": {{ "load": {{
@@ -161,12 +160,12 @@ Registry entry format (json-components-registry.json) for the component if missi
"notes": "Optional notes" "notes": "Optional notes"
}} }}
}} }}
Omit optional fields when not applicable. Omit optional fields when not applicable. Use the provided category "{category}" for both category and source when in doubt.
Return ONLY valid JSON with this shape: Return ONLY valid JSON with this shape:
{{ {{
"componentName": "...", "componentName": "...",
"category": "atoms|molecules|organisms", "category": "{category}",
"isStateful": true|false, "isStateful": true|false,
"hook": {{ "hook": {{
"name": "useComponentName", "name": "useComponentName",
@@ -206,6 +205,7 @@ Return ONLY valid JSON with this shape:
Component category: {category} Component category: {category}
Component path: {path} Component path: {path}
Component subpath under src/components: {component_rel_dir}
Existing file contents for diffing: Existing file contents for diffing:
{existing_files} {existing_files}
@@ -363,9 +363,18 @@ def list_components(roots: Iterable[Path]) -> List[ComponentTarget]:
if not root.exists(): if not root.exists():
continue continue
for path in sorted(root.rglob("*.tsx")): for path in sorted(root.rglob("*.tsx")):
if path.name.lower() in EXCLUDED_FILENAMES:
continue
if re.search(r"\.(test|spec|stories)\.tsx$", path.name):
continue
name = path.stem name = path.stem
try:
rel = path.relative_to(root)
category = rel.parts[0] if len(rel.parts) > 1 else path.parent.name
except ValueError:
category = path.parent.name
targets.append( targets.append(
ComponentTarget(name=name, path=path, category=root.name) ComponentTarget(name=name, path=path, category=category)
) )
return targets return targets
@@ -584,6 +593,13 @@ def run_agent_for_component(
) -> Dict[str, Any]: ) -> Dict[str, Any]:
tsx = target.path.read_text(encoding="utf-8") tsx = target.path.read_text(encoding="utf-8")
config_file_name = f"{_to_kebab_case(target.name)}.json" config_file_name = f"{_to_kebab_case(target.name)}.json"
components_root = ROOT / "src" / "components"
try:
rel_dir = str(target.path.parent.relative_to(components_root))
except ValueError:
rel_dir = str(target.path.parent.relative_to(ROOT))
if rel_dir == ".":
rel_dir = ""
existing_files = { existing_files = {
"src/lib/json-ui/json-components.ts": _read_existing_file( "src/lib/json-ui/json-components.ts": _read_existing_file(
out_dir, "src/lib/json-ui/json-components.ts" out_dir, "src/lib/json-ui/json-components.ts"
@@ -610,6 +626,7 @@ def run_agent_for_component(
) )
prompt = PROMPT_TEMPLATE.format( prompt = PROMPT_TEMPLATE.format(
category=target.category, category=target.category,
component_rel_dir=rel_dir,
path=target.path, path=target.path,
tsx=tsx, tsx=tsx,
existing_files=existing_files_blob, existing_files=existing_files_blob,

View File

@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""
Generate a deletion manifest for legacy TSX components that have JSON equivalents.
Heuristics (no dependency on migration-out):
- Scan all src/components/**/*.tsx (excluding tests/stories).
- Record signals (not required to include):
- Matching JSON definition at src/components/json-definitions/<kebab>.json
- Matching config page at src/config/pages/<category>/<kebab>.json (only for atoms/molecules/organisms)
- Matching registry entry in json-components-registry.json
- All TSX files are listed; signals provide confidence for deletion.
Outputs:
- tsxs-to-delete.txt (one relative TSX path per line)
- tsxs-to-delete.json (array of {name, category, tsxPath, jsonDefinition, configPage?})
"""
from __future__ import annotations
import json
import re
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple
ROOT = Path(__file__).resolve().parents[1]
COMPONENTS_ROOT = ROOT / "src" / "components"
JSON_DEFS_DIR = ROOT / "src" / "components" / "json-definitions"
CONFIG_PAGES_DIR = ROOT / "src" / "config" / "pages"
REGISTRY_FILE = ROOT / "json-components-registry.json"
def to_kebab(name: str) -> str:
if not name:
return ""
return re.sub(r"([A-Z])", r"-\1", name).lower().lstrip("-")
def to_pascal(name: str) -> str:
if not name:
return ""
parts = re.split(r"[-_\\s]", name)
parts = [p for p in parts if p]
if len(parts) == 1:
s = parts[0]
return s[:1].upper() + s[1:]
return "".join(p[:1].upper() + p[1:] for p in parts)
def list_tsx_files() -> List[Tuple[str, Path]]:
files: List[Tuple[str, Path]] = []
if not COMPONENTS_ROOT.exists():
return files
for path in COMPONENTS_ROOT.rglob("*.tsx"):
if ".test." in path.name or ".stories." in path.name:
continue
parts = path.relative_to(COMPONENTS_ROOT).parts
category = parts[0] if parts else "components"
files.append((category, path))
return files
def load_registry_types() -> Set[str]:
if not REGISTRY_FILE.exists():
return set()
try:
data = json.loads(REGISTRY_FILE.read_text(encoding="utf-8"))
except Exception:
return set()
return {c.get("type", "") for c in data.get("components", []) if isinstance(c, dict)}
def matching_json_definition(name: str) -> Optional[Path]:
kebab = to_kebab(name)
candidate = JSON_DEFS_DIR / f"{kebab}.json"
return candidate if candidate.exists() else None
def matching_config_page(category: str, name: str) -> Optional[Path]:
kebab = to_kebab(name)
candidate = CONFIG_PAGES_DIR / category / f"{kebab}.json"
return candidate if candidate.exists() else None
def build_manifest() -> List[Dict[str, str]]:
registry_types = {t.lower() for t in load_registry_types() if t}
manifest: List[Dict[str, str]] = []
for category, tsx_path in list_tsx_files():
name = tsx_path.stem
kebab = to_kebab(name)
pascal = to_pascal(name)
signals: Dict[str, str] = {}
json_def = matching_json_definition(name)
if json_def:
signals["jsonDefinition"] = str(json_def.relative_to(ROOT))
config = matching_config_page(category, name)
if config:
signals["configPage"] = str(config.relative_to(ROOT))
if pascal.lower() in registry_types:
signals["registry"] = "present"
entry: Dict[str, str] = {
"name": name,
"category": category,
"tsxPath": str(tsx_path.relative_to(ROOT)),
**signals,
}
manifest.append(entry)
return manifest
def write_manifest(entries: List[Dict[str, str]]) -> None:
out_dir = ROOT
txt_path = out_dir / "tsxs-to-delete.txt"
json_path = out_dir / "tsxs-to-delete.json"
if not entries:
txt_path.write_text("", encoding="utf-8")
json_path.write_text("[]\n", encoding="utf-8")
print("No TSX files matched the deletion criteria.")
return
def render_line(e: Dict[str, str]) -> str:
parts = [e["tsxPath"]]
arrows: List[str] = []
if "jsonDefinition" in e:
arrows.append(f"JSON:{e['jsonDefinition']}")
if "configPage" in e:
arrows.append(f"CFG:{e['configPage']}")
if e.get("registry") == "present":
arrows.append("REG:yes")
if arrows:
parts.append("--> " + " | ".join(arrows))
return " ".join(parts)
txt_path.write_text("\n".join(render_line(e) for e in entries) + "\n", encoding="utf-8")
json_path.write_text(json.dumps(entries, indent=2) + "\n", encoding="utf-8")
print(f"Wrote {len(entries)} entries to {txt_path}")
print(f"Wrote {len(entries)} entries to {json_path}")
def main() -> int:
entries = build_manifest()
write_manifest(entries)
return 0
if __name__ == "__main__":
raise SystemExit(main())