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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue3-migration",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
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)
@@ -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
- new_lines.append(f"{indent}// ⚠ MIGRATION: {w.message}\n")
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
- block = "".join(
335
- f"// ⚠ MIGRATION: {w.message}\n" for w in unplaced
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
@@ -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 build_warnings_section(
348
- entries: list[MixinEntry],
349
- confidence_map: dict[str, ConfidenceLevel],
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 Warnings section.
351
+ """Build a markdown Migration Summary checklist for the diff report.
352
352
 
353
- Groups warnings by mixin, shows confidence per composable,
354
- and a table of Issue | Action Required for each warning.
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
- if not entries:
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
- lines: list[str] = []
360
- w = lines.append
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
- w("## Migration Warnings\n")
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
- for entry in entries:
365
- stem = entry.mixin_stem
366
- confidence = confidence_map.get(stem)
367
- conf_str = confidence.value if confidence else "?"
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
- w(f"### {stem} Confidence: **{conf_str}**\n")
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
- if entry.warnings:
372
- w("| Issue | Action Required |")
373
- w("|-------|----------------|")
374
- for warning in entry.warnings:
375
- w(f"| {warning.category}: {warning.message} | {warning.action_required} |")
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 extract_hook_body
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, resolve_external_dep_sources,
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, add_vue_import, inject_setup,
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.status not in (
159
- MigrationStatus.BLOCKED_NOT_RETURNED,
160
- MigrationStatus.BLOCKED_MISSING_MEMBERS,
161
- ):
155
+ if not entry.composable:
162
156
  continue
163
- if not entry.composable or not entry.classification:
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
- rec["not_returned"].update(entry.classification.truly_not_returned)
176
- rec["missing"].update(entry.classification.truly_missing)
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, including lifecycle hook conversion.
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
- if entry.lifecycle_hooks:
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
  )