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.
- package/bin/cli.js +41 -0
- package/package.json +24 -0
- package/vue3_migration/__init__.py +3 -0
- package/vue3_migration/__main__.py +6 -0
- package/vue3_migration/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vue3_migration/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vue3_migration/__pycache__/__main__.cpython-312.pyc +0 -0
- package/vue3_migration/__pycache__/cli.cpython-312.pyc +0 -0
- package/vue3_migration/__pycache__/models.cpython-312.pyc +0 -0
- package/vue3_migration/__pycache__/models.cpython-313.pyc +0 -0
- package/vue3_migration/cli.py +323 -0
- package/vue3_migration/core/__init__.py +0 -0
- package/vue3_migration/core/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vue3_migration/core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vue3_migration/core/__pycache__/component_analyzer.cpython-312.pyc +0 -0
- package/vue3_migration/core/__pycache__/component_analyzer.cpython-313.pyc +0 -0
- package/vue3_migration/core/__pycache__/composable_analyzer.cpython-312.pyc +0 -0
- package/vue3_migration/core/__pycache__/composable_analyzer.cpython-313.pyc +0 -0
- package/vue3_migration/core/__pycache__/composable_search.cpython-312.pyc +0 -0
- package/vue3_migration/core/__pycache__/composable_search.cpython-313.pyc +0 -0
- package/vue3_migration/core/__pycache__/file_resolver.cpython-312.pyc +0 -0
- package/vue3_migration/core/__pycache__/file_resolver.cpython-313.pyc +0 -0
- package/vue3_migration/core/__pycache__/js_parser.cpython-312.pyc +0 -0
- package/vue3_migration/core/__pycache__/js_parser.cpython-313.pyc +0 -0
- package/vue3_migration/core/__pycache__/mixin_analyzer.cpython-312.pyc +0 -0
- package/vue3_migration/core/__pycache__/mixin_analyzer.cpython-313.pyc +0 -0
- package/vue3_migration/core/component_analyzer.py +76 -0
- package/vue3_migration/core/composable_analyzer.py +86 -0
- package/vue3_migration/core/composable_search.py +141 -0
- package/vue3_migration/core/file_resolver.py +98 -0
- package/vue3_migration/core/js_parser.py +177 -0
- package/vue3_migration/core/mixin_analyzer.py +54 -0
- package/vue3_migration/models.py +168 -0
- package/vue3_migration/reporting/__init__.py +0 -0
- package/vue3_migration/reporting/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vue3_migration/reporting/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vue3_migration/reporting/__pycache__/markdown.cpython-312.pyc +0 -0
- package/vue3_migration/reporting/__pycache__/markdown.cpython-313.pyc +0 -0
- package/vue3_migration/reporting/__pycache__/terminal.cpython-312.pyc +0 -0
- package/vue3_migration/reporting/__pycache__/terminal.cpython-313.pyc +0 -0
- package/vue3_migration/reporting/markdown.py +202 -0
- package/vue3_migration/reporting/terminal.py +61 -0
- package/vue3_migration/transform/__init__.py +0 -0
- package/vue3_migration/transform/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vue3_migration/transform/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vue3_migration/transform/__pycache__/injector.cpython-312.pyc +0 -0
- package/vue3_migration/transform/__pycache__/injector.cpython-313.pyc +0 -0
- package/vue3_migration/transform/injector.py +134 -0
- package/vue3_migration/workflows/__init__.py +0 -0
- package/vue3_migration/workflows/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vue3_migration/workflows/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vue3_migration/workflows/__pycache__/component_workflow.cpython-312.pyc +0 -0
- package/vue3_migration/workflows/__pycache__/component_workflow.cpython-313.pyc +0 -0
- package/vue3_migration/workflows/__pycache__/mixin_workflow.cpython-312.pyc +0 -0
- package/vue3_migration/workflows/component_workflow.py +446 -0
- 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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
|
Binary file
|
|
Binary file
|