vue3-migration 1.4.3 → 1.4.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue3-migration",
3
- "version": "1.4.3",
3
+ "version": "1.4.4",
4
4
  "description": "Automatically migrate Vue 2 mixins to Vue 3 composables — data, computed, methods, watchers, lifecycle hooks, and all",
5
5
  "bin": {
6
6
  "vue3-migration": "./bin/cli.js"
@@ -184,9 +184,33 @@ def write_diff_report(plan: MigrationPlan, project_root: Path) -> Path:
184
184
  except ValueError:
185
185
  return str(path)
186
186
 
187
+ # Count totals for header
188
+ all_changes = [c for c in (plan.composable_changes + plan.component_changes) if c.has_changes]
189
+ _header_parts = [f"{len(all_changes)} file{'s' if len(all_changes) != 1 else ''}"]
190
+ if plan.entries_by_component:
191
+ _seen: set[str] = set()
192
+ _all_w = []
193
+ for _cp2, _el2 in plan.entries_by_component:
194
+ for _e2 in _el2:
195
+ if _e2.mixin_stem not in _seen:
196
+ _seen.add(_e2.mixin_stem)
197
+ _all_w.extend(_e2.warnings)
198
+ _ec = sum(1 for w in _all_w if w.severity == "error")
199
+ _wc = sum(1 for w in _all_w if w.severity == "warning")
200
+ _ic = sum(1 for w in _all_w if w.severity == "info")
201
+ if _ec:
202
+ _header_parts.append(f"{_ec} error{'s' if _ec != 1 else ''}")
203
+ if _wc:
204
+ _header_parts.append(f"{_wc} warning{'s' if _wc != 1 else ''}")
205
+ if _ic:
206
+ _header_parts.append(f"{_ic} info")
207
+
187
208
  sections: list[str] = [
188
209
  "# Migration Diff Report",
189
- f"Generated: {now.strftime('%Y-%m-%d %H:%M:%S')}",
210
+ "",
211
+ f"`{now.strftime('%Y-%m-%d %H:%M:%S')}` \u2014 {' \u00b7 '.join(_header_parts)}",
212
+ "",
213
+ "---",
190
214
  "",
191
215
  ]
192
216
 
@@ -221,7 +245,7 @@ def write_diff_report(plan: MigrationPlan, project_root: Path) -> Path:
221
245
  else:
222
246
  confidence_map[entry.mixin_stem] = ConfidenceLevel.HIGH
223
247
 
224
- summary = build_warning_summary(plan.entries_by_component, plan.composable_changes)
248
+ summary = build_warning_summary(plan.entries_by_component, plan.composable_changes, project_root)
225
249
  if summary:
226
250
  sections.append(summary)
227
251
  sections.append("")
@@ -235,8 +259,6 @@ def write_diff_report(plan: MigrationPlan, project_root: Path) -> Path:
235
259
  sections.append("")
236
260
 
237
261
  # Diffs
238
- all_changes = [c for c in (plan.composable_changes + plan.component_changes) if c.has_changes]
239
-
240
262
  for change in all_changes:
241
263
  rel = _rel(change.file_path)
242
264
  sections.append(f"## `{rel}`")
@@ -13,6 +13,23 @@ _SKIPPED_CATEGORIES = frozenset({
13
13
  "skipped-no-usage",
14
14
  })
15
15
 
16
+ _CONF_DOT = {
17
+ ConfidenceLevel.LOW: "\U0001f534", # red dot
18
+ ConfidenceLevel.MEDIUM: "\U0001f7e1", # yellow dot
19
+ ConfidenceLevel.HIGH: "\U0001f7e2", # green dot
20
+ }
21
+
22
+
23
+ def _rel_link(path: "Path | str", project_root: Path, label: str | None = None) -> str:
24
+ """Return a markdown hyperlink with a relative path."""
25
+ p = Path(path) if not isinstance(path, Path) else path
26
+ try:
27
+ rel = p.relative_to(project_root)
28
+ except ValueError:
29
+ rel = p
30
+ display = label or rel.name
31
+ return f"[`{display}`]({str(rel).replace(chr(92), '/')})"
32
+
16
33
 
17
34
  def build_component_report(
18
35
  component_path: Path,
@@ -20,23 +37,30 @@ def build_component_report(
20
37
  project_root: Path,
21
38
  ) -> str:
22
39
  """Build a markdown migration report for a single component."""
40
+ from datetime import datetime
41
+
23
42
  lines: list[str] = []
24
43
  w = lines.append
25
44
 
26
- try:
27
- component_rel = component_path.relative_to(project_root)
28
- except ValueError:
29
- component_rel = component_path
45
+ ready_count = sum(
46
+ 1 for e in mixin_entries
47
+ if not e.used_members or (e.classification and e.classification.is_ready)
48
+ )
49
+ blocked_count = len(mixin_entries) - ready_count
30
50
 
31
- w(f"# Migration Report: {md_green(str(component_rel))}\n")
51
+ w(f"# Migration Report: {_rel_link(component_path, project_root)}\n")
52
+ parts = [f"{len(mixin_entries)} mixin{'s' if len(mixin_entries) != 1 else ''}"]
53
+ parts.append(f"{ready_count} ready")
54
+ parts.append(f"{blocked_count} blocked")
55
+ w(f"`{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}` \u2014 {' \u00b7 '.join(parts)}\n")
56
+ w("---\n")
32
57
 
33
58
  ready_entries = []
34
59
  blocked_entries = []
35
60
 
36
61
  for entry in mixin_entries:
37
62
  mixin_name = entry.mixin_stem
38
- w(f"## Mixin: {mixin_name}\n")
39
- w(f"**File:** {md_green(str(entry.mixin_path))}\n")
63
+ w(f"## Mixin: {_rel_link(entry.mixin_path, project_root, mixin_name)}\n")
40
64
 
41
65
  # Members breakdown
42
66
  for section in ("data", "computed", "methods"):
@@ -64,7 +88,7 @@ def build_component_report(
64
88
  w(f"**Composable:** {md_yellow('NOT FOUND')}\n")
65
89
  blocked_entries.append(entry)
66
90
  else:
67
- w(f"**Composable:** {md_green(str(comp.file_path))}")
91
+ w(f"**Composable:** {_rel_link(comp.file_path, project_root)}")
68
92
  w(f"**Function:** `{comp.fn_name}`")
69
93
  w(f"**Import path:** `{comp.import_path}`")
70
94
  w(f"> {md_yellow('Verify the above path and function name are correct.')}\n")
@@ -125,7 +149,7 @@ def build_component_report(
125
149
  w(f"- Add to composable: {', '.join(cls.missing)}")
126
150
  if cls and cls.not_returned:
127
151
  w(f"- Add to return statement: {', '.join(cls.not_returned)}")
128
- w(f"- File: {md_green(str(comp.file_path))}\n")
152
+ w(f"- File: {_rel_link(comp.file_path, project_root)}\n")
129
153
 
130
154
  if ready_entries:
131
155
  w("### Ready for injection")
@@ -233,19 +257,18 @@ def generate_status_report(project_root: Path, config) -> str:
233
257
  needs_manual_count = sum(1 for c in components_info if c["has_manual"])
234
258
  blocked = len(components_info) - ready - needs_manual_count
235
259
 
260
+ header_parts = [f"{len(components_info)} component{'s' if len(components_info) != 1 else ''}"]
261
+ header_parts.append(f"{ready} ready")
262
+ if needs_manual_count:
263
+ header_parts.append(f"{needs_manual_count} manual")
264
+ header_parts.append(f"{blocked} blocked")
265
+
236
266
  lines: list[str] = [
237
267
  "# Vue Migration Status Report",
238
- f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
239
268
  "",
240
- "## Summary",
269
+ f"`{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}` \u2014 {' \u00b7 '.join(header_parts)}",
241
270
  "",
242
- f"- Components with mixins remaining: **{len(components_info)}**",
243
- f"- Ready to migrate now: **{ready}**",
244
- ]
245
- if needs_manual_count:
246
- lines.append(f"- Needs manual migration: **{needs_manual_count}**")
247
- lines.extend([
248
- f"- Blocked (composable missing or incomplete): **{blocked}**",
271
+ "---",
249
272
  "",
250
273
  "> Run `vue3-migration auto` to generate a detailed diff report with warnings, per-component guide, and checklist.",
251
274
  "",
@@ -253,7 +276,7 @@ def generate_status_report(project_root: Path, config) -> str:
253
276
  "",
254
277
  "| Mixin | Used in | Composable |",
255
278
  "|-------|---------|------------|",
256
- ])
279
+ ]
257
280
 
258
281
  for stem, count in mixin_counter.most_common():
259
282
  has_comp = mixin_has_composable(stem, composable_stems)
@@ -283,7 +306,7 @@ def generate_status_report(project_root: Path, config) -> str:
283
306
  missing = comp["total"] - comp["covered"] - comp["needs_manual"]
284
307
  status_str = f"**Blocked** -- {missing} composable(s) missing or incomplete"
285
308
 
286
- lines.append(f"### `{comp['rel_path']}`")
309
+ lines.append(f"### [`{comp['rel_path']}`]({str(comp['rel_path']).replace(chr(92), '/')})")
287
310
  lines.append(f"- Mixins: {', '.join(f'`{s}`' for s in comp['stems'])}")
288
311
  lines.append(f"- Status: {status_str}")
289
312
  lines.append("")
@@ -305,10 +328,19 @@ def build_audit_report(
305
328
  warnings: list[MigrationWarning] | None = None,
306
329
  ) -> str:
307
330
  """Build a markdown audit report for a single mixin."""
331
+ from datetime import datetime
332
+
308
333
  lines: list[str] = []
309
334
  w = lines.append
310
335
 
311
- w(f"# Mixin Audit: {mixin_path.name}\n")
336
+ total_members = len(all_member_names)
337
+ header_parts = [f"{total_members} member{'s' if total_members != 1 else ''}"]
338
+ header_parts.append(f"{len(lifecycle_hooks)} hook{'s' if len(lifecycle_hooks) != 1 else ''}")
339
+ header_parts.append(f"{len(importing_files)} file{'s' if len(importing_files) != 1 else ''}")
340
+
341
+ w(f"# Mixin Audit: {_rel_link(mixin_path, project_root, mixin_path.name)}\n")
342
+ w(f"`{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}` \u2014 {' \u00b7 '.join(header_parts)}\n")
343
+ w("---\n")
312
344
 
313
345
  w("## Mixin Members\n")
314
346
  for section in ("data", "computed", "methods"):
@@ -328,7 +360,7 @@ def build_audit_report(
328
360
  for file_path in sorted(importing_files):
329
361
  relative_path = file_path.relative_to(project_root)
330
362
  used = usage_map.get(str(relative_path), [])
331
- w(f"### {relative_path}\n")
363
+ w(f"### {_rel_link(file_path, project_root, str(relative_path))}\n")
332
364
  if used:
333
365
  w(f"Uses: {', '.join(used)}\n")
334
366
  else:
@@ -379,18 +411,12 @@ def build_per_component_index(
379
411
  if not entries_by_component:
380
412
  return ""
381
413
 
382
- _CONF_ICON = {ConfidenceLevel.LOW: "\u274c", ConfidenceLevel.MEDIUM: "\u26a0\ufe0f", ConfidenceLevel.HIGH: "\u2705"}
383
-
384
414
  lines: list[str] = []
385
415
  a = lines.append
386
416
  a("## Per-Component Guide\n")
387
417
 
388
418
  for comp_path, entry_list in entries_by_component:
389
- try:
390
- comp_rel = comp_path.relative_to(project_root)
391
- except ValueError:
392
- comp_rel = comp_path
393
- a(f"### {comp_rel.name}\n")
419
+ a(f"### {_rel_link(comp_path, project_root)}\n")
394
420
 
395
421
  for entry in entry_list:
396
422
  entry_cats = {w.category for w in entry.warnings}
@@ -401,9 +427,10 @@ def build_per_component_index(
401
427
  a(f"- \u2139\ufe0f **{entry.mixin_stem}** skipped \u2014 {reason}")
402
428
  elif entry.composable:
403
429
  conf = confidence_map.get(entry.mixin_stem, ConfidenceLevel.HIGH)
404
- icon = _CONF_ICON.get(conf, "\u2753")
430
+ dot = _CONF_DOT.get(conf, "\u2753")
405
431
  error_count = sum(1 for w in entry.warnings if w.severity == "error")
406
432
  warn_count = sum(1 for w in entry.warnings if w.severity == "warning")
433
+ comp_link = _rel_link(entry.composable.file_path, project_root, entry.composable.fn_name)
407
434
  if error_count or warn_count:
408
435
  parts = []
409
436
  if error_count:
@@ -411,11 +438,11 @@ def build_per_component_index(
411
438
  if warn_count:
412
439
  parts.append(f"{warn_count} warning{'s' if warn_count != 1 else ''}")
413
440
  detail = ", ".join(parts)
414
- a(f"- {icon} **{entry.composable.fn_name}** ({conf.value}) \u2014 {detail} \u2192 [See warnings](#{entry.mixin_stem})")
441
+ a(f"- {dot} {comp_link} \u2014 {detail} \u2192 [See warnings](#{entry.mixin_stem})")
415
442
  else:
416
- a(f"- {icon} **{entry.composable.fn_name}** ({conf.value}) \u2014 No issues")
443
+ a(f"- {dot} {comp_link} \u2014 No issues")
417
444
  else:
418
- a(f"- \u274c **{entry.mixin_stem}** \u2014 composable not found")
445
+ a(f"- \U0001f534 **{entry.mixin_stem}** \u2014 composable not found")
419
446
 
420
447
  a("")
421
448
 
@@ -508,6 +535,7 @@ def build_checklist(
508
535
  def build_warning_summary(
509
536
  entries_by_component: "list[tuple[Path, list[MixinEntry]]]",
510
537
  composable_changes: "list[FileChange] | None" = None,
538
+ project_root: "Path | None" = None,
511
539
  ) -> str:
512
540
  """Build a markdown Migration Summary checklist for the diff report.
513
541
 
@@ -517,34 +545,51 @@ def build_warning_summary(
517
545
  """
518
546
  from ..core.warning_collector import compute_confidence
519
547
 
520
- # De-duplicate entries by mixin_stem (keep first occurrence)
521
- seen_stems: set[str] = set()
522
- unique_entries: list[MixinEntry] = []
523
- for _comp_path, entry_list in entries_by_component:
548
+ # Group entries by mixin_stem, tracking component paths and warnings
549
+ from collections import OrderedDict
550
+
551
+ # Collect per-mixin: representative entry + all (comp_path, warning) pairs
552
+ _MixinGroup = tuple[MixinEntry, list[tuple[Path, MigrationWarning]]]
553
+ mixin_groups: OrderedDict[str, _MixinGroup] = OrderedDict()
554
+
555
+ for comp_path, entry_list in entries_by_component:
524
556
  for entry in entry_list:
525
- if entry.mixin_stem not in seen_stems:
526
- seen_stems.add(entry.mixin_stem)
527
- unique_entries.append(entry)
557
+ if entry.mixin_stem not in mixin_groups:
558
+ mixin_groups[entry.mixin_stem] = (entry, [])
559
+ group_entry, group_warnings = mixin_groups[entry.mixin_stem]
560
+ for w in entry.warnings:
561
+ group_warnings.append((comp_path, w))
528
562
 
529
- if not unique_entries:
563
+ if not mixin_groups:
530
564
  return ""
531
565
 
532
566
  # Separate skipped entries from active entries
533
567
  skipped_rows: list[tuple[str, str, str]] = [] # (component, mixin, reason)
534
568
  active_entries: list[MixinEntry] = []
569
+ # Map mixin_stem -> de-duped (comp_path, warning) pairs
570
+ active_warnings: dict[str, list[tuple[Path, MigrationWarning]]] = {}
535
571
 
536
- for entry in unique_entries:
572
+ for stem, (entry, comp_warnings) in mixin_groups.items():
537
573
  entry_cats = {w.category for w in entry.warnings}
538
574
  if entry_cats and entry_cats <= _SKIPPED_CATEGORIES:
539
- # Find which component(s) this mixin was used in
540
575
  for comp_path, entry_list in entries_by_component:
541
576
  for e in entry_list:
542
- if e.mixin_stem == entry.mixin_stem:
577
+ if e.mixin_stem == stem:
543
578
  reason = entry.warnings[0].message.split(":", 1)[-1].strip() if entry.warnings else "unknown"
544
- comp_name = comp_path.name
545
- skipped_rows.append((comp_name, entry.mixin_stem, reason))
579
+ skipped_rows.append((comp_path.name, stem, reason))
546
580
  else:
547
581
  active_entries.append(entry)
582
+ # De-dup warnings by (category, message, severity) but keep component paths
583
+ seen_w: dict[tuple[str, str, str], list[Path]] = {}
584
+ deduped: list[tuple[Path, MigrationWarning]] = []
585
+ for cp, w in comp_warnings:
586
+ key = (w.category, w.message, w.severity)
587
+ if key not in seen_w:
588
+ seen_w[key] = []
589
+ deduped.append((cp, w))
590
+ if cp not in seen_w[key]:
591
+ seen_w[key].append(cp)
592
+ active_warnings[stem] = deduped
548
593
 
549
594
  # Build a lookup of composable content by file path
550
595
  composable_content_map: dict[Path, str] = {}
@@ -575,8 +620,8 @@ def build_warning_summary(
575
620
  if not active_entries and not skipped_rows:
576
621
  return ""
577
622
 
578
- # Count totals
579
- all_warnings = [w for e in active_entries for w in e.warnings]
623
+ # Count totals (from de-duped warnings)
624
+ all_warnings = [w for stem in active_warnings for _cp, w in active_warnings[stem]]
580
625
  error_count = sum(1 for w in all_warnings if w.severity == "error")
581
626
  warning_count = sum(1 for w in all_warnings if w.severity == "warning")
582
627
  info_count = sum(1 for w in all_warnings if w.severity == "info")
@@ -588,28 +633,42 @@ def build_warning_summary(
588
633
 
589
634
  # Overview line
590
635
  total_count = len(active_entries) + len(skipped_rows)
591
- parts = [f"**{total_count} composable{'s' if total_count != 1 else ''}**"]
636
+ parts = [f"{total_count} composable{'s' if total_count != 1 else ''}"]
592
637
  if error_count:
593
638
  parts.append(f"{error_count} error{'s' if error_count != 1 else ''}")
594
639
  if warning_count:
595
640
  parts.append(f"{warning_count} warning{'s' if warning_count != 1 else ''}")
596
641
  if info_count:
597
642
  parts.append(f"{info_count} info")
598
- a(" | ".join(parts))
643
+ a(" \u00b7 ".join(parts))
599
644
  a("")
600
645
  a("---\n")
601
646
 
602
647
  # Per-mixin sections
603
- _SEVERITY_ICON = {"error": "\u274c", "warning": "\u26a0\ufe0f", "info": "\u2139\ufe0f"}
604
- _CONF_ICON = {ConfidenceLevel.LOW: "\u274c", ConfidenceLevel.MEDIUM: "\u26a0\ufe0f", ConfidenceLevel.HIGH: "\u2705"}
605
-
606
648
  for entry in active_entries:
607
649
  conf = confidence_map[entry.mixin_stem]
608
- icon = _CONF_ICON.get(conf, "\u2753")
650
+ dot = _CONF_DOT.get(conf, "\u2753")
609
651
 
610
- a(f"### {icon} {entry.mixin_stem} \u2014 Transformation confidence: {conf.value}\n")
652
+ # Build heading: dot mixin_link composable_link · (CONFIDENCE)
653
+ if project_root:
654
+ mixin_link = _rel_link(entry.mixin_path, project_root, entry.mixin_stem)
655
+ else:
656
+ mixin_link = f"`{entry.mixin_stem}`"
657
+
658
+ if entry.composable:
659
+ if project_root:
660
+ comp_link = _rel_link(entry.composable.file_path, project_root, entry.composable.fn_name)
661
+ else:
662
+ comp_link = f"`{entry.composable.fn_name}`"
663
+ heading = f"{dot} {mixin_link} \u2192 {comp_link} \u00b7 ({conf.value})"
664
+ else:
665
+ heading = f"{dot} {mixin_link} \u00b7 ({conf.value})"
611
666
 
612
- if not entry.warnings:
667
+ a(f"<a id=\"{entry.mixin_stem}\"></a>\n")
668
+ a(f"### {heading}\n")
669
+
670
+ entry_w = active_warnings.get(entry.mixin_stem, [])
671
+ if not entry_w:
613
672
  # Only say "No manual changes needed" if the composable is truly clean
614
673
  comp_source = ""
615
674
  if entry.composable and entry.composable.file_path in composable_content_map:
@@ -625,14 +684,23 @@ def build_warning_summary(
625
684
  a("Review generated composable for any remaining migration markers.\n")
626
685
  continue
627
686
 
628
- item_count = len(entry.warnings)
629
- a(f"> {item_count} item{'s' if item_count != 1 else ''} need{'s' if item_count == 1 else ''} attention\n")
687
+ # Determine how many unique components use this mixin
688
+ all_comps_for_mixin = set()
689
+ for cp, el in entries_by_component:
690
+ for e in el:
691
+ if e.mixin_stem == entry.mixin_stem:
692
+ all_comps_for_mixin.add(cp)
630
693
 
631
- for warning in entry.warnings:
632
- sev_icon = _SEVERITY_ICON.get(warning.severity, "\u2753")
633
- a(f"- {sev_icon} **{warning.category}** ({warning.severity}): {warning.message}")
634
- a(f"")
635
- a(f" **\u2192 {warning.action_required}**\n")
694
+ a("| Severity | Issue | Fix |")
695
+ a("|---|---|---|")
696
+ for comp_path, warning in entry_w:
697
+ # Show component name when there are multiple components or when it adds context
698
+ if project_root and len(all_comps_for_mixin) >= 1:
699
+ comp_name = _rel_link(comp_path, project_root)
700
+ a(f"| {warning.severity} | {comp_name}: {warning.message} | {warning.action_required} |")
701
+ else:
702
+ a(f"| {warning.severity} | {warning.message} | {warning.action_required} |")
703
+ a("")
636
704
 
637
705
  # Skipped mixins table
638
706
  if skipped_rows: