vue-refactor
The three most common "need-to-split" scenarios in Vue code: Fat trunk, UI entangled with IO/business logic, reactivity entangled with business logic. This skill acts as a coach — first diagnose the code, then select the corresponding recipe, and move step by step in a compiler-driven way. No test safeguards are written; behavioral equivalence is guaranteed by the compiler + step-by-step rollback.
Methodology sources: Arlo Belshee's "Provable Refactorings", Michael Feathers' "Lean on the Compiler", Michael Thiessen's Humble Component + Thin Composable. Detailed philosophy can be found in
reference/compiler-driven-principles.md
.
Core Disciplines (Four Rules to Follow Every Time)
- Behavioral equivalence is the bottom line. This action must not change any externally observable behavior — DOM output, event timing, network requests, store changes, route jumps must all be equivalent. If you find yourself wanting to "optimize" a behavior in passing, stop and handle it via the feature or issue process.
- The compiler must be green after each step. This includes , Volar type checking, ESLint, and existing unit tests. If any step fails to compile, roll back this step immediately — do not "leave it for later to fix together".
- One semantic unit per step. Move only one field, one method, or one template block at a time. Do not merge, package, or "take one along in passing".
- Create new → Replace references → Delete old. Always follow this three-step cycle. Do not rename in place (renaming will break all references at once); instead, first create the new location, then move references one by one, and finally delete the old location.
Violating any of these rules means reverting to "AI random refactoring" — the purpose of this skill is lost.
Pre-Checks First (Stop and Route if Hit)
Confirm four things before entering diagnosis; stop the skill and provide routing suggestions if any are hit, do not force it:
| Check | Route to when hit |
|---|
| User wants to change behavior at the same time (add features / fix bugs / adjust styles for effects) | Follow feature (cs-feat) or issue (cs-issue) process |
| Code size > 800 lines per single file or > 3 files need to be modified simultaneously | Advise the user to narrow the scope to one SFC / one composable first |
| Project is not configured with TypeScript + Volar, pure JS Vue project | Warning: Compiler-driven guarantees will be significantly weakened; changes rely on grep + manual full test runs
; can still proceed but risk level is raised |
| User says "You decide" without determining the scope of changes | Ask back: "Are we only splitting component X this time, or including its dependencies as well?" — enter diagnosis only after the scope is confirmed |
If the user does not provide a specific code path at all, first ask: "Which file are we splitting this time? Paste the path or code snippet."
Diagnosis Decision Tree
Run four checks after receiving the code. Use the corresponding recipe if matched; if multiple matches exist, prioritize in order (1 > 2 > 3).
Check 1: Is the trunk too fat? → Recipe A (Outward Move to Reveal Core)
Signals (any hit):
- exceeds 200 lines
- Cyclomatic complexity is visually > 10 (multi-layer nested if / switch / try)
- A component has > 5 ref/reactive + > 5 methods + > 3 watch/computed at the same time
- You can point out "these variables are a local theme" at a glance (e.g., all around "form validation" or "drag state")
→ Use
Recipe A: Move this cluster of variables and their methods to a new composable or util module. Details can be found in
reference/recipe-outward-move.md
.
Check 2: Is UI entangled with IO/business logic? → Recipe B (Humble/Controller Split)
Signals (any hit):
- A component has both large rendering and
fetch / axios / useQuery / store.dispatch
calls
- A component both directly converts props for display and handles
router.push / localStorage / window.xxx
- Testing requires mounting the entire component + stubbing the network to run — cannot test rendering alone
- There are "pure display candidates" among child components — a section can be clearly cut out from the template that only communicates via props and emit
→ Use
Recipe B: Split the pure display layer into Humble child components (props in / emit out, zero ref/IO), and keep the parent component as a Controller for scheduling. Details can be found in
reference/recipe-humble-controller.md
.
Check 3: Is reactivity entangled with business logic? → Recipe C (Thin Composable Purification)
Signals (any hit):
- Business calculations (discount calculation, validation, formatting, state conversion) are mixed with in a composable
- To unit test business calculations, you must or to run
- The same calculation logic is copied in multiple composables / components
- You can point out "these lines of code do not rely on any ref, they are just pure calculations" at a glance
→ Use
Recipe C: Extract business logic into pure functions placed in
, and leave only a thin reactivity shell in the composable (unwrap → call pure function → put back into ref). Details can be found in
reference/recipe-thin-composable.md
.
Check 4: All three are hit → Start with the outermost one
If all three signals are hit, the code is corrupted in three dimensions. Follow the order A → B → C:
- First use A to thin the trunk (reveal which parts are UI, which are IO, which are business logic)
- Then use B to separate the UI (clarify the Controller's responsibilities)
- Finally use C to purify business logic from reactivity
Only complete one round of one recipe at a time; stop after completion to let the user see the effect before deciding whether to proceed to the next recipe.
Common Skeleton for All Three Recipes (Follow This Rhythm Every Time)
Regardless of A/B/C, the execution rhythm is the same:
1. Locate a "semantic unit" (a cluster of variables / a template block / a section of pure logic)
2. Create the target location (empty file / empty composable / stub of empty child component), keep the compiler green
3. Move the first member over (usually a field / a prop / a function signature)
↓
The compiler will definitely throw errors: all references to it in the original location are broken
↓
**Treat these errors as a to-do list** — the compiler tells you all the call points
4. Fix errors one by one: either point the reference to the new location, or move the dependent methods over together
5. After all errors in this step are fixed → Commit / mark a rollback point
6. Return to step 1, move the next member
7. When the original location is empty or only has re-export / delegate, perform the final deletion
Key Insight: Every time you "move a member from A to B", you are not "correcting code"; you are letting the compiler tell you "where else this member is being used". Compilation errors are free static analysis — do not resist them, embrace them and use them as a checklist.
If during error fixing you encounter "this method depends on 3 other fields, and 2 of them haven't been moved yet", stop, roll back this step, move those 2 fields first, then come back. Never accumulate half-finished work across steps.
Collaboration Rhythm with Users
This is a coaching-type skill, not an automation tool:
- User provides code → You diagnose, explain the judgment result to the user ("I think Check 1 is hit, the trunk is too fat, recommend Recipe A; reason: …"), let the user confirm or correct the diagnosis
- After entering the recipe → First explain the "semantic unit division" to the user ("I plan to move this cluster of 3 refs + 2 methods out, named "), wait for the user's approval
- Start moving → Pause and report after each semantic unit is moved: "The first cluster has been moved, compiler is green, commit has been submitted. The next cluster is …, continue?"
- If you cannot fix a compilation error / find that moving will change behavior → Stop and report immediately, do not improvise
Do not complete all three recipes at once before reporting. Do not skip confirmation for any cluster. The meaning of coaching is to leave a decision opportunity for the user at every step.
Common Pitfalls
- Using renaming instead of moving: will replace all references at once, which seems error-free but loses the "compiler marks call points" checklist. Always create new → copy and move → modify references one by one → delete old.
- Moving an entire composable at once: It looks like "one semantic unit" but actually contains 10 refs. You cannot handle the compiler errors. Move field by field.
- Fixing type errors / adding type annotations during refactoring: Tempting ("fix it while compiling anyway"), but this changes the type contract, which equals changing behavior. Type tightening should be split into independent changes.
- Moving / into pure functions: Reactive APIs cannot appear in pure functions. Either keep them in the composable shell, or extract the watch callback into a pure function and call it in the watch.
- Secretly accessing store / route in Humble components: As long as it accesses pinia / vue-router, it is no longer humble. Roll back immediately if found; this state should be fetched in the Controller and passed down via props.
- Mixing recipes during execution: Doing A and splitting a Humble component by the way; doing C and thinning the trunk by the way. It seems efficient, but actually blurs the "exit signal" of each step and increases rollback cost. One recipe per round.
- Directly jumping from Vue 2 Options API to : That is migration, not refactoring; behavioral equivalence cannot be proven by the compiler (Options' binding differs from setup's closure semantics). Split into two independent tasks: "First refactor the fat trunk (still in Options) → Then migrate to setup".