vue3-migration 1.0.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.
Files changed (56) hide show
  1. package/bin/cli.js +41 -0
  2. package/package.json +24 -0
  3. package/vue3_migration/__init__.py +3 -0
  4. package/vue3_migration/__main__.py +6 -0
  5. package/vue3_migration/__pycache__/__init__.cpython-312.pyc +0 -0
  6. package/vue3_migration/__pycache__/__init__.cpython-313.pyc +0 -0
  7. package/vue3_migration/__pycache__/__main__.cpython-312.pyc +0 -0
  8. package/vue3_migration/__pycache__/cli.cpython-312.pyc +0 -0
  9. package/vue3_migration/__pycache__/models.cpython-312.pyc +0 -0
  10. package/vue3_migration/__pycache__/models.cpython-313.pyc +0 -0
  11. package/vue3_migration/cli.py +323 -0
  12. package/vue3_migration/core/__init__.py +0 -0
  13. package/vue3_migration/core/__pycache__/__init__.cpython-312.pyc +0 -0
  14. package/vue3_migration/core/__pycache__/__init__.cpython-313.pyc +0 -0
  15. package/vue3_migration/core/__pycache__/component_analyzer.cpython-312.pyc +0 -0
  16. package/vue3_migration/core/__pycache__/component_analyzer.cpython-313.pyc +0 -0
  17. package/vue3_migration/core/__pycache__/composable_analyzer.cpython-312.pyc +0 -0
  18. package/vue3_migration/core/__pycache__/composable_analyzer.cpython-313.pyc +0 -0
  19. package/vue3_migration/core/__pycache__/composable_search.cpython-312.pyc +0 -0
  20. package/vue3_migration/core/__pycache__/composable_search.cpython-313.pyc +0 -0
  21. package/vue3_migration/core/__pycache__/file_resolver.cpython-312.pyc +0 -0
  22. package/vue3_migration/core/__pycache__/file_resolver.cpython-313.pyc +0 -0
  23. package/vue3_migration/core/__pycache__/js_parser.cpython-312.pyc +0 -0
  24. package/vue3_migration/core/__pycache__/js_parser.cpython-313.pyc +0 -0
  25. package/vue3_migration/core/__pycache__/mixin_analyzer.cpython-312.pyc +0 -0
  26. package/vue3_migration/core/__pycache__/mixin_analyzer.cpython-313.pyc +0 -0
  27. package/vue3_migration/core/component_analyzer.py +76 -0
  28. package/vue3_migration/core/composable_analyzer.py +86 -0
  29. package/vue3_migration/core/composable_search.py +141 -0
  30. package/vue3_migration/core/file_resolver.py +98 -0
  31. package/vue3_migration/core/js_parser.py +177 -0
  32. package/vue3_migration/core/mixin_analyzer.py +54 -0
  33. package/vue3_migration/models.py +168 -0
  34. package/vue3_migration/reporting/__init__.py +0 -0
  35. package/vue3_migration/reporting/__pycache__/__init__.cpython-312.pyc +0 -0
  36. package/vue3_migration/reporting/__pycache__/__init__.cpython-313.pyc +0 -0
  37. package/vue3_migration/reporting/__pycache__/markdown.cpython-312.pyc +0 -0
  38. package/vue3_migration/reporting/__pycache__/markdown.cpython-313.pyc +0 -0
  39. package/vue3_migration/reporting/__pycache__/terminal.cpython-312.pyc +0 -0
  40. package/vue3_migration/reporting/__pycache__/terminal.cpython-313.pyc +0 -0
  41. package/vue3_migration/reporting/markdown.py +202 -0
  42. package/vue3_migration/reporting/terminal.py +61 -0
  43. package/vue3_migration/transform/__init__.py +0 -0
  44. package/vue3_migration/transform/__pycache__/__init__.cpython-312.pyc +0 -0
  45. package/vue3_migration/transform/__pycache__/__init__.cpython-313.pyc +0 -0
  46. package/vue3_migration/transform/__pycache__/injector.cpython-312.pyc +0 -0
  47. package/vue3_migration/transform/__pycache__/injector.cpython-313.pyc +0 -0
  48. package/vue3_migration/transform/injector.py +134 -0
  49. package/vue3_migration/workflows/__init__.py +0 -0
  50. package/vue3_migration/workflows/__pycache__/__init__.cpython-312.pyc +0 -0
  51. package/vue3_migration/workflows/__pycache__/__init__.cpython-313.pyc +0 -0
  52. package/vue3_migration/workflows/__pycache__/component_workflow.cpython-312.pyc +0 -0
  53. package/vue3_migration/workflows/__pycache__/component_workflow.cpython-313.pyc +0 -0
  54. package/vue3_migration/workflows/__pycache__/mixin_workflow.cpython-312.pyc +0 -0
  55. package/vue3_migration/workflows/component_workflow.py +446 -0
  56. package/vue3_migration/workflows/mixin_workflow.py +387 -0
