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.
Files changed (49) hide show
  1. package/README.md +195 -31
  2. package/package.json +4 -3
  3. package/vue3_migration/cli.py +9 -3
  4. package/vue3_migration/core/composable_search.py +76 -24
  5. package/vue3_migration/reporting/markdown.py +1 -1
  6. package/vue3_migration/workflows/auto_migrate_workflow.py +6 -4
  7. package/vue3_migration/workflows/mixin_workflow.py +1 -1
  8. package/vue3_migration/__pycache__/__init__.cpython-312.pyc +0 -0
  9. package/vue3_migration/__pycache__/__init__.cpython-313.pyc +0 -0
  10. package/vue3_migration/__pycache__/__main__.cpython-312.pyc +0 -0
  11. package/vue3_migration/__pycache__/cli.cpython-312.pyc +0 -0
  12. package/vue3_migration/__pycache__/models.cpython-312.pyc +0 -0
  13. package/vue3_migration/__pycache__/models.cpython-313.pyc +0 -0
  14. package/vue3_migration/core/__pycache__/__init__.cpython-312.pyc +0 -0
  15. package/vue3_migration/core/__pycache__/__init__.cpython-313.pyc +0 -0
  16. package/vue3_migration/core/__pycache__/component_analyzer.cpython-312.pyc +0 -0
  17. package/vue3_migration/core/__pycache__/component_analyzer.cpython-313.pyc +0 -0
  18. package/vue3_migration/core/__pycache__/composable_analyzer.cpython-312.pyc +0 -0
  19. package/vue3_migration/core/__pycache__/composable_analyzer.cpython-313.pyc +0 -0
  20. package/vue3_migration/core/__pycache__/composable_search.cpython-312.pyc +0 -0
  21. package/vue3_migration/core/__pycache__/composable_search.cpython-313.pyc +0 -0
  22. package/vue3_migration/core/__pycache__/file_resolver.cpython-312.pyc +0 -0
  23. package/vue3_migration/core/__pycache__/file_resolver.cpython-313.pyc +0 -0
  24. package/vue3_migration/core/__pycache__/js_parser.cpython-312.pyc +0 -0
  25. package/vue3_migration/core/__pycache__/js_parser.cpython-313.pyc +0 -0
  26. package/vue3_migration/core/__pycache__/mixin_analyzer.cpython-312.pyc +0 -0
  27. package/vue3_migration/core/__pycache__/mixin_analyzer.cpython-313.pyc +0 -0
  28. package/vue3_migration/core/__pycache__/warning_collector.cpython-312.pyc +0 -0
  29. package/vue3_migration/reporting/__pycache__/__init__.cpython-312.pyc +0 -0
  30. package/vue3_migration/reporting/__pycache__/__init__.cpython-313.pyc +0 -0
  31. package/vue3_migration/reporting/__pycache__/diff.cpython-312.pyc +0 -0
  32. package/vue3_migration/reporting/__pycache__/markdown.cpython-312.pyc +0 -0
  33. package/vue3_migration/reporting/__pycache__/markdown.cpython-313.pyc +0 -0
  34. package/vue3_migration/reporting/__pycache__/terminal.cpython-312.pyc +0 -0
  35. package/vue3_migration/reporting/__pycache__/terminal.cpython-313.pyc +0 -0
  36. package/vue3_migration/transform/__pycache__/__init__.cpython-312.pyc +0 -0
  37. package/vue3_migration/transform/__pycache__/__init__.cpython-313.pyc +0 -0
  38. package/vue3_migration/transform/__pycache__/composable_generator.cpython-312.pyc +0 -0
  39. package/vue3_migration/transform/__pycache__/composable_patcher.cpython-312.pyc +0 -0
  40. package/vue3_migration/transform/__pycache__/injector.cpython-312.pyc +0 -0
  41. package/vue3_migration/transform/__pycache__/injector.cpython-313.pyc +0 -0
  42. package/vue3_migration/transform/__pycache__/lifecycle_converter.cpython-312.pyc +0 -0
  43. package/vue3_migration/transform/__pycache__/this_rewriter.cpython-312.pyc +0 -0
  44. package/vue3_migration/workflows/__pycache__/__init__.cpython-312.pyc +0 -0
  45. package/vue3_migration/workflows/__pycache__/__init__.cpython-313.pyc +0 -0
  46. package/vue3_migration/workflows/__pycache__/auto_migrate_workflow.cpython-312.pyc +0 -0
  47. package/vue3_migration/workflows/__pycache__/component_workflow.cpython-312.pyc +0 -0
  48. package/vue3_migration/workflows/__pycache__/component_workflow.cpython-313.pyc +0 -0
  49. 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
