Vue defineModel Enforcement
Enforces use of Vue 3's defineModel() macro instead of manual v-model implementation.
Overview
This rule detects the old pattern of using modelValue prop with update:modelValue emit and suggests using the defineModel() macro instead.
Source Code
import { defineCodeRule } from "@syncrolabs/claude-code-validator";
export const defineModelRule = defineCodeRule({
name: "define-model",
description:
"Enforce use of defineModel() macro instead of modelValue prop + emit pattern",
shouldRun: (context) => {
return context.filePath.endsWith(".vue");
},
validate(context) {
const errors: string[] = [];
// Check for modelValue in defineProps (various patterns)
const hasModelValueProp =
// Inline type: defineProps<{ modelValue: ... }>
/defineProps\s*<[^>]*{\s*[^}]*modelValue[^}]*}[^>]*>/s.test(
context.content
) ||
// Runtime props: defineProps({ modelValue: ... })
/defineProps\s*\([^)]*modelValue[^)]*\)/s.test(context.content) ||
// Interface/Type usage with defineProps + interface/type with modelValue
(/defineProps\s*<\s*Props\s*>/s.test(context.content) &&
/interface\s+Props\s*{[^}]*modelValue[^}]*}/s.test(context.content)) ||
// withDefaults pattern
(/withDefaults\s*\(\s*defineProps\s*<\s*Props\s*>/s.test(
context.content
) &&
/interface\s+Props\s*{[^}]*modelValue[^}]*}/s.test(context.content));
// Check for update:modelValue in defineEmits
const hasUpdateModelValueEmit =
/defineEmits\s*<[^>]*{\s*[^}]*['"]update:modelValue['"]/s.test(
context.content
) ||
/defineEmits\s*\([^)]*['"]update:modelValue['"]/s.test(context.content);
if (hasModelValueProp && hasUpdateModelValueEmit) {
errors.push(
`â Using modelValue prop with update:modelValue emit is outdated\n` +
` â Use the defineModel() macro instead for two-way binding\n` +
` âšī¸ Example: const model = defineModel<YourType>()\n` +
` đ File: ${context.filePath}`
);
}
return errors;
},
});
What It Catches
Old Pattern (Blocked)
<script setup lang="ts">
interface Props {
modelValue: string;
}
const props = defineProps<Props>();
const emit = defineEmits<{
"update:modelValue": [value: string];
}>();
function updateValue(value: string) {
emit("update:modelValue", value);
}
</script>
New Pattern (Allowed)
<script setup lang="ts">
const model = defineModel<string>();
function updateValue(value: string) {
model.value = value;
}
</script>
Detected Patterns
The rule detects several prop definition patterns:
Inline Type Definition
// â Blocked
const props = defineProps<{
modelValue: string;
}>();
const emit = defineEmits<{
"update:modelValue": [value: string];
}>();
Interface-Based
// â Blocked
interface Props {
modelValue: string;
}
const props = defineProps<Props>();
const emit = defineEmits<{ "update:modelValue": [value: string] }>();
With Defaults
// â Blocked
interface Props {
modelValue?: string;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: "",
});
const emit = defineEmits<{ "update:modelValue": [value: string] }>();
Runtime Props
// â Blocked
const props = defineProps({
modelValue: String,
});
const emit = defineEmits(["update:modelValue"]);
Benefits
- Simpler code - Less boilerplate with
defineModel() - Type safety - Better TypeScript inference
- Consistency - Enforces modern Vue 3 patterns
- Maintainable - Easier to read and understand
Migration Guide
Before (old pattern):
<script setup lang="ts">
interface Props {
modelValue: string;
label?: string;
}
const props = withDefaults(defineProps<Props>(), {
label: "Default label",
});
const emit = defineEmits<{
"update:modelValue": [value: string];
}>();
function handleInput(event: Event) {
const target = event.target as HTMLInputElement;
emit("update:modelValue", target.value);
}
</script>
<template>
<div>
<label>{{ label }}</label>
<input :value="modelValue" @input="handleInput" />
</div>
</template>
After (with defineModel):
<script setup lang="ts">
interface Props {
label?: string;
}
const props = withDefaults(defineProps<Props>(), {
label: "Default label",
});
const model = defineModel<string>();
</script>
<template>
<div>
<label>{{ label }}</label>
<input v-model="model" />
</div>
</template>
Test Cases
describe("Define Model Rule", () => {
it("should detect inline type modelValue pattern", () => {
const context = {
toolName: "Write",
filePath: "test.vue",
content: `
const props = defineProps<{ modelValue: string }>();
const emit = defineEmits<{ 'update:modelValue': [value: string] }>();
`,
operation: "write" as const,
};
const errors = defineModelRule.validate(context);
expect(errors).toHaveLength(1);
});
it("should not flag defineModel usage", () => {
const context = {
toolName: "Write",
filePath: "test.vue",
content: "const model = defineModel<string>();",
operation: "write" as const,
};
const errors = defineModelRule.validate(context);
expect(errors).toHaveLength(0);
});
});