vue3-migration 1.3.0 → 1.3.2

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.3.0",
3
+ "version": "1.3.2",
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"
@@ -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
- ret = re.search(r"\breturn\s*\{", source)
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
- ret = re.search(r"\breturn\s*\{", source)
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)
@@ -40,24 +40,8 @@ _THIS_DOLLAR_PATTERNS: list[tuple[str, str, str, str]] = [
40
40
  "this.$refs is not available in composables",
41
41
  "Use template refs with ref() instead",
42
42
  ),
43
- (
44
- r"this\.\$nextTick\b",
45
- "this.$nextTick",
46
- "this.$nextTick should use the imported nextTick",
47
- "Import nextTick from 'vue' and call it directly",
48
- ),
49
- (
50
- r"this\.\$set\b",
51
- "this.$set",
52
- "this.$set is removed in Vue 3",
53
- "Assign directly — Vue 3 reactivity tracks new properties",
54
- ),
55
- (
56
- r"this\.\$delete\b",
57
- "this.$delete",
58
- "this.$delete is removed in Vue 3",
59
- "Use delete operator directly — Vue 3 reactivity tracks deletions",
60
- ),
43
+ # $nextTick, $set, $delete are auto-migrated by rewrite_this_dollar_refs()
44
+ # in this_rewriter.py — no warning needed.
61
45
  (
62
46
  r"this\.\$on\b",
63
47
  "this.$on",
@@ -305,7 +289,7 @@ def inject_inline_warnings(
305
289
  Optionally prepends a confidence header comment at the top.
306
290
  """
307
291
  if confidence is not None:
308
- header = f"// Migration confidence: {confidence.value} ({warning_count} warnings — see migration report)\n"
292
+ header = f"// Transformation confidence: {confidence.value} ({warning_count} warnings — see migration report)\n"
309
293
  source = header + source
310
294
 
311
295
  unplaced: list[MigrationWarning] = []
@@ -420,6 +404,26 @@ _MIXIN_OPTION_PATTERNS: list[tuple[str, str, str, str]] = [
420
404
  ]
421
405
 
422
406
 
407
+ def _brace_depth_at(source: str, pos: int) -> int:
408
+ """Count net brace depth from start of *source* to *pos*, skipping non-code."""
409
+ from .js_parser import skip_non_code
410
+
411
+ depth = 0
412
+ i = 0
413
+ while i < pos:
414
+ new_i, skipped = skip_non_code(source, i)
415
+ if skipped:
416
+ i = new_i
417
+ continue
418
+ ch = source[i]
419
+ if ch == '{':
420
+ depth += 1
421
+ elif ch == '}':
422
+ depth -= 1
423
+ i += 1
424
+ return depth
425
+
426
+
423
427
  def detect_mixin_options(
424
428
  mixin_source: str, mixin_stem: str
425
429
  ) -> list[MigrationWarning]:
@@ -447,6 +451,10 @@ def detect_mixin_options(
447
451
  pos += 1
448
452
  if in_non_code:
449
453
  continue
454
+ # Mixin options sit at brace depth 1 (inside `export default {}`).
455
+ # Deeper matches (e.g. `filters` inside data()) are not options.
456
+ if _brace_depth_at(mixin_source, m.start()) != 1:
457
+ continue
450
458
  warnings.append(MigrationWarning(
451
459
  mixin_stem=mixin_stem,
452
460
  category=f"mixin-option:{option}",
@@ -425,16 +425,19 @@ def build_warning_summary(
425
425
  conf = confidence_map[entry.mixin_stem]
426
426
  icon = _CONF_ICON.get(conf, "\u2753")
427
427
 
428
- a(f"### {icon} {entry.mixin_stem} \u2014 {conf.value} confidence\n")
428
+ a(f"### {icon} {entry.mixin_stem} \u2014 Transformation confidence: {conf.value}\n")
429
429
 
430
430
  if not entry.warnings:
431
431
  a("No manual changes needed.\n")
432
432
  continue
433
433
 
434
+ item_count = len(entry.warnings)
435
+ a(f"> {item_count} item{'s' if item_count != 1 else ''} need{'s' if item_count == 1 else ''} attention\n")
436
+
434
437
  for warning in entry.warnings:
435
438
  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("")
439
+ a(f"- {sev_icon} **{warning.category}** ({warning.severity}): {warning.message}")
440
+ a(f"")
441
+ a(f" **\u2192 {warning.action_required}**\n")
439
442
 
440
443
  return "\n".join(lines)
@@ -18,7 +18,7 @@ from ..core.mixin_analyzer import (
18
18
  extract_lifecycle_hooks, extract_mixin_members,
19
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,
@@ -336,6 +336,20 @@ def plan_component_injections(
336
336
  if not ready_entries:
337
337
  continue
338
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
+
339
353
  content = comp_source
340
354
  changes_desc = []
341
355
 
@@ -447,6 +461,11 @@ def run_scoped(
447
461
 
448
462
  Exactly one of component_path or mixin_stem must be provided.
449
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.
450
469
  """
451
470
  if component_path is None and mixin_stem is None:
452
471
  raise ValueError("Provide either component_path or mixin_stem")