vue3-migration 1.4.0 → 1.4.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 +1 -1
- package/vue3_migration/__init__.py +1 -1
- package/vue3_migration/cli.py +2 -1
- package/vue3_migration/core/component_analyzer.py +5 -3
- package/vue3_migration/models.py +1 -0
- package/vue3_migration/transform/composable_generator.py +9 -5
- package/vue3_migration/transform/composable_patcher.py +83 -20
- package/vue3_migration/workflows/auto_migrate_workflow.py +152 -23
package/package.json
CHANGED
package/vue3_migration/cli.py
CHANGED
|
@@ -517,9 +517,10 @@ def main(argv: list[str] | None = None):
|
|
|
517
517
|
|
|
518
518
|
parser = argparse.ArgumentParser(add_help=False)
|
|
519
519
|
parser.add_argument("--root", default=None)
|
|
520
|
+
parser.add_argument("--regenerate", "-R", action="store_true", default=False)
|
|
520
521
|
known, args = parser.parse_known_args(args)
|
|
521
522
|
project_root = Path(known.root).resolve() if known.root else Path.cwd()
|
|
522
|
-
config = MigrationConfig(project_root=project_root)
|
|
523
|
+
config = MigrationConfig(project_root=project_root, regenerate=known.regenerate)
|
|
523
524
|
|
|
524
525
|
if not args:
|
|
525
526
|
interactive_menu(config)
|
|
@@ -49,7 +49,7 @@ def find_used_members(component_source: str, member_names: list[str]) -> list[st
|
|
|
49
49
|
"""
|
|
50
50
|
sections = []
|
|
51
51
|
for tag in ("script", "template"):
|
|
52
|
-
tag_match = re.search(rf"<{tag}[^>]*>(
|
|
52
|
+
tag_match = re.search(rf"<{tag}[^>]*>(.*)</{tag}>", component_source, re.DOTALL)
|
|
53
53
|
if tag_match:
|
|
54
54
|
sections.append(tag_match.group(1))
|
|
55
55
|
search_text = "\n".join(sections) if sections else component_source
|
|
@@ -114,8 +114,10 @@ def extract_own_members(component_source: str) -> set[str]:
|
|
|
114
114
|
brace_pos = ret.start() + ret.group().index("{")
|
|
115
115
|
own_members.update(extract_property_names(extract_brace_block(body, brace_pos)))
|
|
116
116
|
|
|
117
|
-
# computed, methods
|
|
118
|
-
|
|
117
|
+
# computed, methods sections (NOT watch — watch keys observe properties,
|
|
118
|
+
# they don't define/override them; a component watching a mixin property
|
|
119
|
+
# still needs the composable to provide that property)
|
|
120
|
+
for section in ("computed", "methods"):
|
|
119
121
|
match = re.search(rf"\b{section}\s*:\s*\{{", source)
|
|
120
122
|
if match:
|
|
121
123
|
own_members.update(
|
package/vue3_migration/models.py
CHANGED
|
@@ -56,15 +56,19 @@ def _extract_func_body(section_body: str, name: str) -> str | None:
|
|
|
56
56
|
|
|
57
57
|
def _extract_func_params(section_body: str, name: str) -> str:
|
|
58
58
|
"""Extract the parameter list of a named function inside a section body."""
|
|
59
|
-
# Standard: name(params)
|
|
60
|
-
m = re.search(rf'\b{re.escape(name)}\s*\(([^)]*)\)', section_body)
|
|
59
|
+
# Standard shorthand: name(params) {
|
|
60
|
+
m = re.search(rf'\b{re.escape(name)}\s*\(([^)]*)\)\s*\{{', section_body)
|
|
61
61
|
if m:
|
|
62
62
|
return m.group(1)
|
|
63
|
-
# name: function(params)
|
|
64
|
-
m = re.search(rf'\b{re.escape(name)}\s*:\s*function\s*\(([^)]*)\)', section_body)
|
|
63
|
+
# name: function(params) {
|
|
64
|
+
m = re.search(rf'\b{re.escape(name)}\s*:\s*function\s*\(([^)]*)\)\s*\{{', section_body)
|
|
65
65
|
if m:
|
|
66
66
|
return m.group(1)
|
|
67
|
-
# name: (params) =>
|
|
67
|
+
# name: (params) => {
|
|
68
|
+
m = re.search(rf'\b{re.escape(name)}\s*:\s*\(([^)]*)\)\s*=>\s*\{{', section_body)
|
|
69
|
+
if m:
|
|
70
|
+
return m.group(1)
|
|
71
|
+
# name: (params) => expr (single-expression arrow without braces)
|
|
68
72
|
m = re.search(rf'\b{re.escape(name)}\s*:\s*\(([^)]*)\)\s*=>', section_body)
|
|
69
73
|
if m:
|
|
70
74
|
return m.group(1)
|
|
@@ -10,6 +10,7 @@ from ..models import MixinMembers
|
|
|
10
10
|
from .this_rewriter import rewrite_this_refs, rewrite_this_dollar_refs
|
|
11
11
|
from .lifecycle_converter import (
|
|
12
12
|
extract_hook_body, convert_lifecycle_hooks, get_required_imports, HOOK_MAP,
|
|
13
|
+
find_lifecycle_referenced_members,
|
|
13
14
|
)
|
|
14
15
|
|
|
15
16
|
|
|
@@ -25,22 +26,41 @@ def _remove_stale_comments(source: str) -> str:
|
|
|
25
26
|
|
|
26
27
|
for line in lines:
|
|
27
28
|
# Check for "NOT defined" or "NOT returned" comments
|
|
28
|
-
# Match patterns like: "count is NOT defined", "count NOT returned",
|
|
29
|
-
|
|
29
|
+
# Match patterns like: "count is NOT defined", "count NOT returned",
|
|
30
|
+
# "count is NOT returned", "canDelete and hasRole are NOT defined"
|
|
31
|
+
not_match = re.search(r'//.*?\b(\w+)\s+(?:(?:is|are)\s+)?NOT\s+(?:defined|returned)', line)
|
|
30
32
|
if not_match:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
# Extract all lowercase-starting identifiers before "NOT" in the comment
|
|
34
|
+
comment_start = line.index('//')
|
|
35
|
+
comment_text = line[comment_start:]
|
|
36
|
+
not_word = re.search(r'\bNOT\b', comment_text)
|
|
37
|
+
not_pos = not_word.start() if not_word else comment_text.index('NOT')
|
|
38
|
+
before_not = comment_text[:not_pos]
|
|
39
|
+
noise = {'is', 'are', 'and', 'or', 'not', 'in', 'this', 'composable', 'the', 'from'}
|
|
40
|
+
identifiers = [m for m in re.findall(r'\b([a-z]\w*)\b', before_not) if m not in noise]
|
|
41
|
+
|
|
42
|
+
if identifiers:
|
|
43
|
+
# All mentioned identifiers must be defined for the comment to be stale
|
|
44
|
+
all_defined = all(
|
|
45
|
+
re.search(rf'\b(?:const|let|var|function)\s+{re.escape(name)}\b', source)
|
|
46
|
+
or re.search(rf'\breturn\s*\{{[^}}]*\b{re.escape(name)}\b', source, re.DOTALL)
|
|
47
|
+
for name in identifiers
|
|
48
|
+
)
|
|
49
|
+
if all_defined:
|
|
50
|
+
continue # Remove stale comment
|
|
51
|
+
else:
|
|
52
|
+
# Fallback to single-member logic
|
|
53
|
+
member_name = not_match.group(1)
|
|
54
|
+
if member_name.upper() == member_name and len(member_name) > 2:
|
|
55
|
+
result_lines.append(line)
|
|
56
|
+
continue
|
|
57
|
+
is_defined = (
|
|
58
|
+
re.search(rf'\b(?:const|let|var|function)\s+{re.escape(member_name)}\b', source)
|
|
59
|
+
or re.search(rf'\breturn\s*\{{[^}}]*\b{re.escape(member_name)}\b', source, re.DOTALL)
|
|
60
|
+
)
|
|
61
|
+
if is_defined:
|
|
62
|
+
continue
|
|
63
|
+
|
|
44
64
|
result_lines.append(line)
|
|
45
65
|
|
|
46
66
|
return '\n'.join(result_lines)
|
|
@@ -119,9 +139,22 @@ def add_keys_to_return(content: str, keys: list[str]) -> str:
|
|
|
119
139
|
if stripped and not stripped.startswith('}'):
|
|
120
140
|
member_indent = line[:len(line) - len(stripped)]
|
|
121
141
|
break
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
142
|
+
|
|
143
|
+
# Strip trailing whitespace before closing }, add comma if needed
|
|
144
|
+
before_close = content[:close_pos].rstrip()
|
|
145
|
+
if before_close and before_close[-1] not in (',', '{'):
|
|
146
|
+
before_close += ','
|
|
147
|
+
|
|
148
|
+
# Detect the closing brace's indentation from the original content
|
|
149
|
+
line_start_of_close = content.rfind('\n', 0, close_pos)
|
|
150
|
+
if line_start_of_close >= 0:
|
|
151
|
+
close_indent = content[line_start_of_close + 1:close_pos]
|
|
152
|
+
else:
|
|
153
|
+
close_indent = ''
|
|
154
|
+
|
|
155
|
+
# Insert new keys between last member and closing }
|
|
156
|
+
new_key_lines = "\n".join(f"{member_indent}{k}," for k in new_keys)
|
|
157
|
+
return before_close + "\n" + new_key_lines + "\n" + close_indent + content[close_pos:]
|
|
125
158
|
else:
|
|
126
159
|
# Single-line return: check if adding keys would make it too long
|
|
127
160
|
# Extract existing keys from the return block content
|
|
@@ -152,9 +185,10 @@ def add_members_to_composable(content: str, member_lines: list[str]) -> str:
|
|
|
152
185
|
"""
|
|
153
186
|
# Normalize CRLF (R-7)
|
|
154
187
|
content = content.replace('\r\n', '\n').replace('\r', '\n')
|
|
155
|
-
|
|
156
|
-
if not
|
|
188
|
+
matches = list(re.finditer(r'\n([ \t]*)\breturn\s*\{', content))
|
|
189
|
+
if not matches:
|
|
157
190
|
return content
|
|
191
|
+
m = matches[-1]
|
|
158
192
|
existing_ids = set(extract_all_identifiers(content))
|
|
159
193
|
lines_to_add = []
|
|
160
194
|
for line in member_lines:
|
|
@@ -512,6 +546,35 @@ def patch_composable(
|
|
|
512
546
|
if lifecycle_hooks:
|
|
513
547
|
hooks_to_add = _missing_hooks(content, lifecycle_hooks)
|
|
514
548
|
if hooks_to_add:
|
|
549
|
+
# Step 3a: Find mixin members referenced inside lifecycle hook bodies
|
|
550
|
+
# that are not yet in the composable (e.g. _handleEscapeKey).
|
|
551
|
+
all_member_names = (
|
|
552
|
+
mixin_members.data + mixin_members.computed
|
|
553
|
+
+ mixin_members.methods + mixin_members.watch
|
|
554
|
+
)
|
|
555
|
+
lifecycle_deps = find_lifecycle_referenced_members(
|
|
556
|
+
mixin_content, hooks_to_add, all_member_names,
|
|
557
|
+
)
|
|
558
|
+
existing_ids = set(extract_all_identifiers(content))
|
|
559
|
+
missing_deps = [m for m in lifecycle_deps if m not in existing_ids]
|
|
560
|
+
if missing_deps:
|
|
561
|
+
dep_decls = [
|
|
562
|
+
generate_member_declaration(
|
|
563
|
+
name, mixin_content, mixin_members,
|
|
564
|
+
ref_members, plain_members, indent,
|
|
565
|
+
)
|
|
566
|
+
for name in missing_deps
|
|
567
|
+
]
|
|
568
|
+
content = add_members_to_composable(content, dep_decls)
|
|
569
|
+
content = add_keys_to_return(content, missing_deps)
|
|
570
|
+
if any("watch(" in d for d in dep_decls):
|
|
571
|
+
content = _add_vue_import(content, "watch")
|
|
572
|
+
if any("ref(" in d for d in dep_decls):
|
|
573
|
+
content = _add_vue_import(content, "ref")
|
|
574
|
+
if any("computed(" in d for d in dep_decls):
|
|
575
|
+
content = _add_vue_import(content, "computed")
|
|
576
|
+
|
|
577
|
+
# Step 3b: Convert and insert lifecycle hooks
|
|
515
578
|
inline_lines, wrapped_lines = convert_lifecycle_hooks(
|
|
516
579
|
mixin_content, hooks_to_add, ref_members, plain_members, indent,
|
|
517
580
|
)
|
|
@@ -378,27 +378,11 @@ def plan_component_injections(
|
|
|
378
378
|
severity="warning",
|
|
379
379
|
))
|
|
380
380
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
for
|
|
385
|
-
|
|
386
|
-
if new != content:
|
|
387
|
-
changes_desc.append(f"Removed import {entry.mixin_stem}")
|
|
388
|
-
content = new
|
|
389
|
-
injectable = entry.classification.injectable if entry.classification else entry.used_members
|
|
390
|
-
if injectable and entry.composable:
|
|
391
|
-
new = add_composable_import(content, entry.composable.fn_name, entry.composable.import_path)
|
|
392
|
-
if new != content:
|
|
393
|
-
changes_desc.append(f"Added import {{{entry.composable.fn_name}}}")
|
|
394
|
-
content = new
|
|
395
|
-
|
|
396
|
-
for entry in ready_entries:
|
|
397
|
-
new = remove_mixin_from_array(content, entry.local_name)
|
|
398
|
-
if new != content:
|
|
399
|
-
changes_desc.append(f"Removed {entry.local_name} from mixins")
|
|
400
|
-
content = new
|
|
401
|
-
|
|
381
|
+
# Pre-compute which entries will produce a composable call.
|
|
382
|
+
# Only migratable entries (those with injectable members) get their
|
|
383
|
+
# mixin removed. Entries that won't produce a composable call are
|
|
384
|
+
# skipped and flagged for manual review.
|
|
385
|
+
migratable_entries = []
|
|
402
386
|
composable_calls = []
|
|
403
387
|
|
|
404
388
|
for entry in ready_entries:
|
|
@@ -420,15 +404,114 @@ def plan_component_injections(
|
|
|
420
404
|
# precedence over setup() returns, so injecting them is redundant.
|
|
421
405
|
# Keep lifecycle-referenced members even if overridden, since the
|
|
422
406
|
# composable's lifecycle wrapper closure may depend on them.
|
|
407
|
+
overridden_in_injectable: list[str] = []
|
|
423
408
|
if injectable and entry.composable:
|
|
424
409
|
lifecycle_members_set = set(lifecycle_members) if entry.lifecycle_hooks else set()
|
|
410
|
+
overridden_in_injectable = [
|
|
411
|
+
m for m in injectable
|
|
412
|
+
if m in own_members and m not in lifecycle_members_set
|
|
413
|
+
]
|
|
425
414
|
injectable = [
|
|
426
415
|
m for m in injectable
|
|
427
416
|
if m not in own_members or m in lifecycle_members_set
|
|
428
417
|
]
|
|
429
418
|
|
|
430
419
|
if injectable and entry.composable:
|
|
420
|
+
migratable_entries.append(entry)
|
|
431
421
|
composable_calls.append((entry.composable.fn_name, injectable))
|
|
422
|
+
# Warn about overridden members that won't be destructured
|
|
423
|
+
if overridden_in_injectable:
|
|
424
|
+
from ..models import MigrationWarning
|
|
425
|
+
names = ", ".join(overridden_in_injectable)
|
|
426
|
+
entry.warnings.append(MigrationWarning(
|
|
427
|
+
mixin_stem=entry.mixin_stem,
|
|
428
|
+
category="overridden-member",
|
|
429
|
+
message=(
|
|
430
|
+
f"Component overrides mixin member(s): {names}. "
|
|
431
|
+
"These are NOT destructured from the composable."
|
|
432
|
+
),
|
|
433
|
+
action_required=(
|
|
434
|
+
f"Verify that the component's own {names} "
|
|
435
|
+
"implementation does not depend on other mixin members."
|
|
436
|
+
),
|
|
437
|
+
line_hint=None,
|
|
438
|
+
severity="info",
|
|
439
|
+
))
|
|
440
|
+
else:
|
|
441
|
+
# Entry won't produce a composable call — keep the mixin.
|
|
442
|
+
from ..models import MigrationWarning
|
|
443
|
+
if not entry.used_members:
|
|
444
|
+
if entry.lifecycle_hooks:
|
|
445
|
+
hooks_str = ", ".join(entry.lifecycle_hooks)
|
|
446
|
+
entry.warnings.append(MigrationWarning(
|
|
447
|
+
mixin_stem=entry.mixin_stem,
|
|
448
|
+
category="skipped-lifecycle-only",
|
|
449
|
+
message=(
|
|
450
|
+
f"Mixin '{entry.mixin_stem}' was NOT migrated: "
|
|
451
|
+
f"it provides lifecycle hooks ({hooks_str}) but "
|
|
452
|
+
"no members are directly referenced by the component."
|
|
453
|
+
),
|
|
454
|
+
action_required=(
|
|
455
|
+
"Manually convert lifecycle hooks to the composable, "
|
|
456
|
+
"or remove the mixin if unused."
|
|
457
|
+
),
|
|
458
|
+
line_hint=None,
|
|
459
|
+
severity="warning",
|
|
460
|
+
))
|
|
461
|
+
else:
|
|
462
|
+
entry.warnings.append(MigrationWarning(
|
|
463
|
+
mixin_stem=entry.mixin_stem,
|
|
464
|
+
category="skipped-no-usage",
|
|
465
|
+
message=(
|
|
466
|
+
f"Mixin '{entry.mixin_stem}' was NOT migrated: "
|
|
467
|
+
"no members are referenced by the component."
|
|
468
|
+
),
|
|
469
|
+
action_required=(
|
|
470
|
+
"Remove the mixin if unused, or manually convert "
|
|
471
|
+
"if it provides side-effect functionality."
|
|
472
|
+
),
|
|
473
|
+
line_hint=None,
|
|
474
|
+
severity="warning",
|
|
475
|
+
))
|
|
476
|
+
else:
|
|
477
|
+
overridden_names = ", ".join(
|
|
478
|
+
entry.classification.overridden + entry.classification.overridden_not_returned
|
|
479
|
+
) if entry.classification else ""
|
|
480
|
+
entry.warnings.append(MigrationWarning(
|
|
481
|
+
mixin_stem=entry.mixin_stem,
|
|
482
|
+
category="skipped-all-overridden",
|
|
483
|
+
message=(
|
|
484
|
+
f"Mixin '{entry.mixin_stem}' was NOT migrated: "
|
|
485
|
+
f"all used members ({overridden_names}) are overridden "
|
|
486
|
+
"by the component."
|
|
487
|
+
),
|
|
488
|
+
action_required=(
|
|
489
|
+
"Remove the mixin if the component's overrides are "
|
|
490
|
+
"self-contained, or keep it if they depend on mixin internals."
|
|
491
|
+
),
|
|
492
|
+
line_hint=None,
|
|
493
|
+
severity="info",
|
|
494
|
+
))
|
|
495
|
+
|
|
496
|
+
content = comp_source
|
|
497
|
+
changes_desc = []
|
|
498
|
+
|
|
499
|
+
for entry in migratable_entries:
|
|
500
|
+
new = remove_import_line(content, entry.mixin_stem)
|
|
501
|
+
if new != content:
|
|
502
|
+
changes_desc.append(f"Removed import {entry.mixin_stem}")
|
|
503
|
+
content = new
|
|
504
|
+
if entry.composable:
|
|
505
|
+
new = add_composable_import(content, entry.composable.fn_name, entry.composable.import_path)
|
|
506
|
+
if new != content:
|
|
507
|
+
changes_desc.append(f"Added import {{{entry.composable.fn_name}}}")
|
|
508
|
+
content = new
|
|
509
|
+
|
|
510
|
+
for entry in migratable_entries:
|
|
511
|
+
new = remove_mixin_from_array(content, entry.local_name)
|
|
512
|
+
if new != content:
|
|
513
|
+
changes_desc.append(f"Removed {entry.local_name} from mixins")
|
|
514
|
+
content = new
|
|
432
515
|
|
|
433
516
|
if composable_calls:
|
|
434
517
|
new = inject_setup(
|
|
@@ -454,11 +537,57 @@ def plan_component_injections(
|
|
|
454
537
|
return component_changes
|
|
455
538
|
|
|
456
539
|
|
|
540
|
+
def plan_regenerated_composables(
|
|
541
|
+
entries_by_component: list[tuple[Path, list[MixinEntry]]],
|
|
542
|
+
project_root: Path,
|
|
543
|
+
) -> list[FileChange]:
|
|
544
|
+
"""Regenerate existing composables from their mixin source.
|
|
545
|
+
|
|
546
|
+
For each mixin that HAS an existing composable on disk, generates
|
|
547
|
+
fresh content using the generator instead of just patching.
|
|
548
|
+
"""
|
|
549
|
+
seen_stems: set[str] = set()
|
|
550
|
+
changes = []
|
|
551
|
+
|
|
552
|
+
for _comp_path, entries in entries_by_component:
|
|
553
|
+
for entry in entries:
|
|
554
|
+
if entry.mixin_stem in seen_stems:
|
|
555
|
+
continue
|
|
556
|
+
# Only regenerate if composable already exists (not BLOCKED_NO_COMPOSABLE)
|
|
557
|
+
if entry.status == MigrationStatus.BLOCKED_NO_COMPOSABLE:
|
|
558
|
+
continue
|
|
559
|
+
if not entry.composable:
|
|
560
|
+
continue
|
|
561
|
+
seen_stems.add(entry.mixin_stem)
|
|
562
|
+
|
|
563
|
+
mixin_source = entry.mixin_path.read_text(errors="ignore").replace('\r\n', '\n').replace('\r', '\n')
|
|
564
|
+
content = generate_composable_from_mixin(
|
|
565
|
+
mixin_source=mixin_source,
|
|
566
|
+
mixin_stem=entry.mixin_stem,
|
|
567
|
+
mixin_members=entry.members,
|
|
568
|
+
lifecycle_hooks=entry.lifecycle_hooks,
|
|
569
|
+
)
|
|
570
|
+
original = entry.composable.file_path.read_text(errors="ignore").replace('\r\n', '\n').replace('\r', '\n')
|
|
571
|
+
if content != original:
|
|
572
|
+
changes.append(FileChange(
|
|
573
|
+
file_path=entry.composable.file_path,
|
|
574
|
+
original_content=original,
|
|
575
|
+
new_content=content,
|
|
576
|
+
changes=[f"Regenerated composable from {entry.mixin_stem}"],
|
|
577
|
+
))
|
|
578
|
+
return changes
|
|
579
|
+
|
|
580
|
+
|
|
457
581
|
def _build_all_composable_changes(
|
|
458
582
|
entries: list[tuple[Path, list[MixinEntry]]],
|
|
459
583
|
project_root: Path,
|
|
584
|
+
config: MigrationConfig | None = None,
|
|
460
585
|
) -> list[FileChange]:
|
|
461
586
|
"""Combine patched-existing + newly-generated composable changes."""
|
|
587
|
+
if config and config.regenerate:
|
|
588
|
+
regenerated = plan_regenerated_composables(entries, project_root)
|
|
589
|
+
generated = plan_new_composables(entries, project_root)
|
|
590
|
+
return regenerated + generated
|
|
462
591
|
patched = plan_composable_patches(entries)
|
|
463
592
|
generated = plan_new_composables(entries, project_root)
|
|
464
593
|
return patched + generated
|
|
@@ -470,7 +599,7 @@ def run(project_root: Path, config: MigrationConfig) -> MigrationPlan:
|
|
|
470
599
|
No file I/O. Returns a MigrationPlan the CLI can show as a diff and write.
|
|
471
600
|
"""
|
|
472
601
|
entries = collect_all_mixin_entries(project_root, config)
|
|
473
|
-
composable_changes = _build_all_composable_changes(entries, project_root)
|
|
602
|
+
composable_changes = _build_all_composable_changes(entries, project_root, config)
|
|
474
603
|
component_changes = plan_component_injections(entries, composable_changes, config)
|
|
475
604
|
return MigrationPlan(
|
|
476
605
|
component_changes=component_changes,
|
|
@@ -509,7 +638,7 @@ def run_scoped(
|
|
|
509
638
|
if any(e.mixin_stem == mixin_stem for e in es)
|
|
510
639
|
]
|
|
511
640
|
|
|
512
|
-
composable_changes = _build_all_composable_changes(entries, project_root)
|
|
641
|
+
composable_changes = _build_all_composable_changes(entries, project_root, config)
|
|
513
642
|
component_changes = plan_component_injections(entries, composable_changes, config)
|
|
514
643
|
return MigrationPlan(
|
|
515
644
|
component_changes=component_changes,
|