vue3-migration 1.0.0
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/bin/cli.js +41 -0
- package/package.json +24 -0
- package/vue3_migration/__init__.py +3 -0
- package/vue3_migration/__main__.py +6 -0
- package/vue3_migration/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vue3_migration/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vue3_migration/__pycache__/__main__.cpython-312.pyc +0 -0
- package/vue3_migration/__pycache__/cli.cpython-312.pyc +0 -0
- package/vue3_migration/__pycache__/models.cpython-312.pyc +0 -0
- package/vue3_migration/__pycache__/models.cpython-313.pyc +0 -0
- package/vue3_migration/cli.py +323 -0
- package/vue3_migration/core/__init__.py +0 -0
- package/vue3_migration/core/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vue3_migration/core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vue3_migration/core/__pycache__/component_analyzer.cpython-312.pyc +0 -0
- package/vue3_migration/core/__pycache__/component_analyzer.cpython-313.pyc +0 -0
- package/vue3_migration/core/__pycache__/composable_analyzer.cpython-312.pyc +0 -0
- package/vue3_migration/core/__pycache__/composable_analyzer.cpython-313.pyc +0 -0
- package/vue3_migration/core/__pycache__/composable_search.cpython-312.pyc +0 -0
- package/vue3_migration/core/__pycache__/composable_search.cpython-313.pyc +0 -0
- package/vue3_migration/core/__pycache__/file_resolver.cpython-312.pyc +0 -0
- package/vue3_migration/core/__pycache__/file_resolver.cpython-313.pyc +0 -0
- package/vue3_migration/core/__pycache__/js_parser.cpython-312.pyc +0 -0
- package/vue3_migration/core/__pycache__/js_parser.cpython-313.pyc +0 -0
- package/vue3_migration/core/__pycache__/mixin_analyzer.cpython-312.pyc +0 -0
- package/vue3_migration/core/__pycache__/mixin_analyzer.cpython-313.pyc +0 -0
- package/vue3_migration/core/component_analyzer.py +76 -0
- package/vue3_migration/core/composable_analyzer.py +86 -0
- package/vue3_migration/core/composable_search.py +141 -0
- package/vue3_migration/core/file_resolver.py +98 -0
- package/vue3_migration/core/js_parser.py +177 -0
- package/vue3_migration/core/mixin_analyzer.py +54 -0
- package/vue3_migration/models.py +168 -0
- package/vue3_migration/reporting/__init__.py +0 -0
- package/vue3_migration/reporting/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vue3_migration/reporting/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vue3_migration/reporting/__pycache__/markdown.cpython-312.pyc +0 -0
- package/vue3_migration/reporting/__pycache__/markdown.cpython-313.pyc +0 -0
- package/vue3_migration/reporting/__pycache__/terminal.cpython-312.pyc +0 -0
- package/vue3_migration/reporting/__pycache__/terminal.cpython-313.pyc +0 -0
- package/vue3_migration/reporting/markdown.py +202 -0
- package/vue3_migration/reporting/terminal.py +61 -0
- package/vue3_migration/transform/__init__.py +0 -0
- package/vue3_migration/transform/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vue3_migration/transform/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vue3_migration/transform/__pycache__/injector.cpython-312.pyc +0 -0
- package/vue3_migration/transform/__pycache__/injector.cpython-313.pyc +0 -0
- package/vue3_migration/transform/injector.py +134 -0
- package/vue3_migration/workflows/__init__.py +0 -0
- package/vue3_migration/workflows/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vue3_migration/workflows/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vue3_migration/workflows/__pycache__/component_workflow.cpython-312.pyc +0 -0
- package/vue3_migration/workflows/__pycache__/component_workflow.cpython-313.pyc +0 -0
- package/vue3_migration/workflows/__pycache__/mixin_workflow.cpython-312.pyc +0 -0
- package/vue3_migration/workflows/component_workflow.py +446 -0
- package/vue3_migration/workflows/mixin_workflow.py +387 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const pythonCandidates = process.platform === 'win32'
|
|
8
|
+
? ['python', 'python3', 'py']
|
|
9
|
+
: ['python3', 'python'];
|
|
10
|
+
|
|
11
|
+
// The package root is one level up from bin/
|
|
12
|
+
const pkgRoot = path.join(__dirname, '..');
|
|
13
|
+
|
|
14
|
+
let python = null;
|
|
15
|
+
for (const candidate of pythonCandidates) {
|
|
16
|
+
const probe = spawnSync(candidate, ['--version'], { encoding: 'utf8' });
|
|
17
|
+
if (probe.status === 0) {
|
|
18
|
+
python = candidate;
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!python) {
|
|
24
|
+
console.error(
|
|
25
|
+
'\n Error: Python 3 is required but was not found in PATH.\n' +
|
|
26
|
+
' Install Python 3 from https://python.org and try again.\n'
|
|
27
|
+
);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const result = spawnSync(
|
|
32
|
+
python,
|
|
33
|
+
['-m', 'vue3_migration', ...process.argv.slice(2)],
|
|
34
|
+
{
|
|
35
|
+
cwd: pkgRoot,
|
|
36
|
+
stdio: 'inherit',
|
|
37
|
+
env: process.env,
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
process.exit(result.status ?? 1);
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vue3-migration",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Vue 2 mixin to Vue 3 composable migration tool",
|
|
5
|
+
"bin": {
|
|
6
|
+
"vue3-migration": "./bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"vue3_migration/"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=14"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"vue",
|
|
17
|
+
"vue3",
|
|
18
|
+
"migration",
|
|
19
|
+
"composable",
|
|
20
|
+
"mixin",
|
|
21
|
+
"vue2"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT"
|
|
24
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI entry point for the Vue Mixin Migration Tool.
|
|
3
|
+
|
|
4
|
+
Provides interactive menu and subcommands: scan, component, audit.
|
|
5
|
+
Replaces the original migrate.py subprocess-based delegation with
|
|
6
|
+
direct module imports.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
from collections import Counter
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from .core.component_analyzer import parse_imports, parse_mixins_array
|
|
17
|
+
from .core.composable_search import (
|
|
18
|
+
collect_composable_stems,
|
|
19
|
+
find_composable_dirs,
|
|
20
|
+
mixin_has_composable,
|
|
21
|
+
)
|
|
22
|
+
from .core.file_resolver import resolve_mixin_stem
|
|
23
|
+
from .models import MigrationConfig
|
|
24
|
+
from .reporting.terminal import bold, cyan, dim, green, red, yellow
|
|
25
|
+
from .workflows import component_workflow, mixin_workflow
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# =============================================================================
|
|
29
|
+
# Scan
|
|
30
|
+
# =============================================================================
|
|
31
|
+
|
|
32
|
+
def _parse_mixins_array(source: str) -> list[str]:
|
|
33
|
+
"""Quick extraction of mixin names from `mixins: [A, B, C]`."""
|
|
34
|
+
return parse_mixins_array(source)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _parse_imports(source: str) -> dict[str, str]:
|
|
38
|
+
"""Parse import statements. Returns { local_name: import_path }."""
|
|
39
|
+
return parse_imports(source)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _find_mixin_file(mixin_stem: str, project_root: Path) -> Optional[Path]:
|
|
43
|
+
"""Try to find a mixin file by its stem name."""
|
|
44
|
+
for dirpath, _, filenames in os.walk(project_root):
|
|
45
|
+
rel = Path(dirpath).relative_to(project_root)
|
|
46
|
+
if any(p in {"node_modules", "dist", ".git"} for p in rel.parts):
|
|
47
|
+
continue
|
|
48
|
+
for fn in filenames:
|
|
49
|
+
fp = Path(dirpath) / fn
|
|
50
|
+
if fp.suffix in (".js", ".ts") and fp.stem == mixin_stem:
|
|
51
|
+
return fp
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def scan_project(project_root: Path, config: MigrationConfig):
|
|
56
|
+
"""Walk the project and find every component that still uses mixins."""
|
|
57
|
+
|
|
58
|
+
print(f"\n{bold('Scanning project for components with mixins...')}\n")
|
|
59
|
+
|
|
60
|
+
components = [] # list of (rel_path, mixin_local_names, mixin_stems)
|
|
61
|
+
|
|
62
|
+
for dirpath, _, filenames in os.walk(project_root):
|
|
63
|
+
rel_dir = Path(dirpath).relative_to(project_root)
|
|
64
|
+
if any(part in config.skip_dirs for part in rel_dir.parts):
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
for fn in filenames:
|
|
68
|
+
if not fn.endswith(".vue"):
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
filepath = Path(dirpath) / fn
|
|
72
|
+
try:
|
|
73
|
+
source = filepath.read_text(errors="ignore")
|
|
74
|
+
except Exception:
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
mixin_names = _parse_mixins_array(source)
|
|
78
|
+
if not mixin_names:
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
imports = _parse_imports(source)
|
|
82
|
+
stems = []
|
|
83
|
+
for name in mixin_names:
|
|
84
|
+
imp = imports.get(name, "")
|
|
85
|
+
stems.append(resolve_mixin_stem(imp) if imp else name)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
rel = filepath.relative_to(project_root)
|
|
89
|
+
except ValueError:
|
|
90
|
+
rel = filepath
|
|
91
|
+
|
|
92
|
+
components.append((rel, mixin_names, stems))
|
|
93
|
+
|
|
94
|
+
if not components:
|
|
95
|
+
print(f"{green('No components with mixins found.')} Migration complete!\n")
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
# Sort by number of mixins (most first)
|
|
99
|
+
components.sort(key=lambda c: -len(c[1]))
|
|
100
|
+
|
|
101
|
+
# Count mixin frequencies
|
|
102
|
+
mixin_counter: Counter[str] = Counter()
|
|
103
|
+
for _, _, stems in components:
|
|
104
|
+
for stem in stems:
|
|
105
|
+
mixin_counter[stem] += 1
|
|
106
|
+
|
|
107
|
+
# Check which composables already exist
|
|
108
|
+
composable_dirs = find_composable_dirs(project_root)
|
|
109
|
+
composable_stems = collect_composable_stems(composable_dirs)
|
|
110
|
+
|
|
111
|
+
# --- Display results ---
|
|
112
|
+
print(f"{bold('Project Overview')}")
|
|
113
|
+
print(f" Components with mixins: {yellow(str(len(components)))}")
|
|
114
|
+
print(f" Unique mixins in use: {yellow(str(len(mixin_counter)))}")
|
|
115
|
+
print(f" Composables directories: {len(composable_dirs)}")
|
|
116
|
+
print()
|
|
117
|
+
|
|
118
|
+
# Mixin frequency table
|
|
119
|
+
print(f"{bold('Mixin Usage Across Project')}")
|
|
120
|
+
print(f" {'Mixin':<40} {'Used in':<10} {'Composable?'}")
|
|
121
|
+
print(f" {'-' * 40} {'-' * 10} {'-' * 12}")
|
|
122
|
+
|
|
123
|
+
for mixin_stem, count in mixin_counter.most_common():
|
|
124
|
+
has_comp = mixin_has_composable(mixin_stem, composable_stems)
|
|
125
|
+
comp_status = green("found") if has_comp else dim("not found")
|
|
126
|
+
print(f" {mixin_stem:<40} {count:<10} {comp_status}")
|
|
127
|
+
|
|
128
|
+
print()
|
|
129
|
+
|
|
130
|
+
# Component list
|
|
131
|
+
print(f"{bold('Components')}")
|
|
132
|
+
print()
|
|
133
|
+
|
|
134
|
+
for idx, (rel_path, mixin_names, stems) in enumerate(components, 1):
|
|
135
|
+
mixin_count = len(mixin_names)
|
|
136
|
+
color = green if mixin_count <= 2 else (yellow if mixin_count <= 4 else red)
|
|
137
|
+
count_str = color(f"{mixin_count} mixin{'s' if mixin_count != 1 else ''}")
|
|
138
|
+
|
|
139
|
+
covered = sum(1 for s in stems if mixin_has_composable(s, composable_stems))
|
|
140
|
+
if covered == len(stems):
|
|
141
|
+
coverage = green("all composables found")
|
|
142
|
+
elif covered > 0:
|
|
143
|
+
coverage = yellow(f"{covered}/{len(stems)} composables found")
|
|
144
|
+
else:
|
|
145
|
+
coverage = dim("no composables yet")
|
|
146
|
+
|
|
147
|
+
print(f" {bold(str(idx) + '.')} {green(str(rel_path))}")
|
|
148
|
+
print(f" {count_str} -- {coverage}")
|
|
149
|
+
print(f" {dim(', '.join(stems))}")
|
|
150
|
+
|
|
151
|
+
# Actions
|
|
152
|
+
print(f"\n{bold('What would you like to do?')}\n")
|
|
153
|
+
print(f" Enter a {bold('number')} (1-{len(components)}) to migrate that component.")
|
|
154
|
+
print(f" Enter a {bold('mixin name')} to audit it across the project.")
|
|
155
|
+
print(f" Enter {bold('q')} to quit.\n")
|
|
156
|
+
|
|
157
|
+
choice = input(" > ").strip()
|
|
158
|
+
|
|
159
|
+
if not choice or choice.lower() == "q":
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
# Number -> migrate component
|
|
163
|
+
try:
|
|
164
|
+
idx = int(choice)
|
|
165
|
+
if 1 <= idx <= len(components):
|
|
166
|
+
rel_path = components[idx - 1][0]
|
|
167
|
+
print()
|
|
168
|
+
component_workflow.run(str(rel_path), config)
|
|
169
|
+
return
|
|
170
|
+
else:
|
|
171
|
+
print(f" {yellow('Number out of range.')}")
|
|
172
|
+
return
|
|
173
|
+
except ValueError:
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
# Text -> try to match a mixin name
|
|
177
|
+
matched_stem = None
|
|
178
|
+
for mixin_stem in mixin_counter:
|
|
179
|
+
if choice.lower() == mixin_stem.lower():
|
|
180
|
+
matched_stem = mixin_stem
|
|
181
|
+
break
|
|
182
|
+
|
|
183
|
+
if not matched_stem:
|
|
184
|
+
for mixin_stem in mixin_counter:
|
|
185
|
+
if choice.lower() in mixin_stem.lower():
|
|
186
|
+
matched_stem = mixin_stem
|
|
187
|
+
break
|
|
188
|
+
|
|
189
|
+
if matched_stem:
|
|
190
|
+
mixin_file = _find_mixin_file(matched_stem, project_root)
|
|
191
|
+
if mixin_file:
|
|
192
|
+
print(f"\n Auditing {green(matched_stem)} -> {green(str(mixin_file))}\n")
|
|
193
|
+
mixin_workflow.run(str(mixin_file), config=config)
|
|
194
|
+
else:
|
|
195
|
+
print(f"\n {yellow('Could not locate the mixin file for')} {matched_stem}.")
|
|
196
|
+
user_path = input(f" Enter the mixin file path: ").strip()
|
|
197
|
+
if user_path:
|
|
198
|
+
mixin_workflow.run(user_path, config=config)
|
|
199
|
+
else:
|
|
200
|
+
print(f" {yellow('No matching mixin found for')} '{choice}'.")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# =============================================================================
|
|
204
|
+
# Interactive Menu
|
|
205
|
+
# =============================================================================
|
|
206
|
+
|
|
207
|
+
def interactive_menu(config: MigrationConfig):
|
|
208
|
+
"""Show the main interactive menu when no arguments are provided."""
|
|
209
|
+
|
|
210
|
+
print()
|
|
211
|
+
print(f" {bold('Vue Mixin Migration Tool')}")
|
|
212
|
+
print(f" {dim('Helps you migrate Vue 2 mixins to Vue 3 composables.')}")
|
|
213
|
+
print()
|
|
214
|
+
print(f" This tool supports two migration workflows:\n")
|
|
215
|
+
print(f" {bold('1.')} {green('Scan project')}")
|
|
216
|
+
print(f" Get an overview of all components still using mixins,")
|
|
217
|
+
print(f" see which composables already exist, and pick a component")
|
|
218
|
+
print(f" or mixin to work on next.\n")
|
|
219
|
+
print(f" {bold('2.')} {green('Migrate a component')}")
|
|
220
|
+
print(f" Given a single .vue file, find all its mixins, match them")
|
|
221
|
+
print(f" to composables, check coverage, and inject setup() for")
|
|
222
|
+
print(f" every mixin that has a ready composable.\n")
|
|
223
|
+
print(f" {bold('3.')} {green('Audit a mixin')}")
|
|
224
|
+
print(f" Given a mixin file, find every component that uses it,")
|
|
225
|
+
print(f" show which members each component relies on, and compare")
|
|
226
|
+
print(f" against a composable to see what's covered.\n")
|
|
227
|
+
print(f" {bold('q.')} Quit\n")
|
|
228
|
+
|
|
229
|
+
choice = input(f" Choose (1/2/3/q): ").strip()
|
|
230
|
+
print()
|
|
231
|
+
|
|
232
|
+
if choice == "1":
|
|
233
|
+
scan_project(config.project_root, config)
|
|
234
|
+
|
|
235
|
+
elif choice == "2":
|
|
236
|
+
print(f" {bold('Migrate a component')}")
|
|
237
|
+
print(f" {dim('Enter the path to a .vue file relative to the project root.')}\n")
|
|
238
|
+
path = input(f" Component path: ").strip()
|
|
239
|
+
if path:
|
|
240
|
+
component_workflow.run(path, config)
|
|
241
|
+
else:
|
|
242
|
+
print(f" {yellow('No path provided.')}")
|
|
243
|
+
|
|
244
|
+
elif choice == "3":
|
|
245
|
+
print(f" {bold('Audit a mixin')}")
|
|
246
|
+
print(f" {dim('Enter the path to a mixin .js/.ts file.')}")
|
|
247
|
+
print(f" {dim('Optionally, also provide a composable path to compare against.')}\n")
|
|
248
|
+
mixin_path = input(f" Mixin path: ").strip()
|
|
249
|
+
if not mixin_path:
|
|
250
|
+
print(f" {yellow('No path provided.')}")
|
|
251
|
+
return
|
|
252
|
+
composable_path = input(f" Composable path (optional, press Enter to skip): ").strip()
|
|
253
|
+
mixin_workflow.run(mixin_path, composable_path or None, config)
|
|
254
|
+
|
|
255
|
+
elif choice.lower() == "q":
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
else:
|
|
259
|
+
print(f" {yellow('Invalid choice.')}")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# =============================================================================
|
|
263
|
+
# CLI Entry Point
|
|
264
|
+
# =============================================================================
|
|
265
|
+
|
|
266
|
+
def main():
|
|
267
|
+
config = MigrationConfig()
|
|
268
|
+
|
|
269
|
+
# No arguments -> interactive menu
|
|
270
|
+
if len(sys.argv) < 2:
|
|
271
|
+
interactive_menu(config)
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
command = sys.argv[1].lower()
|
|
275
|
+
|
|
276
|
+
if command == "scan":
|
|
277
|
+
scan_project(config.project_root, config)
|
|
278
|
+
|
|
279
|
+
elif command == "component":
|
|
280
|
+
if len(sys.argv) < 3:
|
|
281
|
+
print(f"\n {bold('Migrate a component')}")
|
|
282
|
+
print(f" {dim('Finds all mixins in a component, matches them to composables,')}")
|
|
283
|
+
print(f" {dim('and injects setup() for those with full coverage.')}\n")
|
|
284
|
+
print(f" Usage: python -m vue3_migration component <path/to/Component.vue>\n")
|
|
285
|
+
return
|
|
286
|
+
component_workflow.run(sys.argv[2], config)
|
|
287
|
+
|
|
288
|
+
elif command == "audit":
|
|
289
|
+
if len(sys.argv) < 3:
|
|
290
|
+
print(f"\n {bold('Audit a mixin')}")
|
|
291
|
+
print(f" {dim('Finds every component using a mixin, shows member usage,')}")
|
|
292
|
+
print(f" {dim('and compares against a composable if provided.')}\n")
|
|
293
|
+
print(f" Usage: python -m vue3_migration audit <mixin_path> [composable_path]\n")
|
|
294
|
+
return
|
|
295
|
+
composable_arg = sys.argv[3] if len(sys.argv) > 3 else None
|
|
296
|
+
mixin_workflow.run(sys.argv[2], composable_arg, config)
|
|
297
|
+
|
|
298
|
+
elif command in ("help", "--help", "-h"):
|
|
299
|
+
print(f"""
|
|
300
|
+
{bold('Vue Mixin Migration Tool')}
|
|
301
|
+
|
|
302
|
+
{bold('Usage:')}
|
|
303
|
+
python -m vue3_migration Interactive menu
|
|
304
|
+
python -m vue3_migration scan Scan project for migration status
|
|
305
|
+
python -m vue3_migration component <path.vue> Migrate one component
|
|
306
|
+
python -m vue3_migration audit <mixin> [composable] Audit one mixin
|
|
307
|
+
|
|
308
|
+
{bold('Workflow:')}
|
|
309
|
+
1. Run {green('scan')} to see which components still use mixins
|
|
310
|
+
and which composables are already available.
|
|
311
|
+
|
|
312
|
+
2. Pick a component and run {green('component')} to migrate it.
|
|
313
|
+
The tool will match each mixin to a composable, report what's
|
|
314
|
+
missing, and inject setup() for every ready composable.
|
|
315
|
+
|
|
316
|
+
3. If you want to focus on a single mixin across the codebase,
|
|
317
|
+
run {green('audit')} to see every component that depends on it
|
|
318
|
+
and what members they use.
|
|
319
|
+
""")
|
|
320
|
+
|
|
321
|
+
else:
|
|
322
|
+
print(f"\n {yellow('Unknown command')}: {command}")
|
|
323
|
+
print(f" Run {bold('python -m vue3_migration help')} to see available commands.\n")
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Component analysis — parse imports, mixins arrays, member usage, and
|
|
3
|
+
component-defined members from Vue component source code.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from .js_parser import extract_brace_block, extract_property_names
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parse_imports(component_source: str) -> dict[str, str]:
|
|
12
|
+
"""Parse import statements. Returns { local_name: import_path }."""
|
|
13
|
+
imports = {}
|
|
14
|
+
for match in re.finditer(r"""import\s+(\w+)\s+from\s+['"]([^'"]+)['"]""", component_source):
|
|
15
|
+
imports[match.group(1)] = match.group(2)
|
|
16
|
+
return imports
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_mixins_array(component_source: str) -> list[str]:
|
|
20
|
+
"""Extract the local variable names from `mixins: [A, B, C]`."""
|
|
21
|
+
match = re.search(r"mixins\s*:\s*\[([^\]]*)\]", component_source)
|
|
22
|
+
if not match:
|
|
23
|
+
return []
|
|
24
|
+
return [name.strip() for name in match.group(1).split(",") if name.strip()]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def find_used_members(component_source: str, member_names: list[str]) -> list[str]:
|
|
28
|
+
"""Find which mixin members are referenced in the component (script + template).
|
|
29
|
+
|
|
30
|
+
For .vue files, searches within <script> and <template> sections.
|
|
31
|
+
Uses word-boundary regex to avoid partial matches.
|
|
32
|
+
"""
|
|
33
|
+
sections = []
|
|
34
|
+
for tag in ("script", "template"):
|
|
35
|
+
tag_match = re.search(rf"<{tag}[^>]*>(.*?)</{tag}>", component_source, re.DOTALL)
|
|
36
|
+
if tag_match:
|
|
37
|
+
sections.append(tag_match.group(1))
|
|
38
|
+
search_text = "\n".join(sections) if sections else component_source
|
|
39
|
+
|
|
40
|
+
return [
|
|
41
|
+
member for member in member_names
|
|
42
|
+
if re.search(rf"(?<!\w){re.escape(member)}(?!\w)", search_text)
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def extract_own_members(component_source: str) -> set[str]:
|
|
47
|
+
"""Extract member names defined in the component's OWN data/computed/methods/watch.
|
|
48
|
+
|
|
49
|
+
These are members the component defines itself (not inherited from mixins).
|
|
50
|
+
When a component overrides a mixin member, the component's version takes
|
|
51
|
+
precedence, so the composable doesn't need to provide it.
|
|
52
|
+
"""
|
|
53
|
+
own_members: set[str] = set()
|
|
54
|
+
|
|
55
|
+
# Extract just the <script> section for .vue files
|
|
56
|
+
script_match = re.search(r"<script[^>]*>(.*?)</script>", component_source, re.DOTALL)
|
|
57
|
+
source = script_match.group(1) if script_match else component_source
|
|
58
|
+
|
|
59
|
+
# data() { return { ... } }
|
|
60
|
+
data_match = re.search(r"\bdata\s*\(\s*\)\s*\{", source)
|
|
61
|
+
if data_match:
|
|
62
|
+
body = source[data_match.end():]
|
|
63
|
+
ret = re.search(r"\breturn\s*\{", body)
|
|
64
|
+
if ret:
|
|
65
|
+
brace_pos = ret.start() + ret.group().index("{")
|
|
66
|
+
own_members.update(extract_property_names(extract_brace_block(body, brace_pos)))
|
|
67
|
+
|
|
68
|
+
# computed, methods, watch sections
|
|
69
|
+
for section in ("computed", "methods", "watch"):
|
|
70
|
+
match = re.search(rf"\b{section}\s*:\s*\{{", source)
|
|
71
|
+
if match:
|
|
72
|
+
own_members.update(
|
|
73
|
+
extract_property_names(extract_brace_block(source, match.end() - 1))
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return own_members
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Composable analysis — extract identifiers, return keys, and function names
|
|
3
|
+
from Vue 3 composable source files.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from .js_parser import extract_brace_block
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def extract_all_identifiers(source: str) -> list[str]:
|
|
13
|
+
"""Extract ALL identifiers defined anywhere in a composable file.
|
|
14
|
+
|
|
15
|
+
Covers: variable declarations, function declarations, destructuring,
|
|
16
|
+
and return statement keys.
|
|
17
|
+
"""
|
|
18
|
+
ids: set[str] = set()
|
|
19
|
+
|
|
20
|
+
# const/let/var name = ...
|
|
21
|
+
ids.update(re.findall(r"\b(?:const|let|var)\s+(\w+)", source))
|
|
22
|
+
|
|
23
|
+
# function name(...)
|
|
24
|
+
ids.update(re.findall(r"\bfunction\s+(\w+)", source))
|
|
25
|
+
|
|
26
|
+
# Destructured: const { a, b: renamed, c = default } = ...
|
|
27
|
+
for match in re.finditer(r"\b(?:const|let|var)\s*\{([^}]+)\}", source):
|
|
28
|
+
for part in match.group(1).split(","):
|
|
29
|
+
part = part.strip()
|
|
30
|
+
if not part:
|
|
31
|
+
continue
|
|
32
|
+
if ":" in part:
|
|
33
|
+
ids.add(part.split(":")[1].strip().split("=")[0].strip())
|
|
34
|
+
else:
|
|
35
|
+
ids.add(part.split("=")[0].strip())
|
|
36
|
+
|
|
37
|
+
# Return keys: return { foo, bar, baz: val }
|
|
38
|
+
ret = re.search(r"\breturn\s*\{", source)
|
|
39
|
+
if ret:
|
|
40
|
+
block = extract_brace_block(source, ret.end() - 1)
|
|
41
|
+
ids.update(re.findall(r"\b(\w+)\s*[,}\n:]", block))
|
|
42
|
+
|
|
43
|
+
noise = {
|
|
44
|
+
"const", "let", "var", "function", "return", "if", "else", "new",
|
|
45
|
+
"true", "false", "null", "undefined", "async", "await", "from", "import",
|
|
46
|
+
}
|
|
47
|
+
return sorted(ids - noise)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def extract_return_keys(source: str) -> list[str]:
|
|
51
|
+
"""Extract ONLY the keys from the composable's return { ... } statement.
|
|
52
|
+
|
|
53
|
+
Members must be in the return statement to be accessible by the component.
|
|
54
|
+
"""
|
|
55
|
+
ret = re.search(r"\breturn\s*\{", source)
|
|
56
|
+
if not ret:
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
block = extract_brace_block(source, ret.end() - 1)
|
|
60
|
+
keys = re.findall(r"\b(\w+)\b", block)
|
|
61
|
+
|
|
62
|
+
noise = {
|
|
63
|
+
"const", "let", "var", "function", "return", "true", "false",
|
|
64
|
+
"null", "undefined", "new", "value",
|
|
65
|
+
}
|
|
66
|
+
return list(dict.fromkeys(k for k in keys if k not in noise))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def extract_function_name(source: str) -> Optional[str]:
|
|
70
|
+
"""Find the exported function name (e.g. useSelection) in a composable.
|
|
71
|
+
|
|
72
|
+
Checks for:
|
|
73
|
+
- export function useName(
|
|
74
|
+
- export default function useName(
|
|
75
|
+
- export const useName =
|
|
76
|
+
- export default const useName =
|
|
77
|
+
"""
|
|
78
|
+
# export function useName( / export default function useName(
|
|
79
|
+
match = re.search(r"\bexport\s+(?:default\s+)?function\s+(\w+)", source)
|
|
80
|
+
if match:
|
|
81
|
+
return match.group(1)
|
|
82
|
+
# export const useName = (
|
|
83
|
+
match = re.search(r"\bexport\s+(?:default\s+)?const\s+(\w+)\s*=", source)
|
|
84
|
+
if match:
|
|
85
|
+
return match.group(1)
|
|
86
|
+
return None
|