@@ -0,0 +1,202 @@
1
+ """
2
+ Markdown report generation for migration analysis results.
3
+ """
4
+
5
+ from pathlib import Path
6
+
7
+ from ..models import MixinEntry
8
+ from .terminal import md_green, md_yellow
9
+
10
+
11
+ def build_component_report(
12
+ component_path: Path,
13
+ mixin_entries: list[MixinEntry],
14
+ project_root: Path,
15
+ ) -> str:
16
+ """Build a markdown migration report for a single component."""
17
+ lines: list[str] = []
18
+ w = lines.append
19
+
20
+ try:
21
+ component_rel = component_path.relative_to(project_root)
22
+ except ValueError:
23
+ component_rel = component_path
24
+
25
+ w(f"# Migration Report: {md_green(str(component_rel))}\n")
26
+
27
+ ready_entries = []
28
+ blocked_entries = []
29
+
30
+ for entry in mixin_entries:
31
+ mixin_name = entry.mixin_stem
32
+ w(f"## Mixin: {mixin_name}\n")
33
+ w(f"**File:** {md_green(str(entry.mixin_path))}\n")
34
+
35
+ # Members breakdown
36
+ for section in ("data", "computed", "methods"):
37
+ section_members = getattr(entry.members, section)
38
+ if section_members:
39
+ w(f"**{section}:** {', '.join(section_members)}\n")
40
+
41
+ # Lifecycle hooks
42
+ if entry.lifecycle_hooks:
43
+ w(f"\n**Lifecycle hooks:** {', '.join(entry.lifecycle_hooks)}")
44
+ w("> Must be manually migrated (e.g. `mounted` -> `onMounted`).\n")
45
+
46
+ # Used members
47
+ used = entry.used_members
48
+ w(f"\n**Used by component:** {', '.join(used) if used else 'none detected'}\n")
49
+
50
+ # Composable analysis
51
+ comp = entry.composable
52
+ cls = entry.classification
53
+
54
+ if not entry.used_members:
55
+ w("**Status: READY** -- no members used, mixin can be safely removed.\n")
56
+ ready_entries.append(entry)
57
+ elif not comp or not cls:
58
+ w(f"**Composable:** {md_yellow('NOT FOUND')}\n")
59
+ blocked_entries.append(entry)
60
+ else:
61
+ w(f"**Composable:** {md_green(str(comp.file_path))}")
62
+ w(f"**Function:** `{comp.fn_name}`")
63
+ w(f"**Import path:** `{comp.import_path}`")
64
+ w(f"> {md_yellow('Verify the above path and function name are correct.')}\n")
65
+
66
+ if cls.truly_missing:
67
+ w(f"**MISSING from composable:** {', '.join(cls.truly_missing)}\n")
68
+ if cls.overridden:
69
+ w(f"**Overridden by component:** {', '.join(cls.overridden)}")
70
+ w("> These mixin members are redefined in the component itself, "
71
+ "so the composable doesn't need to provide them.\n")
72
+
73
+ if cls.truly_not_returned:
74
+ w(f"**NOT in return statement:** {', '.join(cls.truly_not_returned)}")
75
+ w("> These exist in the composable but are not returned, "
76
+ "so the component cannot access them.\n")
77
+ if cls.overridden_not_returned:
78
+ w(f"**Overridden (not returned):** {', '.join(cls.overridden_not_returned)}")
79
+ w("> Not returned by composable, but the component defines them itself.\n")
80
+
81
+ if cls.is_ready:
82
+ status_note = ""
83
+ override_count = len(cls.overridden) + len(cls.overridden_not_returned)
84
+ if override_count:
85
+ status_note = f" ({override_count} member(s) overridden by component)"
86
+ w(f"**Status: READY**{status_note} -- all needed members are present and returned.\n")
87
+ ready_entries.append(entry)
88
+ else:
89
+ blocked_entries.append(entry)
90
+
91
+ w("---\n")
92
+
93
+ # --- Actionable Summary ---
94
+ w("\n## Action Items\n")
95
+
96
+ if blocked_entries:
97
+ for entry in blocked_entries:
98
+ mixin_name = entry.mixin_stem
99
+ comp = entry.composable
100
+ cls = entry.classification
101
+
102
+ if not comp:
103
+ w(f"### {mixin_name}: Create composable")
104
+ w(f"- {md_yellow('A composable needs to be created')} for `{mixin_name}`.")
105
+ if entry.used_members:
106
+ w(f"- It must expose: {', '.join(entry.used_members)}\n")
107
+ else:
108
+ w(f"### {mixin_name}: Update `{comp.fn_name}`")
109
+ if cls and cls.missing:
110
+ w(f"- Add to composable: {', '.join(cls.missing)}")
111
+ if cls and cls.not_returned:
112
+ w(f"- Add to return statement: {', '.join(cls.not_returned)}")
113
+ w(f"- File: {md_green(str(comp.file_path))}\n")
114
+
115
+ if ready_entries:
116
+ w("### Ready for injection")
117
+ for entry in ready_entries:
118
+ if not entry.used_members:
119
+ w(f"- `{entry.mixin_stem}` -- no members used, will just remove mixin")
120
+ else:
121
+ comp = entry.composable
122
+ w(f"- `{entry.mixin_stem}` -> `{comp.fn_name}` ({len(entry.used_members)} members)")
123
+ w("")
124
+
125
+ if not blocked_entries:
126
+ w("All mixins are ready. Run the script again to inject.\n")
127
+ elif ready_entries:
128
+ w(f"{len(ready_entries)} of {len(mixin_entries)} mixin(s) are ready for partial injection.\n")
129
+ else:
130
+ w(f"{md_yellow('No mixins are ready for injection yet.')} Fix the issues above and re-run.\n")
131
+
132
+ return "\n".join(lines)
133
+
134
+
135
+ def build_audit_report(
136
+ mixin_path: Path,
137
+ members: dict[str, list[str]],
138
+ lifecycle_hooks: list[str],
139
+ importing_files: list[Path],
140
+ all_member_names: list[str],
141
+ composable_path_arg: str | None,
142
+ composable_identifiers: list[str],
143
+ composable_exists: bool,
144
+ project_root: Path,
145
+ usage_map: dict[str, list[str]],
146
+ ) -> str:
147
+ """Build a markdown audit report for a single mixin."""
148
+ lines: list[str] = []
149
+ w = lines.append
150
+
151
+ w(f"# Mixin Audit: {mixin_path.name}\n")
152
+
153
+ w("## Mixin Members\n")
154
+ for section in ("data", "computed", "methods"):
155
+ if members[section]:
156
+ w(f"**{section}:** {', '.join(members[section])}\n")
157
+
158
+ w("\n## Lifecycle Hooks\n")
159
+ if lifecycle_hooks:
160
+ w(f"{', '.join(lifecycle_hooks)}\n")
161
+ w("\n> These hooks contain logic that must be manually migrated "
162
+ "to the composable (e.g. `mounted` -> `onMounted`).\n")
163
+ else:
164
+ w("*No lifecycle hooks found in this mixin.*\n")
165
+
166
+ w(f"\n## Files Importing the Mixin ({len(importing_files)})\n")
167
+
168
+ for file_path in sorted(importing_files):
169
+ relative_path = file_path.relative_to(project_root)
170
+ used = usage_map.get(str(relative_path), [])
171
+ w(f"### {relative_path}\n")
172
+ if used:
173
+ w(f"Uses: {', '.join(used)}\n")
174
+ else:
175
+ w("Uses: *(none detected -- mixin may be unused here)*\n")
176
+
177
+ all_used_members = sorted(set(
178
+ member for used_list in usage_map.values() for member in used_list
179
+ ))
180
+
181
+ w("\n## Composable Status\n")
182
+ if not composable_path_arg:
183
+ w("**No composable path provided.** A composable should be created.\n")
184
+ elif not composable_exists:
185
+ w(f"**Composable file not found at `{composable_path_arg}`.** It should be created.\n")
186
+ else:
187
+ missing = [m for m in all_used_members if m not in composable_identifiers]
188
+ if missing:
189
+ w(f"**Missing from composable:** {', '.join(missing)}\n")
190
+ else:
191
+ w("All used members are present in the composable.\n")
192
+
193
+ w("\n## Summary\n")
194
+ w(f"- Total mixin members: {len(all_member_names)}\n")
195
+ w(f"- Lifecycle hooks: {len(lifecycle_hooks)}\n")
196
+ w(f"- Members used across codebase: {len(all_used_members)}\n")
197
+
198
+ unused_members = [m for m in all_member_names if m not in all_used_members]
199
+ if unused_members:
200
+ w(f"- Unused members (candidates for removal): {', '.join(unused_members)}\n")
201
+
202
+ return "\n".join(lines)
@@ -0,0 +1,61 @@
1
+ """
2
+ Terminal output helpers — ANSI colors and formatting for CLI display.
3
+ """
4
+
5
+ import sys
6
+
7
+ # ANSI escape codes
8
+ _GREEN = "\033[32m"
9
+ _YELLOW = "\033[33m"
10
+ _RED = "\033[31m"
11
+ _CYAN = "\033[36m"
12
+ _BOLD = "\033[1m"
13
+ _DIM = "\033[2m"
14
+ _RESET = "\033[0m"
15
+
16
+
17
+ def supports_color() -> bool:
18
+ """Check if the terminal supports ANSI color codes."""
19
+ if not hasattr(sys.stdout, "isatty"):
20
+ return False
21
+ if not sys.stdout.isatty():
22
+ return False
23
+ return True
24
+
25
+
26
+ def green(text: str) -> str:
27
+ return f"{_GREEN}{text}{_RESET}"
28
+
29
+
30
+ def yellow(text: str) -> str:
31
+ return f"{_YELLOW}{text}{_RESET}"
32
+
33
+
34
+ def red(text: str) -> str:
35
+ return f"{_RED}{text}{_RESET}"
36
+
37
+
38
+ def cyan(text: str) -> str:
39
+ return f"{_CYAN}{text}{_RESET}"
40
+
41
+
42
+ def bold(text: str) -> str:
43
+ return f"{_BOLD}{text}{_RESET}"
44
+
45
+
46
+ def dim(text: str) -> str:
47
+ return f"{_DIM}{text}{_RESET}"
48
+
49
+
50
+ def red_bold(text: str) -> str:
51
+ return f"{_RED}{_BOLD}{text}{_RESET}"
52
+
53
+
54
+ # -- Markdown colors (inline HTML for reports) --
55
+
56
+ def md_green(text: str) -> str:
57
+ return f'<span style="color:#2ea043">{text}</span>'
58
+
59
+
60
+ def md_yellow(text: str) -> str:
61
+ return f'<span style="color:#d29922">{text}</span>'
File without changes
@@ -0,0 +1,134 @@
1
+ """
2
+ Code injection — add/remove imports, manipulate mixins arrays, inject setup() functions.
3
+
4
+ All functions operate on source text strings and return modified text.
5
+ They do NOT perform file I/O — callers handle reading/writing.
6
+ """
7
+
8
+ import re
9
+
10
+ from ..core.js_parser import extract_brace_block
11
+
12
+
13
+ def add_composable_import(content: str, fn_name: str, import_path: str) -> str:
14
+ """Add composable import at the top of <script>. No-op if already present."""
15
+ if re.search(rf"\b{re.escape(fn_name)}\b", content):
16
+ return content
17
+
18
+ import_line = f"import {{ {fn_name} }} from '{import_path}'\n"
19
+
20
+ # Insert after <script> tag
21
+ script_match = re.search(r"(<script[^>]*>\s*\n?)", content)
22
+ if script_match:
23
+ pos = script_match.end()
24
+ return content[:pos] + import_line + content[pos:]
25
+
26
+ # For .js/.ts: after the last existing import
27
+ last_import = None
28
+ for match in re.finditer(r"^import\s+.+$", content, re.MULTILINE):
29
+ last_import = match
30
+ if last_import:
31
+ pos = last_import.end() + 1
32
+ return content[:pos] + import_line + content[pos:]
33
+
34
+ return import_line + content
35
+
36
+
37
+ def remove_import_line(content: str, mixin_stem: str) -> str:
38
+ """Delete the import line for a specific mixin."""
39
+ pattern = rf"""^[ \t]*import\s+\w+\s+from\s+['"].*?{re.escape(mixin_stem)}(?:\.(?:js|ts))?['"].*\n?"""
40
+ return re.sub(pattern, "", content, count=1, flags=re.MULTILINE)
41
+
42
+
43
+ def remove_mixin_from_array(content: str, local_name: str) -> str:
44
+ """Remove a mixin from `mixins: [...]`. Drops the whole line if array empties."""
45
+ match = re.search(r"(\s*)mixins\s*:\s*\[([^\]]*)\](\s*,?)", content)
46
+ if not match:
47
+ return content
48
+
49
+ indent, inner, trailing = match.group(1), match.group(2), match.group(3)
50
+ items = [x.strip() for x in inner.split(",") if x.strip()]
51
+ remaining = [x for x in items if x != local_name]
52
+
53
+ if not remaining:
54
+ return re.sub(r"[ \t]*mixins\s*:\s*\[[^\]]*\]\s*,?[ \t]*\n?", "", content, count=1)
55
+
56
+ rebuilt = f"{indent}mixins: [{', '.join(remaining)}]{trailing}"
57
+ return content[:match.start()] + rebuilt + content[match.end():]
58
+
59
+
60
+ def inject_setup(
61
+ content: str,
62
+ composable_calls: list[tuple[str, list[str]]],
63
+ indent: str = " ",
64
+ ) -> str:
65
+ """Create or merge setup() with multiple composable destructuring calls.
66
+
67
+ Args:
68
+ content: The component source text.
69
+ composable_calls: List of (fn_name, [member1, member2, ...]) tuples.
70
+ indent: Indentation string (default 2 spaces).
71
+
72
+ Returns:
73
+ Modified source text with setup() containing all composable calls.
74
+ """
75
+ all_returned_members = []
76
+ call_lines = []
77
+ for fn_name, members in composable_calls:
78
+ call_lines.append(f"{indent}{indent}const {{ {', '.join(members)} }} = {fn_name}()")
79
+ all_returned_members.extend(members)
80
+
81
+ # --- Existing setup(): prepend calls, merge into return ---
82
+ setup_match = re.search(r"\bsetup\s*\([^)]*\)\s*\{", content)
83
+ if setup_match:
84
+ # Insert composable calls as first lines
85
+ insert_pos = setup_match.end()
86
+ injection = "\n" + "\n".join(call_lines) + "\n"
87
+ content = content[:insert_pos] + injection + content[insert_pos:]
88
+
89
+ # Merge members into existing return statement
90
+ ret_match = re.search(r"\breturn\s*\{", content[setup_match.start():])
91
+ if ret_match:
92
+ abs_pos = setup_match.start() + ret_match.end()
93
+ block_start = abs_pos - 1
94
+ existing_return = extract_brace_block(content, block_start)
95
+ existing_keys = set(re.findall(r"\b(\w+)\b", existing_return))
96
+ new_keys = [m for m in all_returned_members if m not in existing_keys]
97
+ if new_keys:
98
+ content = content[:abs_pos] + " " + ", ".join(new_keys) + "," + content[abs_pos:]
99
+ else:
100
+ # No return yet -- add one before closing brace
101
+ body_start = setup_match.end() - 1
102
+ body = extract_brace_block(content, body_start)
103
+ close_pos = body_start + 1 + len(body)
104
+ ret_stmt = f"\n{indent}{indent}return {{ {', '.join(all_returned_members)} }}\n{indent}"
105
+ content = content[:close_pos] + ret_stmt + content[close_pos:]
106
+
107
+ return content
108
+
109
+ # --- No setup(): create one ---
110
+ lines = [f"{indent}setup() {{"]
111
+ lines.extend(call_lines)
112
+ lines.append("")
113
+ lines.append(f"{indent}{indent}return {{ {', '.join(all_returned_members)} }}")
114
+ lines.append(f"{indent}}},")
115
+ setup_block = "\n".join(lines) + "\n"
116
+
117
+ # Insert before data()
118
+ data_match = re.search(r"^(\s*)data\s*\(\s*\)\s*\{", content, re.MULTILINE)
119
+ if data_match:
120
+ return content[:data_match.start()] + setup_block + content[data_match.start():]
121
+
122
+ # Fallback: after export default {
123
+ export_match = re.search(r"export\s+default\s*\{[ \t]*\n?", content)
124
+ if export_match:
125
+ return content[:export_match.end()] + setup_block + content[export_match.end():]
126
+
127
+ return content
128
+
129
+
130
+ def find_mixin_import_name(content: str, mixin_stem: str) -> str | None:
131
+ """Find the local import name for a mixin by its file stem."""
132
+ pattern = rf"""import\s+(\w+)\s+from\s+['"].*?{re.escape(mixin_stem)}(?:\.(?:js|ts))?['"]"""
133
+ match = re.search(pattern, content)
134
+ return match.group(1) if match else None
File without changes