vue3-migration 1.4.0 → 1.4.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/__init__.py +1 -1
- package/vue3_migration/cli.py +2 -1
- package/vue3_migration/models.py +1 -0
- package/vue3_migration/transform/composable_generator.py +9 -5
- package/vue3_migration/transform/composable_patcher.py +42 -16
- package/vue3_migration/workflows/auto_migrate_workflow.py +48 -2
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)
|
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)
|
|
@@ -25,22 +25,41 @@ def _remove_stale_comments(source: str) -> str:
|
|
|
25
25
|
|
|
26
26
|
for line in lines:
|
|
27
27
|
# Check for "NOT defined" or "NOT returned" comments
|
|
28
|
-
# Match patterns like: "count is NOT defined", "count NOT returned",
|
|
29
|
-
|
|
28
|
+
# Match patterns like: "count is NOT defined", "count NOT returned",
|
|
29
|
+
# "count is NOT returned", "canDelete and hasRole are NOT defined"
|
|
30
|
+
not_match = re.search(r'//.*?\b(\w+)\s+(?:(?:is|are)\s+)?NOT\s+(?:defined|returned)', line)
|
|
30
31
|
if not_match:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
# Extract all lowercase-starting identifiers before "NOT" in the comment
|
|
33
|
+
comment_start = line.index('//')
|
|
34
|
+
comment_text = line[comment_start:]
|
|
35
|
+
not_word = re.search(r'\bNOT\b', comment_text)
|
|
36
|
+
not_pos = not_word.start() if not_word else comment_text.index('NOT')
|
|
37
|
+
before_not = comment_text[:not_pos]
|
|
38
|
+
noise = {'is', 'are', 'and', 'or', 'not', 'in', 'this', 'composable', 'the', 'from'}
|
|
39
|
+
identifiers = [m for m in re.findall(r'\b([a-z]\w*)\b', before_not) if m not in noise]
|
|
40
|
+
|
|
41
|
+
if identifiers:
|
|
42
|
+
# All mentioned identifiers must be defined for the comment to be stale
|
|
43
|
+
all_defined = all(
|
|
44
|
+
re.search(rf'\b(?:const|let|var|function)\s+{re.escape(name)}\b', source)
|
|
45
|
+
or re.search(rf'\breturn\s*\{{[^}}]*\b{re.escape(name)}\b', source, re.DOTALL)
|
|
46
|
+
for name in identifiers
|
|
47
|
+
)
|
|
48
|
+
if all_defined:
|
|
49
|
+
continue # Remove stale comment
|
|
50
|
+
else:
|
|
51
|
+
# Fallback to single-member logic
|
|
52
|
+
member_name = not_match.group(1)
|
|
53
|
+
if member_name.upper() == member_name and len(member_name) > 2:
|
|
54
|
+
result_lines.append(line)
|
|
55
|
+
continue
|
|
56
|
+
is_defined = (
|
|
57
|
+
re.search(rf'\b(?:const|let|var|function)\s+{re.escape(member_name)}\b', source)
|
|
58
|
+
or re.search(rf'\breturn\s*\{{[^}}]*\b{re.escape(member_name)}\b', source, re.DOTALL)
|
|
59
|
+
)
|
|
60
|
+
if is_defined:
|
|
61
|
+
continue
|
|
62
|
+
|
|
44
63
|
result_lines.append(line)
|
|
45
64
|
|
|
46
65
|
return '\n'.join(result_lines)
|
|
@@ -119,9 +138,16 @@ def add_keys_to_return(content: str, keys: list[str]) -> str:
|
|
|
119
138
|
if stripped and not stripped.startswith('}'):
|
|
120
139
|
member_indent = line[:len(line) - len(stripped)]
|
|
121
140
|
break
|
|
141
|
+
# Ensure the last existing member has a trailing comma
|
|
142
|
+
before_close = content[:close_pos]
|
|
143
|
+
temp = before_close.rstrip()
|
|
144
|
+
if temp and temp[-1] not in (',', '{'):
|
|
145
|
+
before_close = temp + ',' + before_close[len(temp):]
|
|
146
|
+
close_pos += 1
|
|
147
|
+
|
|
122
148
|
# Insert new keys before the closing }
|
|
123
149
|
new_key_lines = "".join(f"{member_indent}{k},\n" for k in new_keys)
|
|
124
|
-
return
|
|
150
|
+
return before_close + new_key_lines + content[close_pos:]
|
|
125
151
|
else:
|
|
126
152
|
# Single-line return: check if adding keys would make it too long
|
|
127
153
|
# Extract existing keys from the return block content
|
|
@@ -454,11 +454,57 @@ def plan_component_injections(
|
|
|
454
454
|
return component_changes
|
|
455
455
|
|
|
456
456
|
|
|
457
|
+
def plan_regenerated_composables(
|
|
458
|
+
entries_by_component: list[tuple[Path, list[MixinEntry]]],
|
|
459
|
+
project_root: Path,
|
|
460
|
+
) -> list[FileChange]:
|
|
461
|
+
"""Regenerate existing composables from their mixin source.
|
|
462
|
+
|
|
463
|
+
For each mixin that HAS an existing composable on disk, generates
|
|
464
|
+
fresh content using the generator instead of just patching.
|
|
465
|
+
"""
|
|
466
|
+
seen_stems: set[str] = set()
|
|
467
|
+
changes = []
|
|
468
|
+
|
|
469
|
+
for _comp_path, entries in entries_by_component:
|
|
470
|
+
for entry in entries:
|
|
471
|
+
if entry.mixin_stem in seen_stems:
|
|
472
|
+
continue
|
|
473
|
+
# Only regenerate if composable already exists (not BLOCKED_NO_COMPOSABLE)
|
|
474
|
+
if entry.status == MigrationStatus.BLOCKED_NO_COMPOSABLE:
|
|
475
|
+
continue
|
|
476
|
+
if not entry.composable:
|
|
477
|
+
continue
|
|
478
|
+
seen_stems.add(entry.mixin_stem)
|
|
479
|
+
|
|
480
|
+
mixin_source = entry.mixin_path.read_text(errors="ignore").replace('\r\n', '\n').replace('\r', '\n')
|
|
481
|
+
content = generate_composable_from_mixin(
|
|
482
|
+
mixin_source=mixin_source,
|
|
483
|
+
mixin_stem=entry.mixin_stem,
|
|
484
|
+
mixin_members=entry.members,
|
|
485
|
+
lifecycle_hooks=entry.lifecycle_hooks,
|
|
486
|
+
)
|
|
487
|
+
original = entry.composable.file_path.read_text(errors="ignore").replace('\r\n', '\n').replace('\r', '\n')
|
|
488
|
+
if content != original:
|
|
489
|
+
changes.append(FileChange(
|
|
490
|
+
file_path=entry.composable.file_path,
|
|
491
|
+
original_content=original,
|
|
492
|
+
new_content=content,
|
|
493
|
+
changes=[f"Regenerated composable from {entry.mixin_stem}"],
|
|
494
|
+
))
|
|
495
|
+
return changes
|
|
496
|
+
|
|
497
|
+
|
|
457
498
|
def _build_all_composable_changes(
|
|
458
499
|
entries: list[tuple[Path, list[MixinEntry]]],
|
|
459
500
|
project_root: Path,
|
|
501
|
+
config: MigrationConfig | None = None,
|
|
460
502
|
) -> list[FileChange]:
|
|
461
503
|
"""Combine patched-existing + newly-generated composable changes."""
|
|
504
|
+
if config and config.regenerate:
|
|
505
|
+
regenerated = plan_regenerated_composables(entries, project_root)
|
|
506
|
+
generated = plan_new_composables(entries, project_root)
|
|
507
|
+
return regenerated + generated
|
|
462
508
|
patched = plan_composable_patches(entries)
|
|
463
509
|
generated = plan_new_composables(entries, project_root)
|
|
464
510
|
return patched + generated
|
|
@@ -470,7 +516,7 @@ def run(project_root: Path, config: MigrationConfig) -> MigrationPlan:
|
|
|
470
516
|
No file I/O. Returns a MigrationPlan the CLI can show as a diff and write.
|
|
471
517
|
"""
|
|
472
518
|
entries = collect_all_mixin_entries(project_root, config)
|
|
473
|
-
composable_changes = _build_all_composable_changes(entries, project_root)
|
|
519
|
+
composable_changes = _build_all_composable_changes(entries, project_root, config)
|
|
474
520
|
component_changes = plan_component_injections(entries, composable_changes, config)
|
|
475
521
|
return MigrationPlan(
|
|
476
522
|
component_changes=component_changes,
|
|
@@ -509,7 +555,7 @@ def run_scoped(
|
|
|
509
555
|
if any(e.mixin_stem == mixin_stem for e in es)
|
|
510
556
|
]
|
|
511
557
|
|
|
512
|
-
composable_changes = _build_all_composable_changes(entries, project_root)
|
|
558
|
+
composable_changes = _build_all_composable_changes(entries, project_root, config)
|
|
513
559
|
component_changes = plan_component_injections(entries, composable_changes, config)
|
|
514
560
|
return MigrationPlan(
|
|
515
561
|
component_changes=component_changes,
|