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,446 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Component-centric migration workflow.
|
|
3
|
+
|
|
4
|
+
Analyzes a single Vue component's mixins, matches them to composables,
|
|
5
|
+
generates a migration report, and optionally injects setup() for
|
|
6
|
+
ready composables.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from ..core.component_analyzer import (
|
|
15
|
+
extract_own_members,
|
|
16
|
+
find_used_members,
|
|
17
|
+
parse_imports,
|
|
18
|
+
parse_mixins_array,
|
|
19
|
+
)
|
|
20
|
+
from ..core.composable_analyzer import (
|
|
21
|
+
extract_all_identifiers,
|
|
22
|
+
extract_function_name,
|
|
23
|
+
extract_return_keys,
|
|
24
|
+
)
|
|
25
|
+
from ..core.composable_search import (
|
|
26
|
+
find_composable_dirs,
|
|
27
|
+
generate_candidates,
|
|
28
|
+
search_for_composable,
|
|
29
|
+
)
|
|
30
|
+
from ..core.file_resolver import (
|
|
31
|
+
compute_import_path,
|
|
32
|
+
resolve_import_path,
|
|
33
|
+
try_resolve_with_extensions,
|
|
34
|
+
)
|
|
35
|
+
from ..core.mixin_analyzer import extract_lifecycle_hooks, extract_mixin_members
|
|
36
|
+
from ..models import (
|
|
37
|
+
ComposableCoverage,
|
|
38
|
+
FileChange,
|
|
39
|
+
MigrationConfig,
|
|
40
|
+
MigrationStatus,
|
|
41
|
+
MixinEntry,
|
|
42
|
+
MixinMembers,
|
|
43
|
+
)
|
|
44
|
+
from ..reporting.markdown import build_component_report
|
|
45
|
+
from ..reporting.terminal import bold, cyan, dim, green, red, yellow
|
|
46
|
+
from ..transform.injector import (
|
|
47
|
+
add_composable_import,
|
|
48
|
+
inject_setup,
|
|
49
|
+
remove_import_line,
|
|
50
|
+
remove_mixin_from_array,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def analyze_mixin(
|
|
55
|
+
local_name: str,
|
|
56
|
+
import_path: str,
|
|
57
|
+
component_path: Path,
|
|
58
|
+
component_source: str,
|
|
59
|
+
composable_dirs: list[Path],
|
|
60
|
+
project_root: Path,
|
|
61
|
+
component_own_members: set[str],
|
|
62
|
+
) -> Optional[MixinEntry]:
|
|
63
|
+
"""Analyze a single mixin: resolve file, extract members, find composable."""
|
|
64
|
+
|
|
65
|
+
# -- Resolve mixin file --
|
|
66
|
+
mixin_file = resolve_import_path(import_path, component_path)
|
|
67
|
+
if not mixin_file:
|
|
68
|
+
print(f" {yellow('WARNING')}: Could not resolve file for '{import_path}'. Skipping.")
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
print(f" File: {green(str(mixin_file))}")
|
|
72
|
+
|
|
73
|
+
# -- Extract mixin members and hooks --
|
|
74
|
+
mixin_source = mixin_file.read_text(errors="ignore")
|
|
75
|
+
members_dict = extract_mixin_members(mixin_source)
|
|
76
|
+
members = MixinMembers(**members_dict)
|
|
77
|
+
hooks = extract_lifecycle_hooks(mixin_source)
|
|
78
|
+
all_member_names = members.all_names
|
|
79
|
+
print(f" {len(all_member_names)} members, {len(hooks)} lifecycle hooks")
|
|
80
|
+
|
|
81
|
+
# -- Find which members the component actually uses --
|
|
82
|
+
used = find_used_members(component_source, all_member_names)
|
|
83
|
+
print(f" {len(used)} members used by component")
|
|
84
|
+
|
|
85
|
+
entry = MixinEntry(
|
|
86
|
+
local_name=local_name,
|
|
87
|
+
mixin_path=mixin_file,
|
|
88
|
+
mixin_stem=mixin_file.stem,
|
|
89
|
+
members=members,
|
|
90
|
+
lifecycle_hooks=hooks,
|
|
91
|
+
used_members=used,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# -- Search for matching composable (skip if no members are used) --
|
|
95
|
+
if not used:
|
|
96
|
+
print(f" {dim('No members used -- composable search skipped.')}")
|
|
97
|
+
entry.compute_status()
|
|
98
|
+
return entry
|
|
99
|
+
|
|
100
|
+
candidates = generate_candidates(mixin_file.stem)
|
|
101
|
+
print(f" Looking for: {cyan(', '.join(candidates))}")
|
|
102
|
+
|
|
103
|
+
matches = search_for_composable(mixin_file.stem, composable_dirs)
|
|
104
|
+
composable_file = None
|
|
105
|
+
|
|
106
|
+
if len(matches) == 1:
|
|
107
|
+
composable_file = matches[0]
|
|
108
|
+
print(f" Found: {green(str(composable_file))}")
|
|
109
|
+
elif len(matches) > 1:
|
|
110
|
+
print(f" {yellow('Multiple candidates found')}:")
|
|
111
|
+
for i, fp in enumerate(matches, 1):
|
|
112
|
+
print(f" {i}. {green(str(fp))}")
|
|
113
|
+
choice = input(f" Pick one (1-{len(matches)}), or 0 for none: ").strip()
|
|
114
|
+
try:
|
|
115
|
+
idx = int(choice)
|
|
116
|
+
if 1 <= idx <= len(matches):
|
|
117
|
+
composable_file = matches[idx - 1]
|
|
118
|
+
except ValueError:
|
|
119
|
+
pass
|
|
120
|
+
else:
|
|
121
|
+
print(f" {yellow('No composable found automatically.')}")
|
|
122
|
+
|
|
123
|
+
# If still no match, ask user
|
|
124
|
+
if not composable_file:
|
|
125
|
+
answer = input(f" Is there a composable for {green(mixin_file.stem)}? (y/n): ").strip().lower()
|
|
126
|
+
if answer == "y":
|
|
127
|
+
user_path = input(" Enter composable path: ").strip()
|
|
128
|
+
if user_path:
|
|
129
|
+
resolved = resolve_import_path(user_path, component_path)
|
|
130
|
+
if not resolved:
|
|
131
|
+
resolved = try_resolve_with_extensions((project_root / user_path).resolve())
|
|
132
|
+
if resolved:
|
|
133
|
+
composable_file = resolved
|
|
134
|
+
else:
|
|
135
|
+
print(f" {red('File not found')}: {user_path}")
|
|
136
|
+
|
|
137
|
+
# -- Analyze composable coverage --
|
|
138
|
+
if composable_file:
|
|
139
|
+
comp_source = composable_file.read_text(errors="ignore")
|
|
140
|
+
fn_name = extract_function_name(comp_source)
|
|
141
|
+
if not fn_name:
|
|
142
|
+
print(f" {yellow('Could not detect function name.')}")
|
|
143
|
+
fn_name = input(" Enter function name (e.g. useSelection): ").strip()
|
|
144
|
+
|
|
145
|
+
if fn_name:
|
|
146
|
+
all_identifiers = extract_all_identifiers(comp_source)
|
|
147
|
+
return_keys = extract_return_keys(comp_source)
|
|
148
|
+
import_path_str = compute_import_path(composable_file, project_root)
|
|
149
|
+
|
|
150
|
+
coverage = ComposableCoverage(
|
|
151
|
+
file_path=composable_file,
|
|
152
|
+
fn_name=fn_name,
|
|
153
|
+
import_path=import_path_str,
|
|
154
|
+
all_identifiers=all_identifiers,
|
|
155
|
+
return_keys=return_keys,
|
|
156
|
+
)
|
|
157
|
+
classification = coverage.classify_members(used, component_own_members)
|
|
158
|
+
|
|
159
|
+
entry.composable = coverage
|
|
160
|
+
entry.classification = classification
|
|
161
|
+
|
|
162
|
+
status = "READY" if classification.is_ready else "BLOCKED"
|
|
163
|
+
status_colored = green(status) if status == "READY" else red(status)
|
|
164
|
+
print(f" Status: {status_colored}", end="")
|
|
165
|
+
if classification.truly_missing:
|
|
166
|
+
print(f" | {red('Missing')}: {', '.join(classification.truly_missing)}", end="")
|
|
167
|
+
if classification.truly_not_returned:
|
|
168
|
+
print(f" | {yellow('Not returned')}: {', '.join(classification.truly_not_returned)}", end="")
|
|
169
|
+
if classification.overridden:
|
|
170
|
+
print(f" | {dim('Overridden by component')}: {', '.join(classification.overridden)}", end="")
|
|
171
|
+
if classification.overridden_not_returned:
|
|
172
|
+
print(f" | {dim('Overridden (not returned)')}: {', '.join(classification.overridden_not_returned)}", end="")
|
|
173
|
+
print()
|
|
174
|
+
|
|
175
|
+
entry.compute_status()
|
|
176
|
+
return entry
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def plan_injection(
|
|
180
|
+
component_path: Path,
|
|
181
|
+
entries_to_inject: list[MixinEntry],
|
|
182
|
+
) -> FileChange:
|
|
183
|
+
"""Plan injection changes without writing to disk.
|
|
184
|
+
|
|
185
|
+
Returns a FileChange with original and modified content.
|
|
186
|
+
"""
|
|
187
|
+
content = component_path.read_text(errors="ignore")
|
|
188
|
+
original = content
|
|
189
|
+
changes: list[str] = []
|
|
190
|
+
|
|
191
|
+
# Step 1: Swap imports
|
|
192
|
+
for entry in entries_to_inject:
|
|
193
|
+
new = remove_import_line(content, entry.mixin_stem)
|
|
194
|
+
if new != content:
|
|
195
|
+
changes.append(f"Removed import for {entry.mixin_stem}")
|
|
196
|
+
content = new
|
|
197
|
+
|
|
198
|
+
injectable = _get_injectable_members(entry)
|
|
199
|
+
if injectable and entry.composable:
|
|
200
|
+
comp = entry.composable
|
|
201
|
+
new = add_composable_import(content, comp.fn_name, comp.import_path)
|
|
202
|
+
if new != content:
|
|
203
|
+
changes.append(f"Added import {{ {comp.fn_name} }}")
|
|
204
|
+
content = new
|
|
205
|
+
elif not injectable:
|
|
206
|
+
changes.append(f"Skipped composable import for {entry.mixin_stem} (no injectable members)")
|
|
207
|
+
|
|
208
|
+
# Step 2: Remove processed mixins from the mixins: [] array
|
|
209
|
+
for entry in entries_to_inject:
|
|
210
|
+
new = remove_mixin_from_array(content, entry.local_name)
|
|
211
|
+
if new != content:
|
|
212
|
+
changes.append(f"Removed {entry.local_name} from mixins array")
|
|
213
|
+
content = new
|
|
214
|
+
|
|
215
|
+
# Step 3: Create or merge setup()
|
|
216
|
+
composable_calls = [
|
|
217
|
+
(entry.composable.fn_name, _get_injectable_members(entry))
|
|
218
|
+
for entry in entries_to_inject
|
|
219
|
+
if entry.composable and _get_injectable_members(entry)
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
if composable_calls:
|
|
223
|
+
new = inject_setup(content, composable_calls)
|
|
224
|
+
if new != content:
|
|
225
|
+
fn_names = [c[0] for c in composable_calls]
|
|
226
|
+
all_members = [m for _, members in composable_calls for m in members]
|
|
227
|
+
changes.append(f"setup() with {', '.join(fn_names)} -> {{ {', '.join(all_members)} }}")
|
|
228
|
+
content = new
|
|
229
|
+
|
|
230
|
+
# Clean up excessive blank lines
|
|
231
|
+
if content != original:
|
|
232
|
+
content = re.sub(r"\n{3,}", "\n\n", content)
|
|
233
|
+
|
|
234
|
+
return FileChange(
|
|
235
|
+
file_path=component_path,
|
|
236
|
+
original_content=original,
|
|
237
|
+
new_content=content,
|
|
238
|
+
changes=changes,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def apply_changes(file_change: FileChange) -> None:
|
|
243
|
+
"""Write planned changes to disk."""
|
|
244
|
+
if file_change.has_changes:
|
|
245
|
+
file_change.file_path.write_text(file_change.new_content)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _get_injectable_members(entry: MixinEntry) -> list[str]:
|
|
249
|
+
"""Get the list of members that should be destructured from the composable."""
|
|
250
|
+
if entry.classification:
|
|
251
|
+
return entry.classification.injectable
|
|
252
|
+
return entry.used_members
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def run(component_arg: str, config: MigrationConfig | None = None):
|
|
256
|
+
"""Main entry point for the component migration workflow."""
|
|
257
|
+
if config is None:
|
|
258
|
+
config = MigrationConfig()
|
|
259
|
+
|
|
260
|
+
project_root = config.project_root
|
|
261
|
+
component_path = Path(component_arg).resolve()
|
|
262
|
+
if not component_path.is_file():
|
|
263
|
+
sys.exit(f"Component not found: {component_path}")
|
|
264
|
+
|
|
265
|
+
# ---- Phase 1: Analyze ----
|
|
266
|
+
print(f"Analyzing: {green(component_arg)}")
|
|
267
|
+
component_source = component_path.read_text(errors="ignore")
|
|
268
|
+
|
|
269
|
+
all_imports = parse_imports(component_source)
|
|
270
|
+
mixin_names = parse_mixins_array(component_source)
|
|
271
|
+
|
|
272
|
+
if not mixin_names:
|
|
273
|
+
sys.exit("No mixins found in this component.")
|
|
274
|
+
|
|
275
|
+
print(f"Found {bold(str(len(mixin_names)))} mixin(s): {cyan(', '.join(mixin_names))}\n")
|
|
276
|
+
|
|
277
|
+
# Scan all Composables directories once
|
|
278
|
+
print("Scanning for Composables directories...")
|
|
279
|
+
composable_dirs = find_composable_dirs(project_root)
|
|
280
|
+
print(f"Found {bold(str(len(composable_dirs)))} director(ies).\n")
|
|
281
|
+
|
|
282
|
+
# Extract component's own members for override detection
|
|
283
|
+
component_own_members = extract_own_members(component_source)
|
|
284
|
+
|
|
285
|
+
# Analyze each mixin
|
|
286
|
+
mixin_entries: list[MixinEntry] = []
|
|
287
|
+
for local_name in mixin_names:
|
|
288
|
+
print(f"[{bold(local_name)}]")
|
|
289
|
+
import_path = all_imports.get(local_name)
|
|
290
|
+
if not import_path:
|
|
291
|
+
print(f" {yellow('WARNING')}: No import statement found for {local_name}. Skipping.")
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
entry = analyze_mixin(
|
|
295
|
+
local_name, import_path, component_path,
|
|
296
|
+
component_source, composable_dirs, project_root,
|
|
297
|
+
component_own_members,
|
|
298
|
+
)
|
|
299
|
+
if entry:
|
|
300
|
+
mixin_entries.append(entry)
|
|
301
|
+
|
|
302
|
+
if not mixin_entries:
|
|
303
|
+
sys.exit("No mixins could be processed.")
|
|
304
|
+
|
|
305
|
+
# ---- Phase 2: Report ----
|
|
306
|
+
print("\n" + "=" * 60)
|
|
307
|
+
print("Generating report...")
|
|
308
|
+
|
|
309
|
+
report = build_component_report(component_path, mixin_entries, project_root)
|
|
310
|
+
report_path = project_root / f"migration_{component_path.stem}.md"
|
|
311
|
+
report_path.write_text(report)
|
|
312
|
+
print(f"Report: {green(str(report_path))}")
|
|
313
|
+
|
|
314
|
+
# ---- Phase 3: Inject ----
|
|
315
|
+
|
|
316
|
+
# Split into ready vs blocked
|
|
317
|
+
ready = [e for e in mixin_entries if e.status == MigrationStatus.READY]
|
|
318
|
+
blocked = [e for e in mixin_entries if e.status != MigrationStatus.READY]
|
|
319
|
+
|
|
320
|
+
if not ready and not blocked:
|
|
321
|
+
print(f"\n{yellow('No mixins are ready for injection.')} Fix the issues in the report and re-run.")
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
# Show what's blocked
|
|
325
|
+
if blocked:
|
|
326
|
+
print(f"\n{yellow(str(len(blocked)))} mixin(s) still need work:")
|
|
327
|
+
for e in blocked:
|
|
328
|
+
comp = e.composable
|
|
329
|
+
cls = e.classification
|
|
330
|
+
detail_parts = []
|
|
331
|
+
if comp and cls:
|
|
332
|
+
if cls.truly_missing:
|
|
333
|
+
detail_parts.append(f"{red('missing')}: {', '.join(cls.truly_missing)}")
|
|
334
|
+
if cls.truly_not_returned:
|
|
335
|
+
detail_parts.append(f"{yellow('not returned')}: {', '.join(cls.truly_not_returned)}")
|
|
336
|
+
else:
|
|
337
|
+
detail_parts.append(yellow("no composable found"))
|
|
338
|
+
detail = f" ({'; '.join(detail_parts)})" if detail_parts else ""
|
|
339
|
+
print(f" - {red(e.mixin_stem)}{detail}")
|
|
340
|
+
|
|
341
|
+
# Show what's ready
|
|
342
|
+
if ready:
|
|
343
|
+
label = (
|
|
344
|
+
f"\n{green(str(len(ready)))} mixin(s) are ready:"
|
|
345
|
+
if blocked
|
|
346
|
+
else f"\nAll {green(str(len(ready)))} mixin(s) are ready:"
|
|
347
|
+
)
|
|
348
|
+
print(label)
|
|
349
|
+
|
|
350
|
+
for e in ready:
|
|
351
|
+
if not e.used_members:
|
|
352
|
+
print(f" - {green(e.mixin_stem)} {dim('(no members used -- will just remove mixin)')}")
|
|
353
|
+
else:
|
|
354
|
+
comp = e.composable
|
|
355
|
+
injectable = _get_injectable_members(e)
|
|
356
|
+
override_count = len(e.classification.overridden) + len(e.classification.overridden_not_returned) if e.classification else 0
|
|
357
|
+
override_note = f" {dim(f'({override_count} overridden by component)')}" if override_count else ""
|
|
358
|
+
print(f" - {green(e.mixin_stem)} -> {cyan(comp.fn_name)}() ({len(injectable)} members){override_note}")
|
|
359
|
+
|
|
360
|
+
# --- Manual unblock option ---
|
|
361
|
+
if blocked:
|
|
362
|
+
print(f"\n{bold('Unblock option')}: Some blocked mixins may have members that are intentionally")
|
|
363
|
+
print(f" overridden by another mixin or dynamically defined. You can force-unblock them.")
|
|
364
|
+
unblock_answer = input(
|
|
365
|
+
f"\n Would you like to unblock any of the {yellow(str(len(blocked)))} blocked mixin(s)? (y/n): "
|
|
366
|
+
).strip().lower()
|
|
367
|
+
|
|
368
|
+
if unblock_answer == "y":
|
|
369
|
+
unblockable = [e for e in blocked if e.composable]
|
|
370
|
+
non_unblockable = [e for e in blocked if not e.composable]
|
|
371
|
+
|
|
372
|
+
if non_unblockable:
|
|
373
|
+
print(f"\n {dim('Cannot unblock (no composable found):')}")
|
|
374
|
+
for e in non_unblockable:
|
|
375
|
+
print(f" - {dim(e.mixin_stem)}")
|
|
376
|
+
|
|
377
|
+
if unblockable:
|
|
378
|
+
print(f"\n Select mixin(s) to unblock (comma-separated numbers, or 'a' for all):")
|
|
379
|
+
for i, e in enumerate(unblockable, 1):
|
|
380
|
+
cls = e.classification
|
|
381
|
+
missing_list = (cls.truly_missing + cls.truly_not_returned) if cls else []
|
|
382
|
+
print(f" {i}. {yellow(e.mixin_stem)} -> {cyan(e.composable.fn_name)}()")
|
|
383
|
+
print(f" Unresolved members: {red(', '.join(missing_list))}")
|
|
384
|
+
print(f" {dim('These members will NOT be destructured from the composable.')}")
|
|
385
|
+
|
|
386
|
+
choice = input(f"\n Unblock (1-{len(unblockable)}, comma-sep, or 'a'): ").strip().lower()
|
|
387
|
+
|
|
388
|
+
indices_to_unblock: set[int] = set()
|
|
389
|
+
if choice == "a":
|
|
390
|
+
indices_to_unblock = set(range(len(unblockable)))
|
|
391
|
+
else:
|
|
392
|
+
for part in choice.split(","):
|
|
393
|
+
part = part.strip()
|
|
394
|
+
try:
|
|
395
|
+
idx = int(part) - 1
|
|
396
|
+
if 0 <= idx < len(unblockable):
|
|
397
|
+
indices_to_unblock.add(idx)
|
|
398
|
+
except ValueError:
|
|
399
|
+
pass
|
|
400
|
+
|
|
401
|
+
for idx in sorted(indices_to_unblock):
|
|
402
|
+
e = unblockable[idx]
|
|
403
|
+
cls = e.classification
|
|
404
|
+
if cls:
|
|
405
|
+
all_unresolved = set(cls.truly_missing + cls.truly_not_returned)
|
|
406
|
+
cls.injectable = [
|
|
407
|
+
m for m in e.used_members
|
|
408
|
+
if m not in all_unresolved
|
|
409
|
+
and m not in cls.overridden
|
|
410
|
+
and m not in cls.overridden_not_returned
|
|
411
|
+
]
|
|
412
|
+
e.status = MigrationStatus.FORCE_UNBLOCKED
|
|
413
|
+
blocked.remove(e)
|
|
414
|
+
ready.append(e)
|
|
415
|
+
print(f" {green('Unblocked')}: {e.mixin_stem}")
|
|
416
|
+
|
|
417
|
+
if not ready:
|
|
418
|
+
print(f"\n{yellow('No mixins are ready for injection.')} Fix the issues in the report and re-run.")
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
# Ask user
|
|
422
|
+
if blocked:
|
|
423
|
+
answer = input(
|
|
424
|
+
f"\nWould you like to inject the {green(str(len(ready)))} ready composable(s) now? "
|
|
425
|
+
f"(the {yellow(str(len(blocked)))} blocked one(s) will remain as mixins) (y/n): "
|
|
426
|
+
).strip().lower()
|
|
427
|
+
else:
|
|
428
|
+
answer = input("\nInject all composables? (y/n): ").strip().lower()
|
|
429
|
+
|
|
430
|
+
if answer != "y":
|
|
431
|
+
print("Skipped injection.")
|
|
432
|
+
return
|
|
433
|
+
|
|
434
|
+
# Plan and apply injection
|
|
435
|
+
print("\nInjecting...")
|
|
436
|
+
file_change = plan_injection(component_path, ready)
|
|
437
|
+
|
|
438
|
+
if file_change.has_changes:
|
|
439
|
+
apply_changes(file_change)
|
|
440
|
+
print(f"\n{green('Changes applied')}:")
|
|
441
|
+
for change in file_change.changes:
|
|
442
|
+
print(f" - {change}")
|
|
443
|
+
else:
|
|
444
|
+
print("\nNo changes needed.")
|
|
445
|
+
|
|
446
|
+
print(f"\n{bold('Done.')} Review the changes and test your application.")
|