vue3-migration 1.0.2 → 1.1.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/README.md +195 -31
- package/package.json +4 -3
- package/vue3_migration/cli.py +9 -3
- package/vue3_migration/core/composable_search.py +76 -24
- package/vue3_migration/reporting/markdown.py +1 -1
- package/vue3_migration/workflows/auto_migrate_workflow.py +6 -4
- package/vue3_migration/workflows/mixin_workflow.py +1 -1
- 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/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/__pycache__/warning_collector.cpython-312.pyc +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__/diff.cpython-312.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/transform/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vue3_migration/transform/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vue3_migration/transform/__pycache__/composable_generator.cpython-312.pyc +0 -0
- package/vue3_migration/transform/__pycache__/composable_patcher.cpython-312.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/__pycache__/lifecycle_converter.cpython-312.pyc +0 -0
- package/vue3_migration/transform/__pycache__/this_rewriter.cpython-312.pyc +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__/auto_migrate_workflow.cpython-312.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/README.md
CHANGED
|
@@ -1,58 +1,222 @@
|
|
|
1
1
|
# vue3-migration
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Stop rewriting mixins by hand.** Automatically migrate your Vue 2 mixins to Vue 3 composables — data, computed, methods, watchers, lifecycle hooks, and all.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
One command. Full before/after diffs. No files changed until you say yes.
|
|
6
|
+
|
|
7
|
+
## The Problem
|
|
8
|
+
|
|
9
|
+
Every Vue 2 project sitting on mixins is a migration bottleneck. Rewriting them by hand means:
|
|
6
10
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
11
|
+
- Reading every mixin, understanding every member, tracing every `this.` reference
|
|
12
|
+
- Creating composable files, converting `this.x` to `x.value`, rewriting lifecycle hooks
|
|
13
|
+
- Updating every component that imports the mixin — removing imports, adding `setup()`, destructuring return values
|
|
14
|
+
- Doing it again for the next mixin. And the next. Across dozens (or hundreds) of components.
|
|
9
15
|
|
|
10
|
-
|
|
16
|
+
**vue3-migration does all of this automatically.** It reads your mixins, generates (or patches) composables, rewrites your components, and shows you a clean diff before touching a single file.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
11
19
|
|
|
12
20
|
```bash
|
|
13
21
|
npm install -D vue3-migration
|
|
14
22
|
```
|
|
15
23
|
|
|
16
|
-
|
|
24
|
+
Requires Node.js >= 14 and Python >= 3.9 (used internally for AST analysis).
|
|
17
25
|
|
|
18
|
-
|
|
26
|
+
## Quick Start
|
|
19
27
|
|
|
20
28
|
```bash
|
|
21
29
|
npx vue3-migration
|
|
22
30
|
```
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
That's it. An interactive menu walks you through four options:
|
|
25
33
|
|
|
26
|
-
| # |
|
|
27
|
-
|
|
28
|
-
| 1 | **Full project** |
|
|
29
|
-
| 2 | **Pick a component** | Choose one component from a list. Migrate
|
|
30
|
-
| 3 | **Pick a mixin** |
|
|
31
|
-
| 4 | **Project status** | Read-only. Generates a detailed markdown report
|
|
34
|
+
| # | Mode | What it does |
|
|
35
|
+
|---|------|-------------|
|
|
36
|
+
| **1** | **Full project** | Migrates every component at once. Generates and patches composables, injects `setup()`, removes mixin imports. Shows a full change summary before writing. |
|
|
37
|
+
| **2** | **Pick a component** | Choose one component from a list. Migrate just that file. Low blast radius — perfect for large codebases. |
|
|
38
|
+
| **3** | **Pick a mixin** | Choose one mixin to fully retire. Updates the composable and every component that uses it. |
|
|
39
|
+
| **4** | **Project status** | Read-only scan. Generates a detailed markdown report: what's migrated, what's ready, what's blocked and why. |
|
|
32
40
|
|
|
33
|
-
### Direct
|
|
41
|
+
### Direct Commands
|
|
34
42
|
|
|
35
43
|
```bash
|
|
36
|
-
npx vue3-migration all #
|
|
37
|
-
npx vue3-migration component src/components/Foo.vue #
|
|
38
|
-
npx vue3-migration mixin authMixin # Retire one mixin
|
|
39
|
-
npx vue3-migration status #
|
|
44
|
+
npx vue3-migration all # Full project migration
|
|
45
|
+
npx vue3-migration component src/components/Foo.vue # One component
|
|
46
|
+
npx vue3-migration mixin authMixin # Retire one mixin everywhere
|
|
47
|
+
npx vue3-migration status # Status report only
|
|
48
|
+
npx vue3-migration --root /path/to/project all # Run from outside project root
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## What It Actually Does
|
|
52
|
+
|
|
53
|
+
### Generates composables from scratch
|
|
54
|
+
|
|
55
|
+
No existing composable? The tool creates one. Given a mixin like:
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
// mixins/authMixin.js
|
|
59
|
+
export default {
|
|
60
|
+
data() {
|
|
61
|
+
return { user: null, token: '' }
|
|
62
|
+
},
|
|
63
|
+
computed: {
|
|
64
|
+
isLoggedIn() { return !!this.user }
|
|
65
|
+
},
|
|
66
|
+
methods: {
|
|
67
|
+
async login(credentials) { /* ... */ }
|
|
68
|
+
},
|
|
69
|
+
mounted() {
|
|
70
|
+
this.checkSession()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
40
73
|
```
|
|
41
74
|
|
|
42
|
-
|
|
75
|
+
It generates:
|
|
43
76
|
|
|
44
|
-
|
|
77
|
+
```js
|
|
78
|
+
// composables/useAuth.js
|
|
79
|
+
import { ref, computed, onMounted } from 'vue'
|
|
80
|
+
|
|
81
|
+
export function useAuth() {
|
|
82
|
+
const user = ref(null)
|
|
83
|
+
const token = ref('')
|
|
84
|
+
|
|
85
|
+
const isLoggedIn = computed(() => !!user.value)
|
|
86
|
+
|
|
87
|
+
async function login(credentials) { /* ... */ }
|
|
88
|
+
|
|
89
|
+
onMounted(() => {
|
|
90
|
+
checkSession()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
return { user, token, isLoggedIn, login }
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Every `this.x` reference is rewritten. Lifecycle hooks are converted. Getter/setter computed properties are handled. Watch expressions with `deep`, `immediate`, and handler options are converted.
|
|
98
|
+
|
|
99
|
+
### Patches incomplete composables
|
|
100
|
+
|
|
101
|
+
Already started writing composables manually? The tool detects what's missing and patches it — adds missing member declarations and updates the `return` statement. No duplicate work.
|
|
102
|
+
|
|
103
|
+
### Rewrites components automatically
|
|
104
|
+
|
|
105
|
+
For every component using a mixin, the tool:
|
|
106
|
+
|
|
107
|
+
1. **Removes** the mixin import line
|
|
108
|
+
2. **Adds** the composable import
|
|
109
|
+
3. **Removes** the mixin from the `mixins: [...]` array
|
|
110
|
+
4. **Injects** a `setup()` function with destructured composable calls
|
|
111
|
+
5. **Converts** lifecycle hooks to their Composition API equivalents
|
|
112
|
+
6. **Merges** into existing `setup()` if one already exists
|
|
113
|
+
|
|
114
|
+
**Before:**
|
|
115
|
+
```vue
|
|
116
|
+
<script>
|
|
117
|
+
import authMixin from '@/mixins/authMixin'
|
|
118
|
+
|
|
119
|
+
export default {
|
|
120
|
+
mixins: [authMixin],
|
|
121
|
+
data() { return { localState: true } }
|
|
122
|
+
}
|
|
123
|
+
</script>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**After:**
|
|
127
|
+
```vue
|
|
128
|
+
<script>
|
|
129
|
+
import { useAuth } from '@/composables/useAuth'
|
|
130
|
+
|
|
131
|
+
export default {
|
|
132
|
+
setup() {
|
|
133
|
+
const { user, token, isLoggedIn, login } = useAuth()
|
|
134
|
+
return { user, token, isLoggedIn, login }
|
|
135
|
+
},
|
|
136
|
+
data() { return { localState: true } }
|
|
137
|
+
}
|
|
138
|
+
</script>
|
|
139
|
+
```
|
|
45
140
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
141
|
+
### Handles `this.$` patterns
|
|
142
|
+
|
|
143
|
+
- `this.$nextTick(cb)` → `nextTick(cb)` (auto-imports from `'vue'`)
|
|
144
|
+
- `this.$set(obj, key, val)` → `obj[key] = val`
|
|
145
|
+
- `this.$delete(obj, key)` → `delete obj[key]`
|
|
146
|
+
- `this.$emit`, `this.$router`, `this.$store`, `this.$refs`, and 15+ more patterns are detected and flagged with actionable migration guidance
|
|
147
|
+
|
|
148
|
+
### Smart about what it can't automate
|
|
149
|
+
|
|
150
|
+
Some patterns need human judgment. The tool doesn't silently skip them — it flags them clearly:
|
|
151
|
+
|
|
152
|
+
- **`this.$emit`** → "Use `defineEmits` or pass `emit` from `setup()`"
|
|
153
|
+
- **`this.$router` / `this.$route`** → "Import `useRouter()` / `useRoute()` from `vue-router`"
|
|
154
|
+
- **`this.$store`** → "Import store directly from Pinia/Vuex"
|
|
155
|
+
- **`this.$refs`** → "Use template refs with `ref()`"
|
|
156
|
+
- **Mixin `props`, `inject`, `provide`** → "Must use `defineProps()` / `inject()` manually"
|
|
157
|
+
- **Nested mixins** → "Transitive members may be missed"
|
|
158
|
+
- **`filters`** → "Removed in Vue 3 — convert to methods"
|
|
159
|
+
- **This-aliasing** (`const self = this`) → "Manual replacement needed"
|
|
160
|
+
|
|
161
|
+
Each warning is injected as a comment directly in the generated code, so nothing gets lost.
|
|
162
|
+
|
|
163
|
+
## Safety Features
|
|
164
|
+
|
|
165
|
+
**Nothing changes until you confirm.** Every migration mode shows a complete change summary and asks for explicit `y/n` confirmation before writing any file.
|
|
166
|
+
|
|
167
|
+
**Full diff report.** Every migration writes a `migration-diff-<timestamp>.md` with unified diffs of every changed file — composables and components.
|
|
168
|
+
|
|
169
|
+
**Override-aware.** If a component defines its own `data`, `computed`, or `methods` that overlap with a mixin, the tool knows the component's version takes precedence and won't inject duplicates.
|
|
170
|
+
|
|
171
|
+
**Confidence scoring.** Generated composables include a confidence header:
|
|
172
|
+
- **HIGH** — Clean conversion, no remaining issues
|
|
173
|
+
- **MEDIUM** — Has TODOs or warnings that need review
|
|
174
|
+
- **LOW** — Remaining `this.` references or structural issues
|
|
175
|
+
|
|
176
|
+
**Blocked status.** If a mixin can't be safely migrated (e.g., missing composable, incomplete coverage), the tool marks it as blocked and tells you exactly why — it never partially migrates and leaves broken code.
|
|
177
|
+
|
|
178
|
+
## What It Supports
|
|
179
|
+
|
|
180
|
+
| Mixin feature | Auto-converted |
|
|
181
|
+
|--------------|---------------|
|
|
182
|
+
| `data()` properties | ref() with default values |
|
|
183
|
+
| `computed` (simple) | computed(() => ...) |
|
|
184
|
+
| `computed` (get/set) | computed({ get, set }) |
|
|
185
|
+
| `methods` (sync & async) | Plain functions |
|
|
186
|
+
| `watch` (handler + options) | watch() with deep/immediate |
|
|
187
|
+
| `mounted`, `created`, etc. | onMounted(), inlined, etc. |
|
|
188
|
+
| `beforeDestroy` / `destroyed` | onBeforeUnmount() / onUnmounted() |
|
|
189
|
+
| `this.x` references | x.value or x (context-aware) |
|
|
190
|
+
| `this.$nextTick` | nextTick() |
|
|
191
|
+
| `this.$set` / `this.$delete` | Direct assignment / delete |
|
|
192
|
+
| Multiple mixins per component | Multiple composable calls |
|
|
193
|
+
| Existing `setup()` function | Merges (doesn't overwrite) |
|
|
194
|
+
| `@/` and relative imports | Resolved and rewritten |
|
|
195
|
+
|
|
196
|
+
## Recommended Workflow for Large Projects
|
|
197
|
+
|
|
198
|
+
1. **Run `npx vue3-migration status`** to see the full picture — which mixins are used where, what's ready, what's blocked.
|
|
199
|
+
2. **Start with "Pick a component"** (option 2). Migrate one component, test it, commit.
|
|
200
|
+
3. **When a mixin is fully covered**, use "Pick a mixin" (option 3) to retire it across all components at once.
|
|
201
|
+
4. **Repeat** until the status report shows zero remaining mixins.
|
|
202
|
+
|
|
203
|
+
For smaller projects, "Full project" (option 1) handles everything in one pass.
|
|
204
|
+
|
|
205
|
+
## How It Finds Your Files
|
|
206
|
+
|
|
207
|
+
The tool searches recursively from your project root (or `--root` if specified):
|
|
208
|
+
|
|
209
|
+
- **Components:** Every `.vue` file in the project tree
|
|
210
|
+
- **Mixin files:** Resolved from import paths (`@/`, relative, bare imports)
|
|
211
|
+
- **Composables:** First checks directories named `composables/` (case-insensitive), then falls back to searching the entire project for any `use*.js` / `use*.ts` file
|
|
212
|
+
|
|
213
|
+
Skips `node_modules/`, `dist/`, `.git/`, and `__pycache__/` automatically.
|
|
214
|
+
|
|
215
|
+
## Requirements
|
|
50
216
|
|
|
51
|
-
|
|
217
|
+
- **Node.js** >= 14 (for the CLI wrapper)
|
|
218
|
+
- **Python** >= 3.9 (for the migration engine — auto-detected on your PATH)
|
|
52
219
|
|
|
53
|
-
|
|
220
|
+
## License
|
|
54
221
|
|
|
55
|
-
|
|
56
|
-
2. Use **Pick a component** (option 2) to migrate one component at a time.
|
|
57
|
-
3. Test after each migration, then move on to the next.
|
|
58
|
-
4. When a mixin is fully covered and you're ready to retire it, use **Pick a mixin** (option 3).
|
|
222
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vue3-migration",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Vue 2
|
|
3
|
+
"version": "1.1.0",
|
|
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"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"bin/",
|
|
10
|
-
"vue3_migration
|
|
10
|
+
"vue3_migration/**/*.py",
|
|
11
|
+
"!**/__pycache__/"
|
|
11
12
|
],
|
|
12
13
|
"engines": {
|
|
13
14
|
"node": ">=14"
|
package/vue3_migration/cli.py
CHANGED
|
@@ -173,7 +173,7 @@ def _scan_components_with_mixins(project_root: Path, config: MigrationConfig) ->
|
|
|
173
173
|
from .core.composable_search import collect_composable_stems, find_composable_dirs, mixin_has_composable
|
|
174
174
|
|
|
175
175
|
composable_dirs = find_composable_dirs(project_root)
|
|
176
|
-
composable_stems = collect_composable_stems(composable_dirs)
|
|
176
|
+
composable_stems = collect_composable_stems(composable_dirs, project_root=project_root)
|
|
177
177
|
results: list[dict] = []
|
|
178
178
|
|
|
179
179
|
for dirpath, _, filenames in os.walk(project_root):
|
|
@@ -216,7 +216,7 @@ def _scan_mixin_usage(project_root: Path, config: MigrationConfig) -> list[dict]
|
|
|
216
216
|
from .core.composable_search import collect_composable_stems, find_composable_dirs, mixin_has_composable
|
|
217
217
|
|
|
218
218
|
composable_dirs = find_composable_dirs(project_root)
|
|
219
|
-
composable_stems = collect_composable_stems(composable_dirs)
|
|
219
|
+
composable_stems = collect_composable_stems(composable_dirs, project_root=project_root)
|
|
220
220
|
mixin_counter: Counter[str] = Counter()
|
|
221
221
|
|
|
222
222
|
for dirpath, _, filenames in os.walk(project_root):
|
|
@@ -511,9 +511,15 @@ def project_status(config: MigrationConfig) -> None:
|
|
|
511
511
|
# =============================================================================
|
|
512
512
|
|
|
513
513
|
def main(argv: list[str] | None = None):
|
|
514
|
+
import argparse
|
|
514
515
|
import sys
|
|
515
516
|
args = argv if argv is not None else sys.argv[1:]
|
|
516
|
-
|
|
517
|
+
|
|
518
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
519
|
+
parser.add_argument("--root", default=None)
|
|
520
|
+
known, args = parser.parse_known_args(args)
|
|
521
|
+
project_root = Path(known.root).resolve() if known.root else Path.cwd()
|
|
522
|
+
config = MigrationConfig(project_root=project_root)
|
|
517
523
|
|
|
518
524
|
if not args:
|
|
519
525
|
interactive_menu(config)
|
|
@@ -53,57 +53,109 @@ def find_composable_dirs(project_root: Path) -> list[Path]:
|
|
|
53
53
|
return found
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
_SKIP_DIRS = {"node_modules", "dist", ".git", "__pycache__"}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def find_all_composable_files(project_root: Path) -> list[Path]:
|
|
60
|
+
"""Find every .js/.ts file whose stem starts with 'use' anywhere in the project.
|
|
61
|
+
|
|
62
|
+
Skips node_modules, dist, .git, __pycache__.
|
|
63
|
+
"""
|
|
64
|
+
files: list[Path] = []
|
|
65
|
+
for dirpath, dirnames, filenames in os.walk(project_root):
|
|
66
|
+
rel = Path(dirpath).relative_to(project_root)
|
|
67
|
+
if any(part in _SKIP_DIRS for part in rel.parts):
|
|
68
|
+
dirnames[:] = []
|
|
69
|
+
continue
|
|
70
|
+
# Prune skipped dirs in-place so os.walk won't descend into them
|
|
71
|
+
dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
|
|
72
|
+
for fn in filenames:
|
|
73
|
+
fp = Path(dirpath) / fn
|
|
74
|
+
if fp.suffix in (".js", ".ts") and fp.stem.lower().startswith("use"):
|
|
75
|
+
files.append(fp)
|
|
76
|
+
return files
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def search_for_composable(
|
|
80
|
+
mixin_stem: str,
|
|
81
|
+
composable_dirs: list[Path],
|
|
82
|
+
project_root: "Path | None" = None,
|
|
83
|
+
) -> list[Path]:
|
|
57
84
|
"""Search for composable files matching a mixin name.
|
|
58
85
|
|
|
59
86
|
Phase 1: Exact stem match against generated candidate names (case-insensitive).
|
|
60
87
|
Phase 2: Fuzzy -- any 'use' file whose name contains the mixin's core word.
|
|
88
|
+
|
|
89
|
+
If no matches found in composable_dirs and project_root is provided,
|
|
90
|
+
repeats both phases across all use*.js/ts files found project-wide.
|
|
61
91
|
"""
|
|
62
92
|
candidates = generate_candidates(mixin_stem)
|
|
63
93
|
matches = []
|
|
64
94
|
|
|
65
|
-
|
|
95
|
+
def _search_files(files: list[Path]) -> list[Path]:
|
|
96
|
+
found = []
|
|
97
|
+
# Phase 1: exact
|
|
98
|
+
for fp in files:
|
|
99
|
+
if any(fp.stem.lower() == c.lower() for c in candidates):
|
|
100
|
+
found.append(fp)
|
|
101
|
+
if found:
|
|
102
|
+
return list(dict.fromkeys(found))
|
|
103
|
+
# Phase 2: fuzzy
|
|
104
|
+
core_word = re.sub(r"[_-]?[Mm]ixin$", "", mixin_stem).lower()
|
|
105
|
+
if not core_word:
|
|
106
|
+
return []
|
|
107
|
+
for fp in files:
|
|
108
|
+
if fp.stem.lower().startswith("use") and core_word in fp.stem.lower():
|
|
109
|
+
found.append(fp)
|
|
110
|
+
return list(dict.fromkeys(found))
|
|
111
|
+
|
|
112
|
+
# Collect files from named composable directories
|
|
113
|
+
dir_files: list[Path] = []
|
|
66
114
|
for comp_dir in composable_dirs:
|
|
67
115
|
for dirpath, _, filenames in os.walk(comp_dir):
|
|
68
116
|
for filename in filenames:
|
|
69
|
-
|
|
70
|
-
if
|
|
71
|
-
|
|
72
|
-
if any(filepath.stem.lower() == c.lower() for c in candidates):
|
|
73
|
-
matches.append(filepath)
|
|
117
|
+
fp = Path(dirpath) / filename
|
|
118
|
+
if fp.suffix in (".js", ".ts"):
|
|
119
|
+
dir_files.append(fp)
|
|
74
120
|
|
|
121
|
+
matches = _search_files(dir_files)
|
|
75
122
|
if matches:
|
|
76
|
-
return
|
|
123
|
+
return matches
|
|
77
124
|
|
|
78
|
-
#
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
125
|
+
# Fallback: project-wide search
|
|
126
|
+
if project_root is not None:
|
|
127
|
+
all_files = find_all_composable_files(project_root)
|
|
128
|
+
# Exclude files already covered by composable_dirs to avoid duplicates
|
|
129
|
+
dir_file_set = set(dir_files)
|
|
130
|
+
extra_files = [f for f in all_files if f not in dir_file_set]
|
|
131
|
+
matches = _search_files(extra_files)
|
|
82
132
|
|
|
83
|
-
|
|
84
|
-
for dirpath, _, filenames in os.walk(comp_dir):
|
|
85
|
-
for filename in filenames:
|
|
86
|
-
filepath = Path(dirpath) / filename
|
|
87
|
-
if filepath.suffix not in (".js", ".ts"):
|
|
88
|
-
continue
|
|
89
|
-
if filepath.stem.lower().startswith("use") and core_word in filepath.stem.lower():
|
|
90
|
-
matches.append(filepath)
|
|
91
|
-
|
|
92
|
-
return list(dict.fromkeys(matches))
|
|
133
|
+
return matches
|
|
93
134
|
|
|
94
135
|
|
|
95
|
-
def collect_composable_stems(
|
|
136
|
+
def collect_composable_stems(
|
|
137
|
+
composable_dirs: list[Path],
|
|
138
|
+
project_root: "Path | None" = None,
|
|
139
|
+
) -> set[str]:
|
|
96
140
|
"""Collect all composable file stems (e.g. 'useSelection') from all dirs.
|
|
97
141
|
|
|
98
142
|
Used for quick existence checks during scanning.
|
|
143
|
+
|
|
144
|
+
If composable_dirs yields no stems and project_root is provided,
|
|
145
|
+
falls back to a project-wide search via find_all_composable_files.
|
|
99
146
|
"""
|
|
100
147
|
stems: set[str] = set()
|
|
101
148
|
for comp_dir in composable_dirs:
|
|
102
149
|
for dirpath, _, filenames in os.walk(comp_dir):
|
|
103
150
|
for fn in filenames:
|
|
104
151
|
fp = Path(dirpath) / fn
|
|
105
|
-
if fp.suffix in (".js", ".ts") and fp.stem.startswith("use"):
|
|
152
|
+
if fp.suffix in (".js", ".ts") and fp.stem.lower().startswith("use"):
|
|
106
153
|
stems.add(fp.stem.lower())
|
|
154
|
+
|
|
155
|
+
if not stems and project_root is not None:
|
|
156
|
+
for fp in find_all_composable_files(project_root):
|
|
157
|
+
stems.add(fp.stem.lower())
|
|
158
|
+
|
|
107
159
|
return stems
|
|
108
160
|
|
|
109
161
|
|
|
@@ -148,7 +148,7 @@ def generate_status_report(project_root: Path, config) -> str:
|
|
|
148
148
|
from ..core.file_resolver import resolve_mixin_stem
|
|
149
149
|
|
|
150
150
|
composable_dirs = find_composable_dirs(project_root)
|
|
151
|
-
composable_stems = collect_composable_stems(composable_dirs)
|
|
151
|
+
composable_stems = collect_composable_stems(composable_dirs, project_root=project_root)
|
|
152
152
|
|
|
153
153
|
# Detect composables that need manual migration (reactive() or variable return)
|
|
154
154
|
manual_stems: set[str] = set()
|
|
@@ -67,7 +67,7 @@ def _analyze_mixin_silent(
|
|
|
67
67
|
entry.compute_status()
|
|
68
68
|
return entry
|
|
69
69
|
|
|
70
|
-
matches = search_for_composable(mixin_file.stem, composable_dirs)
|
|
70
|
+
matches = search_for_composable(mixin_file.stem, composable_dirs, project_root=project_root)
|
|
71
71
|
composable_file = matches[0] if matches else None
|
|
72
72
|
|
|
73
73
|
if composable_file:
|
|
@@ -204,9 +204,11 @@ def plan_new_composables(
|
|
|
204
204
|
Returns FileChange objects with original_content="" (new files).
|
|
205
205
|
"""
|
|
206
206
|
composable_dirs = find_composable_dirs(project_root)
|
|
207
|
-
if
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
if composable_dirs:
|
|
208
|
+
target_dir = composable_dirs[0]
|
|
209
|
+
else:
|
|
210
|
+
src_dir = project_root / "src"
|
|
211
|
+
target_dir = (src_dir / "composables") if src_dir.is_dir() else (project_root / "composables")
|
|
210
212
|
|
|
211
213
|
seen_stems: set[str] = set()
|
|
212
214
|
changes = []
|
|
@@ -296,7 +296,7 @@ def run(mixin_arg: str, composable_arg: Optional[str] = None, config: MigrationC
|
|
|
296
296
|
candidates = generate_candidates(mixin_path.stem)
|
|
297
297
|
print(f" Looking for: {cyan(', '.join(candidates))}")
|
|
298
298
|
comp_dirs = find_composable_dirs(project_root)
|
|
299
|
-
matches = search_for_composable(mixin_path.stem, comp_dirs)
|
|
299
|
+
matches = search_for_composable(mixin_path.stem, comp_dirs, project_root=project_root)
|
|
300
300
|
|
|
301
301
|
if len(matches) == 1:
|
|
302
302
|
found_path = matches[0]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|