- A CLI tool that migrates Vue 2 mixins to Vue 3 composables.
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
- ## Requirements
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
- - Node.js >= 14
8
- - Python >= 3.9
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
- ## Installation
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
- ## Usage
24
+ Requires Node.js >= 14 and Python >= 3.9 (used internally for AST analysis).
17
25
 
18
- Run from the root of your Vue project:
26
+ ## Quick Start
19
27
 
20
28
  ```bash
21
29
  npx vue3-migration
22
30
  ```
23
31
 
24
- This opens an interactive menu with four options:
32
+ That's it. An interactive menu walks you through four options:
25
33
 
26
- | # | Option | Description |
27
- |---|--------|-------------|
28
- | 1 | **Full project** | Migrate every component at once. Auto-patches and generates composables as needed. Shows a per-file change summary and requires confirmation before writing. |
29
- | 2 | **Pick a component** | Choose one component from a list. Migrate only that file. Safe for large projects — low blast radius, easy to test and review. |
30
- | 3 | **Pick a mixin** | Fully retire one mixin. Patches/generates its composable and updates every component that uses it. |
31
- | 4 | **Project status** | Read-only. Generates a detailed markdown report of what's migrated, what's ready, and what's blocked. No files are changed. |
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 commands
41
+ ### Direct Commands
34
42
 
35
43
  ```bash
36
- npx vue3-migration all # Migrate entire project
37
- npx vue3-migration component src/components/Foo.vue # Migrate one component
38
- npx vue3-migration mixin authMixin # Retire one mixin
39
- npx vue3-migration status # Generate status report
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
- ### Output files
75
+ It generates:
43
76
 
44
- Every migration writes a `migration-diff-<timestamp>.md` with a full before/after diff of every changed file.
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
- `npx vue3-migration status` writes a `migration-status-<timestamp>.md` with:
47
- - Summary counts (total, ready, blocked)
48
- - Mixin overview table
49
- - Per-component status and blocking reason
141
+ ### Handles `this.$` patterns
142
+
143
+ - `this.$nextTick(cb)` &rarr; `nextTick(cb)` (auto-imports from `'vue'`)
144
+ - `this.$set(obj, key, val)` &rarr; `obj[key] = val`
145
+ - `this.$delete(obj, key)` &rarr; `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`** &rarr; "Use `defineEmits` or pass `emit` from `setup()`"
153
+ - **`this.$router` / `this.$route`** &rarr; "Import `useRouter()` / `useRoute()` from `vue-router`"
154
+ - **`this.$store`** &rarr; "Import store directly from Pinia/Vuex"
155
+ - **`this.$refs`** &rarr; "Use template refs with `ref()`"
156
+ - **Mixin `props`, `inject`, `provide`** &rarr; "Must use `defineProps()` / `inject()` manually"
157
+ - **Nested mixins** &rarr; "Transitive members may be missed"
158
+ - **`filters`** &rarr; "Removed in Vue 3 — convert to methods"
159
+ - **This-aliasing** (`const self = this`) &rarr; "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
- ## Workflow for large projects
217
+ - **Node.js** >= 14 (for the CLI wrapper)
218
+ - **Python** >= 3.9 (for the migration engine — auto-detected on your PATH)
52
219
 
53
- For large codebases where each change is critical:
220
+ ## License
54
221
 
55
- 1. Run `npx vue3-migration status` to see the full picture.
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.2",
4
- "description": "Vue 2 mixin to Vue 3 composable migration tool",
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"
@@ -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
- config = MigrationConfig()
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
- def search_for_composable(mixin_stem: str, composable_dirs: list[Path]) -> list[Path]:
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
- # Phase 1: Exact name match (case-insensitive)
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
- filepath = Path(dirpath) / filename
70
- if filepath.suffix not in (".js", ".ts"):
71
- continue
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 list(dict.fromkeys(matches))
123
+ return matches
77
124
 
78
- # Phase 2: Fuzzy fallback -- "use" prefix + core word substring
79
- core_word = re.sub(r"[_-]?[Mm]ixin$", "", mixin_stem).lower()
80
- if not core_word:
81
- return []
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
- for comp_dir in composable_dirs:
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(composable_dirs: list[Path]) -> set[str]:
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 not composable_dirs:
208
- return []
209
- target_dir = composable_dirs[0]
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]