vue3-migration 1.2.0 → 1.3.1
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/package.json +1 -1
- package/vue3_migration/core/composable_analyzer.py +6 -4
- package/vue3_migration/core/warning_collector.py +7 -4
- package/vue3_migration/models.py +1 -0
- package/vue3_migration/reporting/diff.py +8 -0
- package/vue3_migration/reporting/markdown.py +84 -24
- package/vue3_migration/transform/composable_patcher.py +38 -2
- package/vue3_migration/workflows/auto_migrate_workflow.py +56 -132
package/package.json
CHANGED
|
@@ -34,8 +34,9 @@ def extract_all_identifiers(source: str) -> list[str]:
|
|
|
34
34
|
else:
|
|
35
35
|
ids.add(part.split("=")[0].strip())
|
|
36
36
|
|
|
37
|
-
# Return keys: return { foo, bar, baz: val }
|
|
38
|
-
|
|
37
|
+
# Return keys: return { foo, bar, baz: val } — use LAST match to skip nested returns
|
|
38
|
+
ret_matches = list(re.finditer(r"\breturn\s*\{", source))
|
|
39
|
+
ret = ret_matches[-1] if ret_matches else None
|
|
39
40
|
if ret:
|
|
40
41
|
block = extract_brace_block(source, ret.end() - 1)
|
|
41
42
|
ids.update(re.findall(r"\b(\w+)\s*[,}\n:]", block))
|
|
@@ -59,8 +60,9 @@ def extract_return_keys(source: str) -> list[str]:
|
|
|
59
60
|
"null", "undefined", "new", "value",
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
# Case 1: direct return { ... }
|
|
63
|
-
|
|
63
|
+
# Case 1: direct return { ... } — use LAST match to skip nested returns
|
|
64
|
+
matches = list(re.finditer(r"\breturn\s*\{", source))
|
|
65
|
+
ret = matches[-1] if matches else None
|
|
64
66
|
if ret:
|
|
65
67
|
block = extract_brace_block(source, ret.end() - 1)
|
|
66
68
|
keys = re.findall(r"\b(\w+)\b", block)
|
|
@@ -321,7 +321,8 @@ def inject_inline_warnings(
|
|
|
321
321
|
for line in lines:
|
|
322
322
|
if not injected and w.line_hint in line:
|
|
323
323
|
indent = line[: len(line) - len(line.lstrip())]
|
|
324
|
-
|
|
324
|
+
for msg_line in f"// ⚠ MIGRATION: {w.message}".splitlines():
|
|
325
|
+
new_lines.append(f"{indent}{msg_line}\n")
|
|
325
326
|
injected = True
|
|
326
327
|
new_lines.append(line)
|
|
327
328
|
if injected:
|
|
@@ -331,9 +332,11 @@ def inject_inline_warnings(
|
|
|
331
332
|
|
|
332
333
|
# Place unmatched warnings as a block after the confidence header
|
|
333
334
|
if unplaced:
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
335
|
+
block_lines: list[str] = []
|
|
336
|
+
for w in unplaced:
|
|
337
|
+
for msg_line in f"// ⚠ MIGRATION: {w.message}".splitlines():
|
|
338
|
+
block_lines.append(f"{msg_line}\n")
|
|
339
|
+
block = "".join(block_lines)
|
|
337
340
|
if confidence is not None:
|
|
338
341
|
# Insert right after the first line (confidence header)
|
|
339
342
|
idx = source.index('\n') + 1
|
package/vue3_migration/models.py
CHANGED
|
@@ -178,6 +178,7 @@ class MigrationPlan:
|
|
|
178
178
|
"""All planned file changes for a project-wide auto-migrate run."""
|
|
179
179
|
component_changes: list["FileChange"] = field(default_factory=list)
|
|
180
180
|
composable_changes: list["FileChange"] = field(default_factory=list)
|
|
181
|
+
entries_by_component: list[tuple[Path, list["MixinEntry"]]] = field(default_factory=list)
|
|
181
182
|
|
|
182
183
|
@property
|
|
183
184
|
def all_changes(self) -> list["FileChange"]:
|
|
@@ -7,6 +7,7 @@ from datetime import datetime
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
from ..models import FileChange, MigrationPlan
|
|
10
|
+
from .markdown import build_warning_summary
|
|
10
11
|
from .terminal import bold, dim, green
|
|
11
12
|
|
|
12
13
|
|
|
@@ -188,6 +189,13 @@ def write_diff_report(plan: MigrationPlan, project_root: Path) -> Path:
|
|
|
188
189
|
"",
|
|
189
190
|
]
|
|
190
191
|
|
|
192
|
+
# Prepend warning summary before diffs
|
|
193
|
+
if plan.entries_by_component:
|
|
194
|
+
summary = build_warning_summary(plan.entries_by_component, plan.composable_changes)
|
|
195
|
+
if summary:
|
|
196
|
+
sections.append(summary)
|
|
197
|
+
sections.append("")
|
|
198
|
+
|
|
191
199
|
all_changes = [c for c in (plan.composable_changes + plan.component_changes) if c.has_changes]
|
|
192
200
|
|
|
193
201
|
for change in all_changes:
|
|
@@ -4,7 +4,7 @@ Markdown report generation for migration analysis results.
|
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
from ..models import ConfidenceLevel, MixinEntry
|
|
7
|
+
from ..models import ConfidenceLevel, FileChange, MixinEntry
|
|
8
8
|
from .terminal import md_green, md_yellow
|
|
9
9
|
|
|
10
10
|
|
|
@@ -344,37 +344,97 @@ def build_audit_report(
|
|
|
344
344
|
return "\n".join(lines)
|
|
345
345
|
|
|
346
346
|
|
|
347
|
-
def
|
|
348
|
-
|
|
349
|
-
|
|
347
|
+
def build_warning_summary(
|
|
348
|
+
entries_by_component: "list[tuple[Path, list[MixinEntry]]]",
|
|
349
|
+
composable_changes: "list[FileChange] | None" = None,
|
|
350
350
|
) -> str:
|
|
351
|
-
"""Build a markdown Migration
|
|
351
|
+
"""Build a markdown Migration Summary checklist for the diff report.
|
|
352
352
|
|
|
353
|
-
Groups warnings by mixin
|
|
354
|
-
and
|
|
353
|
+
Groups warnings by mixin/composable with confidence levels, severity
|
|
354
|
+
icons, and actionable checkboxes. De-duplicates entries that share the
|
|
355
|
+
same mixin_stem (a mixin used by multiple components).
|
|
355
356
|
"""
|
|
356
|
-
|
|
357
|
+
from ..core.warning_collector import compute_confidence
|
|
358
|
+
|
|
359
|
+
# De-duplicate entries by mixin_stem (keep first occurrence)
|
|
360
|
+
seen_stems: set[str] = set()
|
|
361
|
+
unique_entries: list[MixinEntry] = []
|
|
362
|
+
for _comp_path, entry_list in entries_by_component:
|
|
363
|
+
for entry in entry_list:
|
|
364
|
+
if entry.mixin_stem not in seen_stems:
|
|
365
|
+
seen_stems.add(entry.mixin_stem)
|
|
366
|
+
unique_entries.append(entry)
|
|
367
|
+
|
|
368
|
+
if not unique_entries:
|
|
357
369
|
return ""
|
|
358
370
|
|
|
359
|
-
|
|
360
|
-
|
|
371
|
+
# Build a lookup of composable content by file path
|
|
372
|
+
composable_content_map: dict[Path, str] = {}
|
|
373
|
+
if composable_changes:
|
|
374
|
+
for change in composable_changes:
|
|
375
|
+
if change.has_changes:
|
|
376
|
+
composable_content_map[change.file_path] = change.new_content
|
|
377
|
+
|
|
378
|
+
# Compute confidence for each entry
|
|
379
|
+
confidence_map: dict[str, ConfidenceLevel] = {}
|
|
380
|
+
for entry in unique_entries:
|
|
381
|
+
comp_source = ""
|
|
382
|
+
if entry.composable and entry.composable.file_path in composable_content_map:
|
|
383
|
+
comp_source = composable_content_map[entry.composable.file_path]
|
|
384
|
+
if comp_source:
|
|
385
|
+
confidence_map[entry.mixin_stem] = compute_confidence(comp_source, entry.warnings)
|
|
386
|
+
elif any(w.severity == "error" for w in entry.warnings):
|
|
387
|
+
confidence_map[entry.mixin_stem] = ConfidenceLevel.LOW
|
|
388
|
+
elif entry.warnings:
|
|
389
|
+
confidence_map[entry.mixin_stem] = ConfidenceLevel.MEDIUM
|
|
390
|
+
else:
|
|
391
|
+
confidence_map[entry.mixin_stem] = ConfidenceLevel.HIGH
|
|
361
392
|
|
|
362
|
-
|
|
393
|
+
# Sort: LOW first, then MEDIUM, then HIGH (most urgent at top)
|
|
394
|
+
_CONF_ORDER = {ConfidenceLevel.LOW: 0, ConfidenceLevel.MEDIUM: 1, ConfidenceLevel.HIGH: 2}
|
|
395
|
+
unique_entries.sort(key=lambda e: _CONF_ORDER.get(confidence_map[e.mixin_stem], 2))
|
|
363
396
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
397
|
+
# Count totals
|
|
398
|
+
all_warnings = [w for e in unique_entries for w in e.warnings]
|
|
399
|
+
error_count = sum(1 for w in all_warnings if w.severity == "error")
|
|
400
|
+
warning_count = sum(1 for w in all_warnings if w.severity == "warning")
|
|
401
|
+
info_count = sum(1 for w in all_warnings if w.severity == "info")
|
|
368
402
|
|
|
369
|
-
|
|
403
|
+
lines: list[str] = []
|
|
404
|
+
a = lines.append
|
|
405
|
+
|
|
406
|
+
a("## Migration Summary\n")
|
|
407
|
+
|
|
408
|
+
# Overview line
|
|
409
|
+
parts = [f"**{len(unique_entries)} composable{'s' if len(unique_entries) != 1 else ''}**"]
|
|
410
|
+
if error_count:
|
|
411
|
+
parts.append(f"{error_count} error{'s' if error_count != 1 else ''}")
|
|
412
|
+
if warning_count:
|
|
413
|
+
parts.append(f"{warning_count} warning{'s' if warning_count != 1 else ''}")
|
|
414
|
+
if info_count:
|
|
415
|
+
parts.append(f"{info_count} info")
|
|
416
|
+
a(" | ".join(parts))
|
|
417
|
+
a("")
|
|
418
|
+
a("---\n")
|
|
419
|
+
|
|
420
|
+
# Per-mixin sections
|
|
421
|
+
_SEVERITY_ICON = {"error": "\u274c", "warning": "\u26a0\ufe0f", "info": "\u2139\ufe0f"}
|
|
422
|
+
_CONF_ICON = {ConfidenceLevel.LOW: "\u274c", ConfidenceLevel.MEDIUM: "\u26a0\ufe0f", ConfidenceLevel.HIGH: "\u2705"}
|
|
423
|
+
|
|
424
|
+
for entry in unique_entries:
|
|
425
|
+
conf = confidence_map[entry.mixin_stem]
|
|
426
|
+
icon = _CONF_ICON.get(conf, "\u2753")
|
|
427
|
+
|
|
428
|
+
a(f"### {icon} {entry.mixin_stem} \u2014 {conf.value} confidence\n")
|
|
429
|
+
|
|
430
|
+
if not entry.warnings:
|
|
431
|
+
a("No manual changes needed.\n")
|
|
432
|
+
continue
|
|
370
433
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
w("")
|
|
377
|
-
else:
|
|
378
|
-
w("No warnings detected.\n")
|
|
434
|
+
for warning in entry.warnings:
|
|
435
|
+
sev_icon = _SEVERITY_ICON.get(warning.severity, "\u2753")
|
|
436
|
+
a(f"- [ ] **{warning.category}** ({warning.severity}): {warning.message}")
|
|
437
|
+
a(f" \u2192 {warning.action_required}")
|
|
438
|
+
a("")
|
|
379
439
|
|
|
380
440
|
return "\n".join(lines)
|
|
@@ -8,7 +8,9 @@ from ..core.warning_collector import (
|
|
|
8
8
|
)
|
|
9
9
|
from ..models import MixinMembers
|
|
10
10
|
from .this_rewriter import rewrite_this_refs, rewrite_this_dollar_refs
|
|
11
|
-
from .lifecycle_converter import
|
|
11
|
+
from .lifecycle_converter import (
|
|
12
|
+
extract_hook_body, convert_lifecycle_hooks, get_required_imports, HOOK_MAP,
|
|
13
|
+
)
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
def _add_vue_import(content: str, name: str) -> str:
|
|
@@ -314,12 +316,31 @@ def generate_member_declaration(
|
|
|
314
316
|
return f"{indent}// {name} — could not classify, migrate manually"
|
|
315
317
|
|
|
316
318
|
|
|
319
|
+
def _missing_hooks(composable_src: str, hooks: list[str]) -> list[str]:
|
|
320
|
+
"""Return lifecycle hooks not yet present in the composable source.
|
|
321
|
+
|
|
322
|
+
For wrapped hooks (onMounted etc.), checks for ``fnName(`` to detect calls.
|
|
323
|
+
Inline hooks (beforeCreate/created) are always considered missing since
|
|
324
|
+
there's no reliable wrapper to detect.
|
|
325
|
+
"""
|
|
326
|
+
missing: list[str] = []
|
|
327
|
+
for hook in hooks:
|
|
328
|
+
vue3_fn = HOOK_MAP.get(hook)
|
|
329
|
+
if vue3_fn is None:
|
|
330
|
+
# beforeCreate/created — inline, always treat as missing
|
|
331
|
+
missing.append(hook)
|
|
332
|
+
elif f"{vue3_fn}(" not in composable_src:
|
|
333
|
+
missing.append(hook)
|
|
334
|
+
return missing
|
|
335
|
+
|
|
336
|
+
|
|
317
337
|
def patch_composable(
|
|
318
338
|
composable_content: str,
|
|
319
339
|
mixin_content: str,
|
|
320
340
|
not_returned: list[str],
|
|
321
341
|
missing: list[str],
|
|
322
342
|
mixin_members: MixinMembers,
|
|
343
|
+
lifecycle_hooks: list[str] | None = None,
|
|
323
344
|
indent: str = " ",
|
|
324
345
|
) -> str:
|
|
325
346
|
"""Orchestrate composable patching for both blocked cases.
|
|
@@ -329,6 +350,8 @@ def patch_composable(
|
|
|
329
350
|
Step 1 (not_returned): Add keys to the return statement.
|
|
330
351
|
Step 2 (missing): Generate declarations and insert before return,
|
|
331
352
|
then add the names to the return statement.
|
|
353
|
+
Step 3 (lifecycle_hooks): Convert and insert lifecycle hooks that the
|
|
354
|
+
composable doesn't already contain.
|
|
332
355
|
|
|
333
356
|
Returns modified composable content (unchanged if reactive() guard triggered).
|
|
334
357
|
"""
|
|
@@ -359,13 +382,26 @@ def patch_composable(
|
|
|
359
382
|
content = add_members_to_composable(content, declarations)
|
|
360
383
|
content = add_keys_to_return(content, missing)
|
|
361
384
|
|
|
385
|
+
# Step 3: add lifecycle hooks not yet present in the composable
|
|
386
|
+
if lifecycle_hooks:
|
|
387
|
+
hooks_to_add = _missing_hooks(content, lifecycle_hooks)
|
|
388
|
+
if hooks_to_add:
|
|
389
|
+
inline_lines, wrapped_lines = convert_lifecycle_hooks(
|
|
390
|
+
mixin_content, hooks_to_add, ref_members, plain_members, indent,
|
|
391
|
+
)
|
|
392
|
+
hook_lines = inline_lines + wrapped_lines
|
|
393
|
+
if hook_lines:
|
|
394
|
+
content = add_members_to_composable(content, hook_lines)
|
|
395
|
+
for imp in get_required_imports(hooks_to_add):
|
|
396
|
+
content = _add_vue_import(content, imp)
|
|
397
|
+
|
|
362
398
|
# Apply this.$ auto-rewrites ($nextTick, $set, $delete)
|
|
363
399
|
content, dollar_imports = rewrite_this_dollar_refs(content)
|
|
364
400
|
for imp in dollar_imports:
|
|
365
401
|
content = _add_vue_import(content, imp)
|
|
366
402
|
|
|
367
403
|
# Collect warnings and inject inline comments + confidence header
|
|
368
|
-
warnings = collect_mixin_warnings(mixin_content, mixin_members, [])
|
|
404
|
+
warnings = collect_mixin_warnings(mixin_content, mixin_members, lifecycle_hooks or [])
|
|
369
405
|
confidence = compute_confidence(content, warnings)
|
|
370
406
|
content = inject_inline_warnings(content, warnings, confidence, len(warnings))
|
|
371
407
|
|
|
@@ -16,25 +16,22 @@ from ..core.composable_search import find_composable_dirs, search_for_composable
|
|
|
16
16
|
from ..core.file_resolver import compute_import_path, resolve_import_path
|
|
17
17
|
from ..core.mixin_analyzer import (
|
|
18
18
|
extract_lifecycle_hooks, extract_mixin_members,
|
|
19
|
-
find_external_this_refs,
|
|
19
|
+
find_external_this_refs,
|
|
20
20
|
)
|
|
21
|
-
from ..core.warning_collector import collect_mixin_warnings
|
|
21
|
+
from ..core.warning_collector import collect_mixin_warnings, detect_name_collisions
|
|
22
22
|
from ..models import (
|
|
23
23
|
ComposableCoverage, FileChange, MigrationConfig, MigrationPlan, MigrationStatus,
|
|
24
24
|
MixinEntry, MixinMembers,
|
|
25
25
|
)
|
|
26
26
|
from ..transform.composable_generator import (
|
|
27
|
-
generate_composable_from_mixin,
|
|
28
|
-
mixin_stem_to_composable_name,
|
|
27
|
+
generate_composable_from_mixin, mixin_stem_to_composable_name,
|
|
29
28
|
)
|
|
30
29
|
from ..transform.composable_patcher import patch_composable
|
|
31
30
|
from ..transform.injector import (
|
|
32
|
-
add_composable_import,
|
|
31
|
+
add_composable_import, inject_setup,
|
|
33
32
|
remove_import_line, remove_mixin_from_array,
|
|
34
33
|
)
|
|
35
|
-
from ..transform.lifecycle_converter import
|
|
36
|
-
convert_lifecycle_hooks, find_lifecycle_referenced_members, get_required_imports,
|
|
37
|
-
)
|
|
34
|
+
from ..transform.lifecycle_converter import find_lifecycle_referenced_members
|
|
38
35
|
|
|
39
36
|
|
|
40
37
|
def _analyze_mixin_silent(
|
|
@@ -155,12 +152,17 @@ def plan_composable_patches(
|
|
|
155
152
|
|
|
156
153
|
for _comp_path, entries in entries_by_component:
|
|
157
154
|
for entry in entries:
|
|
158
|
-
if entry.
|
|
159
|
-
MigrationStatus.BLOCKED_NOT_RETURNED,
|
|
160
|
-
MigrationStatus.BLOCKED_MISSING_MEMBERS,
|
|
161
|
-
):
|
|
155
|
+
if not entry.composable:
|
|
162
156
|
continue
|
|
163
|
-
|
|
157
|
+
has_blocked = (
|
|
158
|
+
entry.status in (
|
|
159
|
+
MigrationStatus.BLOCKED_NOT_RETURNED,
|
|
160
|
+
MigrationStatus.BLOCKED_MISSING_MEMBERS,
|
|
161
|
+
)
|
|
162
|
+
and entry.classification
|
|
163
|
+
)
|
|
164
|
+
has_hooks = bool(entry.lifecycle_hooks)
|
|
165
|
+
if not has_blocked and not has_hooks:
|
|
164
166
|
continue
|
|
165
167
|
comp_path = entry.composable.file_path
|
|
166
168
|
if comp_path not in patch_map:
|
|
@@ -168,12 +170,18 @@ def plan_composable_patches(
|
|
|
168
170
|
"content": comp_path.read_text(errors="ignore").replace('\r\n', '\n').replace('\r', '\n'),
|
|
169
171
|
"not_returned": set(),
|
|
170
172
|
"missing": set(),
|
|
173
|
+
"lifecycle_hooks": [],
|
|
171
174
|
"mixin_members": entry.members,
|
|
172
175
|
"mixin_content": entry.mixin_path.read_text(errors="ignore").replace('\r\n', '\n').replace('\r', '\n'),
|
|
173
176
|
}
|
|
174
177
|
rec = patch_map[comp_path]
|
|
175
|
-
|
|
176
|
-
|
|
178
|
+
if has_blocked:
|
|
179
|
+
rec["not_returned"].update(entry.classification.truly_not_returned)
|
|
180
|
+
rec["missing"].update(entry.classification.truly_missing)
|
|
181
|
+
if has_hooks:
|
|
182
|
+
for h in entry.lifecycle_hooks:
|
|
183
|
+
if h not in rec["lifecycle_hooks"]:
|
|
184
|
+
rec["lifecycle_hooks"].append(h)
|
|
177
185
|
|
|
178
186
|
changes = []
|
|
179
187
|
for comp_path, rec in patch_map.items():
|
|
@@ -184,12 +192,15 @@ def plan_composable_patches(
|
|
|
184
192
|
not_returned=list(rec["not_returned"]),
|
|
185
193
|
missing=list(rec["missing"]),
|
|
186
194
|
mixin_members=rec["mixin_members"],
|
|
195
|
+
lifecycle_hooks=rec["lifecycle_hooks"] or None,
|
|
187
196
|
)
|
|
188
197
|
change_descs = []
|
|
189
198
|
if rec["not_returned"]:
|
|
190
199
|
change_descs.append(f"Added to return: {', '.join(sorted(rec['not_returned']))}")
|
|
191
200
|
if rec["missing"]:
|
|
192
201
|
change_descs.append(f"Added declarations: {', '.join(sorted(rec['missing']))}")
|
|
202
|
+
if rec["lifecycle_hooks"]:
|
|
203
|
+
change_descs.append(f"Added lifecycle hooks: {', '.join(rec['lifecycle_hooks'])}")
|
|
193
204
|
changes.append(FileChange(
|
|
194
205
|
file_path=comp_path,
|
|
195
206
|
original_content=original,
|
|
@@ -247,92 +258,16 @@ def plan_new_composables(
|
|
|
247
258
|
return changes
|
|
248
259
|
|
|
249
260
|
|
|
250
|
-
def _inject_setup_external_dep_warnings(
|
|
251
|
-
lines: list[str],
|
|
252
|
-
entry: "MixinEntry",
|
|
253
|
-
all_entries: list["MixinEntry"],
|
|
254
|
-
own_members: set[str],
|
|
255
|
-
component_name: str,
|
|
256
|
-
indent: str,
|
|
257
|
-
component_members_by_section: dict[str, list[str]] | None = None,
|
|
258
|
-
) -> list[str]:
|
|
259
|
-
"""Scan lifecycle lines for remaining ``this.X`` and prepend source-resolved warnings.
|
|
260
|
-
|
|
261
|
-
Only processes non-``$`` references that survived ``rewrite_this_refs``
|
|
262
|
-
(i.e. external deps not defined in the current mixin).
|
|
263
|
-
"""
|
|
264
|
-
if not lines:
|
|
265
|
-
return lines
|
|
266
|
-
|
|
267
|
-
all_text = "\n".join(lines)
|
|
268
|
-
external_names: list[str] = []
|
|
269
|
-
for m in re.finditer(r"\bthis\.(\w+)", all_text):
|
|
270
|
-
name = m.group(1)
|
|
271
|
-
if not name.startswith("$") and name not in external_names:
|
|
272
|
-
external_names.append(name)
|
|
273
|
-
|
|
274
|
-
if not external_names:
|
|
275
|
-
return lines
|
|
276
|
-
|
|
277
|
-
siblings = [e for e in all_entries if e is not entry]
|
|
278
|
-
sources = resolve_external_dep_sources(
|
|
279
|
-
external_names, siblings, own_members, component_name,
|
|
280
|
-
component_members_by_section=component_members_by_section,
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
result: list[str] = []
|
|
284
|
-
warned: set[str] = set()
|
|
285
|
-
for line in lines:
|
|
286
|
-
for m in re.finditer(r"\bthis\.(\w+)", line):
|
|
287
|
-
name = m.group(1)
|
|
288
|
-
if name.startswith("$") or name in warned:
|
|
289
|
-
continue
|
|
290
|
-
warned.add(name)
|
|
291
|
-
src = sources.get(name, {"kind": "unknown", "detail": None, "sources": []})
|
|
292
|
-
result.extend(
|
|
293
|
-
_format_setup_warning(name, src, entry, indent)
|
|
294
|
-
)
|
|
295
|
-
result.append(line)
|
|
296
|
-
return result
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
def _format_setup_warning(
|
|
300
|
-
name: str, src: dict, entry: "MixinEntry", indent: str
|
|
301
|
-
) -> list[str]:
|
|
302
|
-
"""Format a source-resolved warning comment for a setup() external dep."""
|
|
303
|
-
if src["kind"] == "sibling":
|
|
304
|
-
detail = src["detail"] # e.g. "loadingMixin.data"
|
|
305
|
-
sibling_stem = detail.split(".")[0]
|
|
306
|
-
sibling_composable = mixin_stem_to_composable_name(sibling_stem)
|
|
307
|
-
return [
|
|
308
|
-
f"{indent}// ⚠ MIGRATION: '{name}' — external dep (source: {detail} → {sibling_composable}).",
|
|
309
|
-
f"{indent}// this.{name} is unavailable in setup(). Use return value from {sibling_composable}() instead.",
|
|
310
|
-
]
|
|
311
|
-
elif src["kind"] == "component":
|
|
312
|
-
detail = src["detail"]
|
|
313
|
-
return [
|
|
314
|
-
f"{indent}// ⚠ MIGRATION: '{name}' — external dep (source: {detail}).",
|
|
315
|
-
f"{indent}// this.{name} is unavailable in setup(). Replace with local ref.",
|
|
316
|
-
]
|
|
317
|
-
elif src["kind"] == "ambiguous":
|
|
318
|
-
sources_str = ", ".join(src["sources"])
|
|
319
|
-
return [
|
|
320
|
-
f"{indent}// ⚠ MIGRATION: '{name}' — external dep (sources: {sources_str}).",
|
|
321
|
-
f"{indent}// Ambiguous origin — verify correct source before replacing this.{name}.",
|
|
322
|
-
]
|
|
323
|
-
else:
|
|
324
|
-
return [
|
|
325
|
-
f"{indent}// ⚠ MIGRATION: '{name}' — external dep, source not found in component or sibling mixins.",
|
|
326
|
-
f"{indent}// May come from props, inject, or a parent component. Replace this.{name} manually.",
|
|
327
|
-
]
|
|
328
|
-
|
|
329
|
-
|
|
330
261
|
def plan_component_injections(
|
|
331
262
|
entries_by_component: list[tuple[Path, list[MixinEntry]]],
|
|
332
263
|
composable_patches: list[FileChange],
|
|
333
264
|
config: MigrationConfig,
|
|
334
265
|
) -> list[FileChange]:
|
|
335
|
-
"""Plan all component setup() injections
|
|
266
|
+
"""Plan all component setup() injections.
|
|
267
|
+
|
|
268
|
+
Lifecycle hooks live in the composable (generated or patched), never in
|
|
269
|
+
setup(). Members referenced in lifecycle hook bodies are included in the
|
|
270
|
+
composable destructure so the hooks can access them.
|
|
336
271
|
|
|
337
272
|
Re-classifies entries whose composables were patched to account for
|
|
338
273
|
the updated return_keys and identifiers.
|
|
@@ -355,7 +290,6 @@ def plan_component_injections(
|
|
|
355
290
|
# R-7: normalize CRLF
|
|
356
291
|
comp_source = comp_path.read_text(errors="ignore").replace('\r\n', '\n').replace('\r', '\n')
|
|
357
292
|
own_members = extract_own_members(comp_source)
|
|
358
|
-
comp_members_by_section = extract_mixin_members(comp_source)
|
|
359
293
|
ready_entries = []
|
|
360
294
|
|
|
361
295
|
for entry in entries:
|
|
@@ -402,6 +336,20 @@ def plan_component_injections(
|
|
|
402
336
|
if not ready_entries:
|
|
403
337
|
continue
|
|
404
338
|
|
|
339
|
+
# Detect member name collisions across composables for this component
|
|
340
|
+
if len(ready_entries) > 1:
|
|
341
|
+
composable_members_map = {}
|
|
342
|
+
for entry in ready_entries:
|
|
343
|
+
if entry.composable and entry.classification:
|
|
344
|
+
composable_members_map[entry.composable.fn_name] = list(
|
|
345
|
+
entry.classification.injectable
|
|
346
|
+
)
|
|
347
|
+
collision_warnings = detect_name_collisions(composable_members_map)
|
|
348
|
+
if collision_warnings:
|
|
349
|
+
for w in collision_warnings:
|
|
350
|
+
w.mixin_stem = "cross-composable"
|
|
351
|
+
ready_entries[0].warnings.extend(collision_warnings)
|
|
352
|
+
|
|
405
353
|
content = comp_source
|
|
406
354
|
changes_desc = []
|
|
407
355
|
|
|
@@ -424,14 +372,13 @@ def plan_component_injections(
|
|
|
424
372
|
content = new
|
|
425
373
|
|
|
426
374
|
composable_calls = []
|
|
427
|
-
all_inline_lines: list[str] = []
|
|
428
|
-
all_lifecycle_calls: list[str] = []
|
|
429
375
|
|
|
430
376
|
for entry in ready_entries:
|
|
431
377
|
injectable = list(entry.classification.injectable if entry.classification else entry.used_members)
|
|
432
|
-
mixin_content = None
|
|
433
378
|
|
|
434
|
-
# Augment injectable with members referenced in lifecycle hook bodies
|
|
379
|
+
# Augment injectable with members referenced in lifecycle hook bodies —
|
|
380
|
+
# the composable contains the hooks and needs these members destructured.
|
|
381
|
+
lifecycle_members: list[str] = []
|
|
435
382
|
if entry.lifecycle_hooks and entry.composable:
|
|
436
383
|
mixin_content = entry.mixin_path.read_text(errors="ignore").replace('\r\n', '\n').replace('\r', '\n')
|
|
437
384
|
lifecycle_members = find_lifecycle_referenced_members(
|
|
@@ -455,41 +402,11 @@ def plan_component_injections(
|
|
|
455
402
|
if injectable and entry.composable:
|
|
456
403
|
composable_calls.append((entry.composable.fn_name, injectable))
|
|
457
404
|
|
|
458
|
-
|
|
459
|
-
if mixin_content is None:
|
|
460
|
-
mixin_content = entry.mixin_path.read_text(errors="ignore").replace('\r\n', '\n').replace('\r', '\n')
|
|
461
|
-
ref_m = entry.members.data + entry.members.computed + entry.members.watch
|
|
462
|
-
plain_m = entry.members.methods
|
|
463
|
-
inline, wrapped = convert_lifecycle_hooks(
|
|
464
|
-
mixin_content, entry.lifecycle_hooks, ref_m, plain_m,
|
|
465
|
-
config.indent + config.indent, # double indent to match setup() body level
|
|
466
|
-
)
|
|
467
|
-
|
|
468
|
-
# Inject source-resolved warnings for external deps in lifecycle code
|
|
469
|
-
comp_name = comp_path.stem
|
|
470
|
-
setup_indent = config.indent + config.indent
|
|
471
|
-
inline = _inject_setup_external_dep_warnings(
|
|
472
|
-
inline, entry, entries, own_members, comp_name, setup_indent,
|
|
473
|
-
component_members_by_section=comp_members_by_section,
|
|
474
|
-
)
|
|
475
|
-
wrapped = _inject_setup_external_dep_warnings(
|
|
476
|
-
wrapped, entry, entries, own_members, comp_name, setup_indent,
|
|
477
|
-
component_members_by_section=comp_members_by_section,
|
|
478
|
-
)
|
|
479
|
-
|
|
480
|
-
all_inline_lines.extend(inline)
|
|
481
|
-
all_lifecycle_calls.extend(wrapped)
|
|
482
|
-
|
|
483
|
-
for hook_import in get_required_imports(entry.lifecycle_hooks):
|
|
484
|
-
content = add_vue_import(content, hook_import)
|
|
485
|
-
|
|
486
|
-
if composable_calls or all_inline_lines or all_lifecycle_calls:
|
|
405
|
+
if composable_calls:
|
|
487
406
|
new = inject_setup(
|
|
488
407
|
content,
|
|
489
408
|
composable_calls,
|
|
490
409
|
config.indent,
|
|
491
|
-
lifecycle_calls=all_lifecycle_calls or None,
|
|
492
|
-
inline_setup_lines=all_inline_lines or None,
|
|
493
410
|
)
|
|
494
411
|
if new != content:
|
|
495
412
|
fn_names = [c[0] for c in composable_calls]
|
|
@@ -530,6 +447,7 @@ def run(project_root: Path, config: MigrationConfig) -> MigrationPlan:
|
|
|
530
447
|
return MigrationPlan(
|
|
531
448
|
component_changes=component_changes,
|
|
532
449
|
composable_changes=composable_changes,
|
|
450
|
+
entries_by_component=entries,
|
|
533
451
|
)
|
|
534
452
|
|
|
535
453
|
|
|
@@ -543,6 +461,11 @@ def run_scoped(
|
|
|
543
461
|
|
|
544
462
|
Exactly one of component_path or mixin_stem must be provided.
|
|
545
463
|
No file I/O. Returns a MigrationPlan the CLI can show as a diff and write.
|
|
464
|
+
|
|
465
|
+
Note: When component_path is provided, composable patches only aggregate
|
|
466
|
+
requirements from that component's entries. Shared composables may receive
|
|
467
|
+
incomplete patches. Use mixin_stem scope or full-project run for complete
|
|
468
|
+
composable coverage.
|
|
546
469
|
"""
|
|
547
470
|
if component_path is None and mixin_stem is None:
|
|
548
471
|
raise ValueError("Provide either component_path or mixin_stem")
|
|
@@ -563,4 +486,5 @@ def run_scoped(
|
|
|
563
486
|
return MigrationPlan(
|
|
564
487
|
component_changes=component_changes,
|
|
565
488
|
composable_changes=composable_changes,
|
|
489
|
+
entries_by_component=entries,
|
|
566
490
|
)
|