Cross-file rules
Every rule on this page includes its purpose, prerequisites, configuration, and complete Bad/Good examples. Highlighted lines show what changes; copying an example gives you the complete source. Each rule also states its current support limits.
This page shows all 66 project examples with their complete Bad and Good versions and the files both versions share. Use the shared files with both. Of the 60 diagnostic codes, the CLI reports 19 today (18 verified against source examples and one shown with an illustrative project), 16 exist only in the experimental Rust analyzer and are not reported individually by the CLI, and 25 are specified but nothing produces them yet. Configuring an ID does not turn on a check that is not available.
The public CLI exposes the same pass with vize lint --cross-file. Displayed vize:croquis/cf/* codes use croquis/cf/* in vue.linter.rules (omit vize:). Information/hint diagnostics become CLI warnings. Related locations explain the source/consumer relationship.
| Rule | Examples | Purpose | Current support |
|---|---|---|---|
ecosystem/vue-router-extra-param |
Bad · Good | The route does not declare tab; Vue Router discards it. | Project-specific lint ID |
ecosystem/vue-router-missing-param |
Bad · Good | Required postId is missing; relying on the current route is fragile. | Project-specific lint ID |
ecosystem/vue-router-param-type |
Bad · Good | postId is not repeatable, so an array is invalid. | Project-specific lint ID |
ecosystem/vue-router-unknown-route |
Bad · Good | The name is absent from the complete installed router. | Project-specific lint ID |
html/cross-component-nesting |
Bad · Good | Check actual HTML nesting after imported components are composed. | Project-specific lint ID |
vize:croquis/cf/array-mutation |
Bad · Good | This producer-free contract uses a historical Vue 2.7 array-index assignment scenario. Vue 2 does not notify updates for direct index assignment; Vue 3 tracks such assignments. The Bad/Good files illustrate the Vue 2 repair, not a Vue 3 defect. | Contract; no current producer |
vize:croquis/cf/async-boundary |
Bad · Good | Reactive state crosses an async boundary and can be observed stale. | CLI |
vize:croquis/cf/async-no-suspense |
Bad · Good | An async component is rendered without a Suspense boundary. | Experimental Rust analyzer; not an individual CLI code |
vize:croquis/cf/browser-api-ssr |
Bad · Good | A browser-only API is used where the component can render on the server. | CLI |
vize:croquis/cf/circular-dep |
Bad · Good | Components import each other in a cycle. | Contract; no current producer |
vize:croquis/cf/circular-reactive-dependency |
Bad · Good | Reactive computations depend on each other in a cycle. | CLI |
vize:croquis/cf/closure-captures-reactive |
Bad · Good | A closure captures a reactive value and will not see later updates. | Contract; no current producer |
vize:croquis/cf/composable-outside-setup |
Bad · Good | A composable is called outside setup. |
Contract; no current producer |
vize:croquis/cf/computed-side-effects |
Bad · Good | A computed getter writes state or performs another side effect. | Contract; no current producer |
vize:croquis/cf/deep-import |
Bad · Good | An import chain is deeper than the project allows. | Contract; no current producer |
vize:croquis/cf/destructuring-breaks-reactivity |
Bad · Good | Destructuring a reactive object copies the fields and drops tracking. | CLI |
vize:croquis/cf/di-outside-setup |
Bad · Good | provide or inject is called outside setup. |
Contract; no current producer |
vize:croquis/cf/dom-access-without-next-tick |
Bad · Good | The DOM is read before Vue has flushed the update. | Contract; no current producer |
vize:croquis/cf/duplicate-id |
Bad · Good | The same element id is used in more than one component. | CLI |
vize:croquis/cf/event-listener-leak |
Bad · Good | An event listener is registered and never removed. | Contract; no current producer |
vize:croquis/cf/event-modifier |
Bad · Good | An event listener uses a modifier the emit does not support. | Experimental Rust analyzer; not an individual CLI code |
vize:croquis/cf/hydration-risk |
Bad · Good | This code groups several reactivity findings, including a prop copied into a ref. It does not imply that every Date.now() expression is detected by the cross-file pass. | CLI |
vize:croquis/cf/inherit-attrs-unused |
Bad · Good | inheritAttrs: false is set and the component never reads the attributes. |
Experimental Rust analyzer; not an individual CLI code |
vize:croquis/cf/inject-without-symbol |
Bad · Good | inject uses a plain key instead of an InjectionKey symbol. |
CLI |
vize:croquis/cf/injected-async-mutation-race |
Bad · Good | An injected value is mutated from an async task that can race. | CLI |
vize:croquis/cf/lifecycle-outside-setup |
Bad · Good | A lifecycle hook is registered outside setup. |
Contract; no current producer |
vize:croquis/cf/lifecycle-without-cleanup |
Bad · Good | A lifecycle hook starts work and never cleans it up. | Experimental Rust analyzer; not an individual CLI code |
vize:croquis/cf/missing-required-prop |
Bad · Good | A required prop is not passed. | Experimental Rust analyzer; not an individual CLI code |
vize:croquis/cf/missing-suspense |
Bad · Good | An async dependency is used outside a Suspense boundary. | Contract; no current producer |
vize:croquis/cf/module-scope-reactive |
Bad · Good | Reactive state is created at module scope and shared by every caller. | Contract; no current producer |
vize:croquis/cf/multi-root-attrs |
Bad · Good | A multi-root component receives attributes and has nowhere to put them. | Experimental Rust analyzer; not an individual CLI code |
vize:croquis/cf/mutated-after-escape |
Bad · Good | A reactive object is mutated after it has escaped its owner. | Contract; no current producer |
vize:croquis/cf/non-reactive-provide |
Bad · Good | A provided value is not reactive, so descendants will not see updates. | CLI |
vize:croquis/cf/non-unique-id |
Bad · Good | An element id inside a loop is not unique per item. | CLI |
vize:croquis/cf/object-identity-comparison |
Bad · Good | A raw object and its reactive proxy have distinct object identities, and toRaw() returns the original object. This contract illustrates comparing stable record IDs when record identity is intended. | Contract; no current producer |
vize:croquis/cf/pinia-getter |
Bad · Good | This contract illustrates copying a Pinia getter's current primitive value during setup, leaving a snapshot that no longer follows updates. Direct getter reads within a tracked computation or template remain reactive; storeToRefs() is the extraction repair shown here. | Contract; no current producer |
vize:croquis/cf/prop-type-mismatch |
Bad · Good | A passed prop value does not match the declared type. | Experimental Rust analyzer; not an individual CLI code |
vize:croquis/cf/provide-inject-type |
Bad · Good | A provided value and its inject do not have the same type. | CLI |
vize:croquis/cf/provide-without-symbol |
Bad · Good | provide uses a plain key instead of an InjectionKey symbol. |
CLI |
vize:croquis/cf/reactive-export |
Bad · Good | Reactive state is exported from the module. | Contract; no current producer |
vize:croquis/cf/reactivity-outside-setup |
Bad · Good | A reactive API is called outside setup. |
Contract; no current producer |
vize:croquis/cf/reassignment-breaks-reactivity |
Bad · Good | Reassigning a reactive binding replaces it with a plain value. | CLI |
vize:croquis/cf/reference-escapes-scope |
Bad · Good | A reactive reference escapes the scope that owns its lifetime. | Contract; no current producer |
vize:croquis/cf/setup-context-violation |
Bad · Good | This experimental analyzer scenario places ref(0) at normal script module scope rather than per-instance setup. Vue permits this reactive state; the example requires each component instance to own its own count. The CLI does not emit this individual code. | Experimental Rust analyzer; not an individual CLI code |
vize:croquis/cf/shallow-deep-access |
Bad · Good | A deep property of a shallowReactive or shallowRef value is read as if it were tracked. |
Contract; no current producer |
vize:croquis/cf/spread-breaks-reactivity |
Bad · Good | Spreading a reactive object copies its values and drops the tracking. | CLI |
vize:croquis/cf/suspense-no-fallback |
Bad · Good | <Suspense> has no fallback content. |
Contract; no current producer |
vize:croquis/cf/template-ref-timing |
Bad · Good | A template ref is read before the component is mounted. | Contract; no current producer |
vize:croquis/cf/toraw-mutation |
Bad · Good | toRaw is used and the raw object is then mutated. |
Contract; no current producer |
vize:croquis/cf/uncaught-error |
Bad · Good | A component can throw and no error boundary catches it. | CLI |
vize:croquis/cf/undeclared-emit |
Bad · Good | The component emits an event that is not declared. | Experimental Rust analyzer; not an individual CLI code |
vize:croquis/cf/undeclared-prop |
Bad · Good | A parent passes a prop the child does not declare. | Experimental Rust analyzer; not an individual CLI code |
vize:croquis/cf/undefined-slot |
Bad · Good | A parent fills a slot the child does not expose. | Contract; no current producer |
vize:croquis/cf/unhandled-event |
Bad · Good | A child emits an event that no parent handles. | Experimental Rust analyzer; not an individual CLI code |
vize:croquis/cf/unmatched-inject |
Bad · Good | inject names a key that no ancestor provides. |
CLI |
vize:croquis/cf/unmatched-listener |
Bad · Good | A parent listens for an event the child does not emit. | Experimental Rust analyzer; not an individual CLI code |
vize:croquis/cf/unregistered-component |
Bad · Good | A template uses a component that is not registered or imported. | Experimental Rust analyzer; not an individual CLI code |
vize:croquis/cf/unresolved-import |
Bad · Good | An import does not resolve to a module. | Experimental Rust analyzer; not an individual CLI code |
vize:croquis/cf/unused-attrs |
Bad · Good | Fallthrough attributes are passed to a multi-root component that does not use them. | Experimental Rust analyzer; not an individual CLI code |
vize:croquis/cf/unused-emit |
Bad · Good | A declared emit is never used. | Experimental Rust analyzer; not an individual CLI code |
vize:croquis/cf/unused-provide |
Bad · Good | A provided key is never injected. | CLI |
vize:croquis/cf/value-extraction-breaks-reactivity |
Bad · Good | Reading a reactive value out into a local drops later updates. | CLI |
vize:croquis/cf/watch-can-be-computed |
Bad · Good | A watcher only copies a value into state and can be a computed. | Contract; no current producer |
vize:croquis/cf/watcheffect-async |
Bad · Good | The async watchEffect shown here has no invalidation cleanup or stale-response guard. Vue supports cleanup for watchEffect; the Good example uses an explicit watch source and cancels invalidated work. | CLI |
vize:croquis/cf/watcher-outside-setup |
Bad · Good | watch or watchEffect is called outside setup. |
Contract; no current producer |
vue/cross-file-attrs-fallthrough |
Bad · Good | A parent passes attributes to a resolved child whose root cannot inherit them and does not explicitly use $attrs. | Project-specific lint ID |
All rules · Rule Options · ESLint migration map · Project checks · Attributes across components
ecosystem/vue-router-extra-param
The route does not declare tab; Vue Router discards it.
Default severity: error
Applies to: Reachable project declarations and imported components
Options: crossFile; rule severity (off/warn/error)
Automatic fix: None
The complete installed router must be reachable from the application's createApp(...).use(router). Unknown/dynamic route tables do not prove unknown-name findings. Missing params are warnings because navigation may inherit a value from the current route.
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "ecosystem/vue-router-extra-param": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
index.html
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
src/main.ts
import { createApp } from "vue";
import App from "./App.vue";
import { router } from "./router";
createApp(App).use(router).mount("#app");
src/App.vue
<script setup lang="ts">
import { RouterView } from "vue-router";
</script>
<template><RouterView /></template>
src/router.ts
import { createRouter, createWebHistory } from "vue-router";
import UserPost from "./UserPost.vue";
export const router = createRouter({
history: createWebHistory(),
routes: [{ path: "/users/:userId/posts/:postId", name: "user-post", component: UserPost }],
});
Bad
The user-post path declares userId and postId, but the navigation also supplies undeclared tab as a path parameter.
src/UserPost.vue
<script setup lang="ts">
import { useRouter } from "vue-router";
const router = useRouter();
router.push({ name: "user-post", params: { userId: "1", postId: "2", tab: "a" } });
</script>
<template><p>Post</p></template>
Good
Remove tab from params and retain only the keys present in the route path. Use query separately if the application needs a tab selection.
src/UserPost.vue
<script setup lang="ts">
import { useRouter } from "vue-router";
const router = useRouter();
router.push({ name: "user-post", params: { userId: "1", postId: "2" } });
</script>
<template><p>Post</p></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
ecosystem/vue-router-missing-param
Required postId is missing; relying on the current route is fragile.
Default severity: warning
Applies to: Reachable project declarations and imported components
Options: crossFile; rule severity (off/warn/error)
Automatic fix: None
The complete installed router must be reachable from the application's createApp(...).use(router). Unknown/dynamic route tables do not prove unknown-name findings. Missing params are warnings because navigation may inherit a value from the current route.
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "ecosystem/vue-router-missing-param": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
index.html
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
src/main.ts
import { createApp } from "vue";
import App from "./App.vue";
import { router } from "./router";
createApp(App).use(router).mount("#app");
src/App.vue
<script setup lang="ts">
import { RouterView } from "vue-router";
</script>
<template><RouterView /></template>
src/router.ts
import { createRouter, createWebHistory } from "vue-router";
import UserPost from "./UserPost.vue";
export const router = createRouter({
history: createWebHistory(),
routes: [{ path: "/users/:userId/posts/:postId", name: "user-post", component: UserPost }],
});
Bad
The navigation omits required postId from the user-post path. This is a warning because Vue Router may inherit a value from the current route.
src/UserPost.vue
<script setup lang="ts">
import { useRouter } from "vue-router";
const router = useRouter();
router.push({ name: "user-post", params: { userId: "1" } });
</script>
<template><p>Post</p></template>
Good
Pass both userId and postId explicitly so navigation does not depend on the current route's parameter state.
src/UserPost.vue
<script setup lang="ts">
import { useRouter } from "vue-router";
const router = useRouter();
router.push({ name: "user-post", params: { userId: "1", postId: "2" } });
</script>
<template><p>Post</p></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
ecosystem/vue-router-param-type
postId is not repeatable, so an array is invalid.
Default severity: error
Applies to: Reachable project declarations and imported components
Options: crossFile; rule severity (off/warn/error)
Automatic fix: None
The complete installed router must be reachable from the application's createApp(...).use(router). Unknown/dynamic route tables do not prove unknown-name findings. Missing params are warnings because navigation may inherit a value from the current route.
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "ecosystem/vue-router-param-type": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
index.html
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
src/main.ts
import { createApp } from "vue";
import App from "./App.vue";
import { router } from "./router";
createApp(App).use(router).mount("#app");
src/App.vue
<script setup lang="ts">
import { RouterView } from "vue-router";
</script>
<template><RouterView /></template>
src/router.ts
import { createRouter, createWebHistory } from "vue-router";
import UserPost from "./UserPost.vue";
export const router = createRouter({
history: createWebHistory(),
routes: [{ path: "/users/:userId/posts/:postId", name: "user-post", component: UserPost }],
});
Bad
postId is a scalar path parameter, but the navigation gives it the array ["2"].
src/UserPost.vue
<script setup lang="ts">
import { useRouter } from "vue-router";
const router = useRouter();
router.push({ name: "user-post", params: { userId: "1", postId: ["2"] } });
</script>
<template><p>Post</p></template>
Good
Pass the scalar "2" for the non-repeatable postId segment.
src/UserPost.vue
<script setup lang="ts">
import { useRouter } from "vue-router";
const router = useRouter();
router.push({ name: "user-post", params: { userId: "1", postId: "2" } });
</script>
<template><p>Post</p></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
ecosystem/vue-router-unknown-route
The name is absent from the complete installed router.
Default severity: error
Applies to: Reachable project declarations and imported components
Options: crossFile; rule severity (off/warn/error)
Automatic fix: None
The complete installed router must be reachable from the application's createApp(...).use(router). Unknown/dynamic route tables do not prove unknown-name findings. Missing params are warnings because navigation may inherit a value from the current route.
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "ecosystem/vue-router-unknown-route": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
index.html
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
src/main.ts
import { createApp } from "vue";
import App from "./App.vue";
import { router } from "./router";
createApp(App).use(router).mount("#app");
src/App.vue
<script setup lang="ts">
import { RouterView } from "vue-router";
</script>
<template><RouterView /></template>
src/router.ts
import { createRouter, createWebHistory } from "vue-router";
import UserPost from "./UserPost.vue";
export const router = createRouter({
history: createWebHistory(),
routes: [{ path: "/users/:userId/posts/:postId", name: "user-post", component: UserPost }],
});
Bad
The reachable installed router declares user-post, but navigation uses the misspelled user-posts name.
src/UserPost.vue
<script setup lang="ts">
import { useRouter } from "vue-router";
const router = useRouter();
router.push({ name: "user-posts", params: { userId: "1", postId: "2" } });
</script>
<template><p>Post</p></template>
Good
Use the registered user-post name while retaining both declared path parameters.
src/UserPost.vue
<script setup lang="ts">
import { useRouter } from "vue-router";
const router = useRouter();
router.push({ name: "user-post", params: { userId: "1", postId: "2" } });
</script>
<template><p>Post</p></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
html/cross-component-nesting
Check actual HTML nesting after imported components are composed.
Default severity: warning
Applies to: Reachable project declarations and imported components
Options: crossFile; rule severity (off/warn/error)
Automatic fix: None
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "html/cross-component-nesting": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
The parent's <p> contains a resolved child whose root is <div>, producing invalid paragraph/block nesting after composition.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><p><Child /></p></template>
Child.vue
<template><div>Block content</div></template>
Good
Use a <section> container that can contain the child's block element; the child stays unchanged.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><section><Child /></section></template>
Child.vue
<template><div>Block content</div></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/array-mutation
This producer-free contract uses a historical Vue 2.7 array-index assignment scenario. Vue 2 does not notify updates for direct index assignment; Vue 3 tracks such assignments. The Bad/Good files illustrate the Vue 2 repair, not a Vue 3 defect.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
Historical Vue 2.7 only: use matching Vue 2.7 and SFC compiler dependencies for this scenario. Vue 3 proxies track array index assignment, so items[0] = next is reactive in Vue 3 and is not a Vue 3 defect. This published code has no current producer.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import Vue from 'vue';
import App from './App.vue';
new Vue({ render: h => h(App) }).$mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
App.vue
<script lang="ts">
import Vue from 'vue';
import { replaceFirst } from './replace-first';
export default Vue.extend({
data() { return { items: ['Before'] }; },
methods: { replace() { replaceFirst(this.items, 'After'); } },
});
</script>
<template><section><p>{{ items[0] }}</p><button @click="replace">Replace</button></section></template>
Bad
In this historical Vue 2.7 project, items[0] = next changes the array without notifying Vue 2’s array observer, so the displayed first item need not update.
replace-first.ts
export function replaceFirst(items: string[], next: string): void {
items[0] = next;
}
Good
splice(0, 1, next) uses the array mutation method observed by Vue 2, allowing the same replacement to update the view.
replace-first.ts
export function replaceFirst(items: string[], next: string): void {
items.splice(0, 1, next);
}
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/async-boundary
Reactive state crosses an async boundary and can be observed stale.
Default severity: error
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "croquis/cf/async-boundary": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./SearchPage.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
api.ts
export interface Result { items: string[]; }
export async function load(query: string, options?: { signal?: AbortSignal }): Promise<Result> {
const response = await fetch(`/search?q=${encodeURIComponent(query)}`, options);
return response.json();
}
Bad
A slower old query can finish after a newer query and overwrite result, because the watcher has no invalidation cleanup.
SearchPage.vue
<script setup lang="ts">
import { ref } from "vue";
import SearchResults from "./SearchResults.vue";
const query = ref("");
</script>
<template>
<SearchResults :query="query" />
</template>
SearchResults.vue
<script setup lang="ts">
import { load, type Result } from "./api";
import { ref, watch } from "vue";
const props = defineProps<{ query: string }>();
const result = ref<Result | null>(null);
watch(
() => props.query,
async (value) => {
result.value = await load(value);
},
);
</script>
Good
Register cleanup before awaiting: abort the old request and invalidate its active flag, then assign only a still-active response.
SearchPage.vue
<script setup lang="ts">
import { ref } from "vue";
import SearchResults from "./SearchResults.vue";
const query = ref("");
</script>
<template>
<SearchResults :query="query" />
</template>
SearchResults.vue
<script setup lang="ts">
import { load, type Result } from "./api";
import { ref, watch } from "vue";
const props = defineProps<{ query: string }>();
const result = ref<Result | null>(null);
watch(
() => props.query,
async (value, _oldValue, onCleanup) => {
const controller = new AbortController();
let active = true;
onCleanup(() => {
active = false;
controller.abort();
});
const next = await load(value, { signal: controller.signal });
if (active) result.value = next;
},
);
</script>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/async-no-suspense
An async component is rendered without a Suspense boundary.
Default severity: warning
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
The experimental Rust CrossFileAnalyzer has a producer for this code. The CLI pass does not emit this individual code; configuring its ID does not enable that Rust pass. These scenarios describe the analyzer's supported graph/facts, not a Vite+ promise.
Current support: no-source-async-fact
The boundary producer reads macros.is_async(), but source parsing currently records top-level await on the script-setup scope instead. The complete Bad/Good source pair below therefore produces no async-no-suspense finding through the current CLI. It explains the Suspense convention; supplying the missing macro fact is implementation follow-up work.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
The child has top-level await but its parent supplies no <Suspense> boundary. Current source parsing does not supply the macro fact required to emit this code.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child /></template>
Child.vue
<script setup lang="ts">
const greeting = await Promise.resolve("Hello");
</script>
<template><p>{{ greeting }}</p></template>
Good
The parent wraps the same async child in <Suspense> with a loading fallback. This demonstrates the convention; both source alternatives remain non-emitted by the current pass.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Suspense><Child /><template #fallback><p>Loading</p></template></Suspense></template>
Child.vue
<script setup lang="ts">
const greeting = await Promise.resolve("Hello");
</script>
<template><p>{{ greeting }}</p></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/browser-api-ssr
A browser-only API is used where the component can render on the server.
Default severity: warning
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "croquis/cf/browser-api-ssr": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
window.innerWidth runs during setup, where an SSR environment has no browser window.
App.vue
<script setup lang="ts">
const width = window.innerWidth;
</script>
<template><p>Content</p></template>
Good
Initialize a ref to a server-safe value and read window inside onMounted, which runs after client mounting.
App.vue
<script setup lang="ts">
import { onMounted, ref } from "vue";
const width = ref(0);
onMounted(() => { width.value = window.innerWidth; });
</script>
<template><p>Content</p></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/circular-dep
Components import each other in a cycle.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
This illustrates a concrete eager-initialization cycle. A recursive Vue component or every circular import is not automatically erroneous. No current producer emits this contract code.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
App.vue
<script setup lang="ts">
import { aLabel } from './a';
</script>
<template>
<p>{{ aLabel }}</p>
</template>
labels.ts
export const aPrefix = 'A';
export const bPrefix = 'B';
Bad
a.ts imports b.ts, which imports a.ts back. Both eagerly initialize a constant from the other module’s still-uninitialized constant, creating a temporal-dead-zone failure.
a.ts
import { bLabel } from './b';
export const aLabel = 'A' + bLabel;
b.ts
import { aLabel } from './a';
export const bLabel = 'B' + aLabel;
Good
Both modules read initialized prefixes from the independent labels.ts module, removing the cycle and the eager cross-read.
a.ts
import { bPrefix } from './labels';
export const aLabel = 'A' + bPrefix;
b.ts
import { aPrefix } from './labels';
export const bLabel = 'B' + aPrefix;
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/circular-reactive-dependency
Reactive computations depend on each other in a cycle.
Default severity: context-dependent
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "croquis/cf/circular-reactive-dependency": "warn" },
},
},
});
vp run lint
Example qualification: illustrative-source-pair
The complete Vue project below illustrates update feedback and its repair. It is not a qualified CLI finding witness: the diagnostic producer requires retained reactive-flow reference identities and edges, as shown by the accompanying graph. These sources do not establish that the current source path will emit this exact code. Dedicated tracked-ID graph finding controls remain separate from source grammar checks.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
count-key.ts
import type { InjectionKey, Ref } from 'vue';
export const countKey: InjectionKey<Ref<number>> = Symbol('count');
App.vue
<script setup lang="ts">
import { provide, ref } from 'vue';
import { countKey } from './count-key';
import CycleView from './CycleView.vue';
const count = ref(1); // A: the provider-owned source.
provide(countKey, count);
</script>
<template>
<button @click="count++">Increment</button>
<CycleView />
</template>
Bad
App owns and provides count (A). CycleView derives nextCount (B), then immediately writes each derived value back into the same injected count. Every write changes the input to the computation again, creating update feedback A → B → A. The identities in the retained graph below represent these two references, not unrelated bindings with matching names.
CycleView.vue
<script setup lang="ts">
import { computed, inject, watch } from 'vue';
import { countKey } from './count-key';
const count = inject(countKey)!; // App provides this same A reference.
const nextCount = computed(() => count.value + 1); // B: the derived consumer.
watch(nextCount, value => { count.value = value; }, { immediate: true });
</script>
<template><p>{{ nextCount }}</p></template>
Tracked references: A = provider source; B = consumer reference
Tracked flows: A -> B; B -> A
Good
Remove the watcher that writes B back into A. App keeps ownership of count and changes it only through its explicit Increment action; CycleView reads the derived nextCount without feeding the result back. The same references retain only the A → B dependency.
CycleView.vue
<script setup lang="ts">
import { computed, inject } from 'vue';
import { countKey } from './count-key';
const count = inject(countKey)!; // App provides this same A reference.
const nextCount = computed(() => count.value + 1); // B: the derived consumer.
</script>
<template><p>{{ nextCount }}</p></template>
Tracked references: A = provider source; B = consumer reference
Tracked flows: A -> B
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/closure-captures-reactive
A closure captures a reactive value and will not see later updates.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
App.vue
<script setup lang="ts">
import { computed, ref } from 'vue';
import { makeReader } from './reader';
const count = ref(0);
const read = makeReader(count);
const shown = computed(read);
</script>
<template>
<button @click="count++">Increment {{ count }}</button><p>{{ shown }}</p>
</template>
Bad
makeReader copies count.value before creating the closure. The computed reader then returns that initial number without reading a reactive dependency.
reader.ts
import type { Ref } from 'vue';
export function makeReader(count: Ref<number>): () => number {
const captured = count.value;
return () => captured;
}
Good
The closure reads count.value when invoked, so the computed getter can track the ref and update shown after increments.
reader.ts
import type { Ref } from 'vue';
export function makeReader(count: Ref<number>): () => number {
return () => count.value;
}
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/composable-outside-setup
A composable is called outside setup.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
The concern is this lifecycle-dependent composable, not a blanket ban on ordinary utility functions or all Composition API calls outside setup. This contract has no current producer.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
App.vue
<script setup lang="ts">
import { useTitle } from './use-title';
const title = useTitle();
</script>
<template>
<h1>{{ title }}</h1>
</template>
Bad
Importing use-title.ts registers onMounted before a component setup is active. Calling its exported function later only returns that module-level ref; it cannot repair the missed lifecycle ownership.
use-title.ts
import { onMounted, ref } from 'vue';
const title = ref('Before mount');
onMounted(() => { title.value = 'Mounted'; });
export function useTitle() { return title; }
Good
Both state creation and hook registration move into useTitle, which App calls synchronously inside setup. The mounted hook now belongs to that App instance.
use-title.ts
import { onMounted, ref } from 'vue';
export function useTitle() {
const title = ref('Before mount');
onMounted(() => { title.value = 'Mounted'; });
return title;
}
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/computed-side-effects
A computed getter writes state or performs another side effect.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
App.vue
<script setup lang="ts">
import { useDouble } from './use-double';
const { count, doubled, lastCalculated } = useDouble();
</script>
<template>
<button @click="count++">Increment {{ count }}</button><p>{{ doubled }} / {{ lastCalculated }}</p>
</template>
Bad
Evaluating doubled writes lastCalculated, so reading a computed value also mutates separate state. That couples the side effect to when the lazy getter is read.
use-double.ts
import { computed, ref } from 'vue';
export function useDouble() {
const count = ref(0);
const lastCalculated = ref(0);
const doubled = computed(() => {
const next = count.value * 2;
lastCalculated.value = next;
return next;
});
return { count, doubled, lastCalculated };
}
Good
The getter only returns the derived number. A separate watcher owns the write to lastCalculated when count changes, including its initial value.
use-double.ts
import { computed, ref, watch } from 'vue';
export function useDouble() {
const count = ref(0);
const lastCalculated = ref(0);
const doubled = computed(() => count.value * 2);
watch(count, next => { lastCalculated.value = next * 2; }, { immediate: true });
return { count, doubled, lastCalculated };
}
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/deep-import
An import chain is deeper than the project allows.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
This is an explicitly chosen project layout policy; it does not invent a supported depth threshold or option. There is no current diagnostic producer for this contract.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
App.vue
<script setup lang="ts">
import { label } from './entry';
</script>
<template>
<p>{{ label }}</p>
</template>
value.ts
export const label = 'Notice';
level-one.ts
export { label } from './level-two';
level-two.ts
export { label } from './level-three';
level-three.ts
export { label } from './value';
public-api.ts
export { label } from './value';
Bad
The entry routes a simple value through level-one, level-two, and level-three, creating an unnecessarily deep import chain for a project that wants a shallow public boundary.
entry.ts
export { label } from './level-one';
Good
The entry uses public-api.ts, which re-exports the value directly. The consumer keeps the same imported name while the chain becomes shorter.
entry.ts
export { label } from './public-api';
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/destructuring-breaks-reactivity
Destructuring a reactive object copies the fields and drops tracking.
Default severity: error
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "croquis/cf/destructuring-breaks-reactivity": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./UserPage.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
Ordinary destructuring of the props object copies its current item value; this is separate from direct Vue 3.5 defineProps() destructuring.
UserPage.vue
<script setup lang="ts">
import { reactive } from "vue";
import UserSummary from "./UserSummary.vue";
const user = reactive({ name: "Ada" });
</script>
<template>
<UserSummary :item="user" />
</template>
UserSummary.vue
<script setup lang="ts">
const props = defineProps<{ item: { name: string } }>();
const { item } = props;
</script>
Good
toRef(props, "item") retains the connection to the property on props.
UserPage.vue
<script setup lang="ts">
import { reactive } from "vue";
import UserSummary from "./UserSummary.vue";
const user = reactive({ name: "Ada" });
</script>
<template>
<UserSummary :item="user" />
</template>
UserSummary.vue
<script setup lang="ts">
import { toRef } from "vue";
const props = defineProps<{ item: { name: string } }>();
const item = toRef(props, "item");
</script>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/di-outside-setup
provide or inject is called outside setup.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
This example uses component provide/inject. app.provide and supported app.runWithContext injection are different valid ownership surfaces, not prohibited by this scenario. No current producer emits this contract code.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
theme.ts
import { inject, provide } from 'vue';
import type { InjectionKey } from 'vue';
export const ThemeKey: InjectionKey<string> = Symbol('theme');
export function provideTheme() { provide(ThemeKey, 'dark'); }
export function useTheme() { return inject(ThemeKey, 'light'); }
ThemedText.vue
<script setup lang="ts">
import { useTheme } from './theme';
const theme = useTheme();
</script>
<template>
<p>{{ theme }}</p>
</template>
Bad
main.ts calls component provide with no active component instance. The child’s inject therefore cannot receive this intended ancestor value and uses light.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { provideTheme } from './theme';
provideTheme();
createApp(App).mount('#app');
App.vue
<script setup lang="ts">
import ThemedText from './ThemedText.vue';
</script>
<template>
<ThemedText />
</template>
Good
App calls the provider from its setup before rendering the child. The child now inherits the dark value from its component ancestor.
App.vue
<script setup lang="ts">
import ThemedText from './ThemedText.vue';
import { provideTheme } from './theme';
provideTheme();
</script>
<template>
<ThemedText />
</template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/dom-access-without-next-tick
The DOM is read before Vue has flushed the update.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
read-label.ts
export function readLabel(node: HTMLElement | null): string {
return node?.textContent ?? '';
}
Bad
The click handler increments count and immediately reads the rendered paragraph, before Vue flushes the scheduled DOM update. sampled can contain the previous count.
App.vue
<script setup lang="ts">
import { ref } from 'vue';
import { readLabel } from './read-label';
const count = ref(0);
const label = ref<HTMLElement | null>(null);
const sampled = ref('');
function increment() {
count.value++;
sampled.value = readLabel(label.value);
}
</script>
<template>
<button @click="increment">Increment</button><p ref="label">{{ count }}</p><p>DOM sample: {{ sampled }}</p>
</template>
Good
Awaiting nextTick() after the state write lets Vue update the paragraph before readLabel samples its text.
App.vue
<script setup lang="ts">
import { nextTick, ref } from 'vue';
import { readLabel } from './read-label';
const count = ref(0);
const label = ref<HTMLElement | null>(null);
const sampled = ref('');
async function increment() {
count.value++;
await nextTick();
sampled.value = readLabel(label.value);
}
</script>
<template>
<button @click="increment">Increment</button><p ref="label">{{ count }}</p><p>DOM sample: {{ sampled }}</p>
</template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/duplicate-id
The same element id is used in more than one component.
Default severity: warning
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "croquis/cf/duplicate-id": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./CheckoutForm.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
The reachable shipping and billing components both render id="postal-code", so their labels share an ambiguous document target.
CheckoutForm.vue
<script setup lang="ts">
import BillingAddress from "./BillingAddress.vue";
import ShippingAddress from "./ShippingAddress.vue";
</script>
<template>
<ShippingAddress />
<BillingAddress />
</template>
ShippingAddress.vue
<template>
<label for="postal-code">Shipping postal code</label>
<input id="postal-code" />
</template>
BillingAddress.vue
<template>
<label for="postal-code">Billing postal code</label>
<input id="postal-code" />
</template>
Good
Each component calls useId() and binds its own value to both label and input, preserving the association without a repeated literal ID.
CheckoutForm.vue
<script setup lang="ts">
import BillingAddress from "./BillingAddress.vue";
import ShippingAddress from "./ShippingAddress.vue";
</script>
<template>
<ShippingAddress />
<BillingAddress />
</template>
ShippingAddress.vue
<script setup lang="ts">
import { useId } from "vue";
const postalCodeId = useId();
</script>
<template>
<label :for="postalCodeId">Shipping postal code</label>
<input :id="postalCodeId" />
</template>
BillingAddress.vue
<script setup lang="ts">
import { useId } from "vue";
const postalCodeId = useId();
</script>
<template>
<label :for="postalCodeId">Billing postal code</label>
<input :id="postalCodeId" />
</template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/event-listener-leak
An event listener is registered and never removed.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
App.vue
<script setup lang="ts">
import { useWidth } from './use-width';
const width = useWidth();
</script>
<template>
<p>{{ width }}</p>
</template>
Bad
Mounting adds a window resize listener that captures the component’s width ref, but unmounting never removes it. Repeated mounts can retain unused listeners and state.
use-width.ts
import { onMounted, ref } from 'vue';
export function useWidth() {
const width = ref(0);
const resize = () => { width.value = window.innerWidth; };
onMounted(() => { resize(); window.addEventListener('resize', resize); });
return width;
}
Good
onUnmounted removes the exact same resize function registered at mount, ending that instance’s external listener lifetime.
use-width.ts
import { onMounted, onUnmounted, ref } from 'vue';
export function useWidth() {
const width = ref(0);
const resize = () => { width.value = window.innerWidth; };
onMounted(() => { resize(); window.addEventListener('resize', resize); });
onUnmounted(() => { window.removeEventListener('resize', resize); });
return width;
}
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/event-modifier
An event listener uses a modifier the emit does not support.
Default severity: info
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
The experimental Rust CrossFileAnalyzer has a producer for this code. The CLI pass does not emit this individual code; configuring its ID does not enable that Rust pass. These scenarios describe the analyzer's supported graph/facts, not a Vite+ promise.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
.stop assumes a native event's propagation method on the child's custom save event, whose payload need not be a DOM Event.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child @save.stop="() => {}" /></template>
Child.vue
<script setup lang="ts">
const emit = defineEmits<{ save: [] }>();
emit("save");
</script>
<template><p>Content</p></template>
Good
Remove .stop from the custom-event listener; handle native propagation at the actual DOM listener when needed.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child @save="() => {}" /></template>
Child.vue
<script setup lang="ts">
const emit = defineEmits<{ save: [] }>();
emit("save");
</script>
<template><p>Content</p></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/hydration-risk
This code groups several reactivity findings, including a prop copied into a ref. It does not imply that every Date.now() expression is detected by the cross-file pass.
Default severity: error
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "croquis/cf/hydration-risk": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
The child initializes ref(props.count) once, so its local count no longer follows later parent prop changes. This is the current prop-to-ref producer, not a general nondeterministic-SSR example.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child :count="0" /></template>
Child.vue
<script setup lang="ts">
import { ref } from "vue";
const props = defineProps<{ count: number }>();
const count = ref(props.count);
</script>
<template><p>{{ count }}</p></template>
Good
toRef(props, "count") points to the prop instead of copying its initial value into independent state.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child :count="0" /></template>
Child.vue
<script setup lang="ts">
import { toRef } from "vue";
const props = defineProps<{ count: number }>();
const count = toRef(props, "count");
</script>
<template><p>{{ count }}</p></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/inherit-attrs-unused
inheritAttrs: false is set and the component never reads the attributes.
Default severity: warning
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
The experimental Rust CrossFileAnalyzer has a producer for this code. The CLI pass does not emit this individual code; configuring its ID does not enable that Rust pass. These scenarios describe the analyzer's supported graph/facts, not a Vite+ promise.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
The child sets inheritAttrs: false but never forwards the parent's class="notice" attribute.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child class="notice" /></template>
Child.vue
<script setup lang="ts">
defineOptions({ inheritAttrs: false });
</script>
<template><main>Content</main></template>
Good
Keep explicit inheritance control and bind $attrs to the intended <main> target.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child class="notice" /></template>
Child.vue
<script setup lang="ts">
defineOptions({ inheritAttrs: false });
</script>
<template><main v-bind="$attrs">Content</main></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/inject-without-symbol
inject uses a plain key instead of an InjectionKey symbol.
Default severity: warning
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "croquis/cf/inject-without-symbol": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./ThemeProvider.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
keys/theme.ts
import type { InjectionKey, Ref } from "vue";
export interface Theme { color: string; }
export const ThemeKey: InjectionKey<Ref<Theme>> = Symbol("theme");
Bad
The consumer injects the untyped string key "theme", which offers no symbol identity shared with the provider.
ThemeProvider.vue
<script setup lang="ts">
import { provide, ref } from "vue";
import ThemeLabel from "./ThemeLabel.vue";
const theme = ref({ color: "blue" });
provide("theme", theme);
</script>
<template>
<ThemeLabel />
</template>
ThemeLabel.vue
<script setup lang="ts">
import { inject } from "vue";
const theme = inject("theme");
</script>
Good
The consumer and provider import the same ThemeKey instead of duplicating string names.
ThemeProvider.vue
<script setup lang="ts">
import { provide, ref } from "vue";
import ThemeLabel from "./ThemeLabel.vue";
import { ThemeKey } from "./keys/theme";
const theme = ref({ color: "blue" });
provide(ThemeKey, theme);
</script>
<template>
<ThemeLabel />
</template>
ThemeLabel.vue
<script setup lang="ts">
import { inject } from "vue";
import { ThemeKey } from "./keys/theme";
const theme = inject(ThemeKey);
</script>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/injected-async-mutation-race
An injected value is mutated from an async task that can race.
Default severity: error
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "croquis/cf/injected-async-mutation-race": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./StoreProvider.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
api.ts
export async function loadCount(query: string, options?: { signal?: AbortSignal }): Promise<number> {
const response = await fetch(`/count?q=${encodeURIComponent(query)}`, options);
return Number(await response.text());
}
CountSummary.vue
<script setup lang="ts">
import { inject } from "vue";
import { StoreKey } from "./keys/store";
const store = inject(StoreKey)!;
</script>
<template><p>{{ store.count }}</p></template>
Bad
CountLoader.vue writes an awaited result directly into the injected store shared with CountSummary.vue, letting stale work affect both consumers.
keys/store.ts
import type { InjectionKey } from "vue";
export interface Store {
count: number;
}
export const StoreKey: InjectionKey<Store> = Symbol("store");
StoreProvider.vue
<script setup lang="ts">
import { provide, reactive } from "vue";
import CountLoader from "./CountLoader.vue";
import CountSummary from "./CountSummary.vue";
import { StoreKey, type Store } from "./keys/store";
const store = reactive<Store>({ count: 0 });
provide(StoreKey, store);
</script>
<template>
<CountLoader />
<CountSummary />
</template>
CountLoader.vue
<script setup lang="ts">
import { loadCount } from "./api";
import { inject, ref, watch } from "vue";
import { StoreKey } from "./keys/store";
const store = inject(StoreKey)!;
const query = ref("");
watch(query, async (value) => {
store.count = await loadCount(value);
});
</script>
Good
The loader cancels invalidated work and emits only an active result. The provider owns the store mutation through applyLoadedCount.
keys/store.ts
import type { InjectionKey } from "vue";
export interface Store {
count: number;
}
export const StoreKey: InjectionKey<Store> = Symbol("store");
StoreProvider.vue
<script setup lang="ts">
import { provide, reactive } from "vue";
import CountLoader from "./CountLoader.vue";
import CountSummary from "./CountSummary.vue";
import { StoreKey, type Store } from "./keys/store";
const store = reactive<Store>({ count: 0 });
provide(StoreKey, store);
function applyLoadedCount(count: number) {
store.count = count;
}
</script>
<template>
<CountLoader @loaded="applyLoadedCount" />
<CountSummary />
</template>
CountLoader.vue
<script setup lang="ts">
import { loadCount } from "./api";
import { ref, watch } from "vue";
const emit = defineEmits<{ loaded: [count: number] }>();
const query = ref("");
watch(query, async (value, _oldValue, onCleanup) => {
const controller = new AbortController();
let active = true;
onCleanup(() => {
active = false;
controller.abort();
});
const count = await loadCount(value, { signal: controller.signal });
if (active) emit("loaded", count);
});
</script>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/lifecycle-outside-setup
A lifecycle hook is registered outside setup.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
install-title.ts
import { onMounted } from 'vue';
export function installTitle() {
onMounted(() => { document.title = 'Mounted application'; });
}
Bad
The entry calls installTitle() before mounting an app, so onMounted is registered without an active component setup context.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { installTitle } from './install-title';
installTitle();
createApp(App).mount('#app');
App.vue
<script setup lang="ts">
</script>
<template>
<p>Application</p>
</template>
Good
Calling the same helper synchronously from App’s setup attaches the lifecycle callback to that instance’s mount.
App.vue
<script setup lang="ts">
import { installTitle } from './install-title';
installTitle();
</script>
<template>
<p>Application</p>
</template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/lifecycle-without-cleanup
A lifecycle hook starts work and never cleans it up.
Default severity: warning
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
The experimental Rust CrossFileAnalyzer has a producer for this code. The CLI pass does not emit this individual code; configuring its ID does not enable that Rust pass. These scenarios describe the analyzer's supported graph/facts, not a Vite+ promise.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
Mounting registers a window resize listener, but unmounting never removes the same callback.
App.vue
<script setup lang="ts">
import { onMounted } from "vue";
const resize = () => {};
onMounted(() => { window.addEventListener("resize", resize); });
</script>
<template><p>Content</p></template>
Good
onUnmounted removes the listener with the same event name and function identity used by addEventListener.
App.vue
<script setup lang="ts">
import { onMounted, onUnmounted } from "vue";
const resize = () => {};
onMounted(() => { window.addEventListener("resize", resize); });
onUnmounted(() => { window.removeEventListener("resize", resize); });
</script>
<template><p>Content</p></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/missing-required-prop
A required prop is not passed.
Default severity: error
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
The experimental Rust CrossFileAnalyzer has a producer for this code. The CLI pass does not emit this individual code; configuring its ID does not enable that Rust pass. These scenarios describe the analyzer's supported graph/facts, not a Vite+ promise.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
The parent renders <Child /> without the child's required title: string prop.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child /></template>
Child.vue
<script setup lang="ts">
const props = defineProps<{ title: string }>();
</script>
<template><p>{{ props.title }}</p></template>
Good
title="Hello" supplies the required prop declared by the resolved child.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child title="Hello" /></template>
Child.vue
<script setup lang="ts">
const props = defineProps<{ title: string }>();
</script>
<template><p>{{ props.title }}</p></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/missing-suspense
An async dependency is used outside a Suspense boundary.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
AsyncCard.vue
<script setup lang="ts">
const message = await Promise.resolve('Ready');
</script>
<template>
<p>{{ message }}</p>
</template>
Bad
AsyncCard has top-level await, making its setup asynchronous, but App renders it without a Suspense boundary to coordinate that dependency.
App.vue
<script setup lang="ts">
import AsyncCard from './AsyncCard.vue';
</script>
<template>
<AsyncCard />
</template>
Good
App wraps the async child in Suspense and supplies a loading fallback until the child setup resolves.
App.vue
<script setup lang="ts">
import { Suspense } from 'vue';
import AsyncCard from './AsyncCard.vue';
</script>
<template>
<Suspense><AsyncCard /><template #fallback><p>Loading…</p></template></Suspense>
</template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/module-scope-reactive
Reactive state is created at module scope and shared by every caller.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
Module-scope reactive state is legal for intentional application stores. This example assumes component/request isolation; the published contract currently has no producer.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
App.vue
<script setup lang="ts">
import Counter from './Counter.vue';
</script>
<template>
<Counter /><Counter />
</template>
Counter.vue
<script setup lang="ts">
import { createCounter } from './counter';
const { count } = createCounter();
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
Bad
The module initializes count once, and both Counter instances receive the same ref. Clicking one changes both counters even though this example intends independent instance state.
counter.ts
import { ref } from 'vue';
const count = ref(0);
export function createCounter() { return { count }; }
Good
Creating the ref inside createCounter gives each synchronous setup call a separate state object, so each button owns its counter.
counter.ts
import { ref } from 'vue';
export function createCounter() {
const count = ref(0);
return { count };
}
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/multi-root-attrs
A multi-root component receives attributes and has nowhere to put them.
Default severity: warning
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
The experimental Rust CrossFileAnalyzer has a producer for this code. The CLI pass does not emit this individual code; configuring its ID does not enable that Rust pass. These scenarios describe the analyzer's supported graph/facts, not a Vite+ promise.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
The child has <main> and <aside> roots, so Vue has no single root that can automatically receive the parent's class.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child class="notice" /></template>
Child.vue
<template><main>Content</main><aside>Help</aside></template>
Good
Forward $attrs explicitly to <main> while keeping the second root.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child class="notice" /></template>
Child.vue
<template><main v-bind="$attrs">Content</main><aside>Help</aside></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/mutated-after-escape
A reactive object is mutated after it has escaped its owner.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
This is an explicit immutable-history ownership policy, not a general prohibition on passing or later mutating reactive objects. No current producer emits this contract.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
archive.ts
export interface Profile { name: string }
const records: Readonly<Profile>[] = [];
export function publish(profile: Readonly<Profile>): void { records.push(profile); }
export function latestName(): string { return records.at(-1)?.name ?? ''; }
App.vue
<script setup lang="ts">
import { publishProfile } from './profile';
import { latestName } from './archive';
publishProfile();
const archivedName = latestName();
</script>
<template>
<p>Archived name: {{ archivedName }}</p>
</template>
Bad
The archive retains the same object passed to publish. The owner then changes its name, retroactively changing the supposedly historical record to Grace. TypeScript’s Readonly parameter does not copy the object.
profile.ts
import { reactive } from 'vue';
import { publish } from './archive';
export function publishProfile(): void {
const profile = reactive({ name: 'Ada' });
publish(profile);
profile.name = 'Grace';
}
Good
Publishing a plain copy separates the archived Ada record from later edits of the reactive profile. The archive’s snapshot policy is now maintained.
profile.ts
import { reactive } from 'vue';
import { publish } from './archive';
export function publishProfile(): void {
const profile = reactive({ name: 'Ada' });
publish({ ...profile });
profile.name = 'Grace';
}
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/non-reactive-provide
A provided value is not reactive, so descendants will not see updates.
Default severity: context-dependent
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "croquis/cf/non-reactive-provide": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./ThemeProvider.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
ThemeProvider.vue provides a plain object. Mutating that object's fields does not give the injected consumer a Vue reactive dependency.
keys/theme.ts
export const ThemeKey = Symbol("theme");
ThemeProvider.vue
<script setup lang="ts">
import { provide } from "vue";
import ThemeLabel from "./ThemeLabel.vue";
import { ThemeKey } from "./keys/theme";
const theme = { color: "blue" };
provide(ThemeKey, theme);
</script>
<template>
<ThemeLabel />
</template>
ThemeLabel.vue
<script setup lang="ts">
import { inject } from "vue";
import { ThemeKey } from "./keys/theme";
const theme = inject(ThemeKey);
</script>
Good
The provider wraps the theme in ref; the same injected reference can track later changes.
keys/theme.ts
export const ThemeKey = Symbol("theme");
ThemeProvider.vue
<script setup lang="ts">
import { provide, ref } from "vue";
import ThemeLabel from "./ThemeLabel.vue";
import { ThemeKey } from "./keys/theme";
const theme = ref({ color: "blue" });
provide(ThemeKey, theme);
</script>
<template>
<ThemeLabel />
</template>
ThemeLabel.vue
<script setup lang="ts">
import { inject } from "vue";
import { ThemeKey } from "./keys/theme";
const theme = inject(ThemeKey);
</script>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/non-unique-id
An element id inside a loop is not unique per item.
Default severity: error
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "croquis/cf/non-unique-id": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./ResultsList.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
Every v-for iteration renders the same literal result-title ID; the loop's key does not make DOM IDs unique.
ResultsList.vue
<script setup lang="ts">
const results = [{ id: "first", title: "First result" }, { id: "second", title: "Second result" }];
</script>
<template>
<article v-for="result in results" :key="result.id">
<h2 id="result-title">{{ result.title }}</h2>
</article>
</template>
Good
The heading ID includes the result's stable ID, producing a distinct document identifier for each item.
ResultsList.vue
<script setup lang="ts">
const results = [{ id: "first", title: "First result" }, { id: "second", title: "Second result" }];
</script>
<template>
<article v-for="result in results" :key="result.id">
<h2 :id="`result-${result.id}-title`">{{ result.title }}</h2>
</article>
</template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/object-identity-comparison
A raw object and its reactive proxy have distinct object identities, and toRaw() returns the original object. This contract illustrates comparing stable record IDs when record identity is intended.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
The example assumes IDs uniquely identify records. Comparing two references to the same reactive proxy remains valid; this contract has no current producer.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
user.ts
import { reactive } from 'vue';
export function makeUser() {
const raw = { id: 7, name: 'Ada' };
return { raw, proxy: reactive(raw) };
}
Bad
proxy === raw compares wrapper identity, so it is false even though both represent the same user record. The application intended record identity, not object-wrapper identity.
App.vue
<script setup lang="ts">
import { makeUser } from './user';
const { raw, proxy } = makeUser();
const sameRecord = proxy === raw;
</script>
<template>
<p>Same record: {{ sameRecord }}</p>
</template>
Good
Comparing the stable record id answers the intended question without depending on whether the object is raw or proxied.
App.vue
<script setup lang="ts">
import { makeUser } from './user';
const { raw, proxy } = makeUser();
const sameRecord = proxy.id === raw.id;
</script>
<template>
<p>Same record: {{ sameRecord }}</p>
</template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/pinia-getter
This contract illustrates copying a Pinia getter's current primitive value during setup, leaving a snapshot that no longer follows updates. Direct getter reads within a tracked computation or template remain reactive; storeToRefs() is the extraction repair shown here.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
Pinia must be installed, and main.ts installs its plugin before mounting. Reading store.doubled directly inside a tracked computation or template is valid; the defect here is taking a plain snapshot. This contract currently has no producer.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
createApp(App).use(createPinia()).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
counter-store.ts
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: { doubled: state => state.count * 2 },
});
Bad
const doubled = store.doubled copies the getter’s current number during setup. The copied number does not follow later store.count updates.
App.vue
<script setup lang="ts">
import { useCounterStore } from './counter-store';
const store = useCounterStore();
const doubled = store.doubled;
</script>
<template>
<button @click="store.count++">{{ store.count }}</button><p>{{ doubled }}</p>
</template>
Good
storeToRefs(store) supplies a reactive getter ref that can be destructured and unwrapped by the template while staying connected to the store.
App.vue
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useCounterStore } from './counter-store';
const store = useCounterStore();
const { doubled } = storeToRefs(store);
</script>
<template>
<button @click="store.count++">{{ store.count }}</button><p>{{ doubled }}</p>
</template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/prop-type-mismatch
A passed prop value does not match the declared type.
Default severity: error
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
The experimental Rust CrossFileAnalyzer has a producer for this code. The CLI pass does not emit this individual code; configuring its ID does not enable that Rust pass. These scenarios describe the analyzer's supported graph/facts, not a Vite+ promise.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
The parent passes the numeric expression 42 to the resolved child's title: string prop.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child :title="42" /></template>
Child.vue
<script setup lang="ts">
const props = defineProps<{ title: string }>();
</script>
<template><p>{{ props.title }}</p></template>
Good
The literal title="Hello" supplies a string matching the child's declaration.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child title="Hello" /></template>
Child.vue
<script setup lang="ts">
const props = defineProps<{ title: string }>();
</script>
<template><p>{{ props.title }}</p></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/provide-inject-type
A provided value and its inject do not have the same type.
Default severity: warning
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "croquis/cf/provide-inject-type": "warn" },
},
},
});
vp run lint
This check compares explicit provider/consumer type annotations, not inferred literal value types. Keep the provider's as string annotation in this example.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
The provider explicitly annotates title as string, while the descendant requests inject<number> for the same key.
App.vue
<script setup lang="ts">
import { provide } from "vue";
import Child from "./Child.vue";
provide("title", "Hello" as string);
</script>
<template><Child /></template>
Child.vue
<script setup lang="ts">
import { inject } from "vue";
const title = inject<number>("title");
</script>
<template><p>Content</p></template>
Good
The consumer's explicit inject<string> agrees with the provider annotation. Keep as string: this producer compares explicit annotations, not inferred literal types.
App.vue
<script setup lang="ts">
import { provide } from "vue";
import Child from "./Child.vue";
provide("title", "Hello" as string);
</script>
<template><Child /></template>
Child.vue
<script setup lang="ts">
import { inject } from "vue";
const title = inject<string>("title");
</script>
<template><p>Content</p></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/provide-without-symbol
provide uses a plain key instead of an InjectionKey symbol.
Default severity: warning
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "croquis/cf/provide-without-symbol": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./ThemeProvider.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
Both components use the string "theme"; unrelated features can accidentally reuse that key.
ThemeProvider.vue
<script setup lang="ts">
import { provide, ref } from "vue";
import ThemeLabel from "./ThemeLabel.vue";
const theme = ref({ color: "blue" });
provide("theme", theme);
</script>
<template>
<ThemeLabel />
</template>
ThemeLabel.vue
<script setup lang="ts">
import { inject } from "vue";
const theme = inject("theme");
</script>
Good
Export one typed ThemeKey symbol and import that same value at both provide and inject sites. Creating separate symbols with the same description would not connect them.
ThemeProvider.vue
<script setup lang="ts">
import { provide, ref } from "vue";
import ThemeLabel from "./ThemeLabel.vue";
import { ThemeKey, type Theme } from "./keys/theme";
const theme = ref<Theme>({ color: "blue" });
provide(ThemeKey, theme);
</script>
<template>
<ThemeLabel />
</template>
ThemeLabel.vue
<script setup lang="ts">
import { inject } from "vue";
import { ThemeKey } from "./keys/theme";
const theme = inject(ThemeKey);
</script>
keys/theme.ts
import type { InjectionKey, Ref } from "vue";
export interface Theme {
color: string;
}
export const ThemeKey: InjectionKey<Ref<Theme>> = Symbol("theme");
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/reactive-export
Reactive state is exported from the module.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
Intentional shared application stores may export reactive state. This scenario requires isolated state and does not claim every reactive export is invalid. No current producer emits this contract.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
Bad
The module exports one initialized reactive object, so every importer receives the same count. In an SSR module shared between requests, this defeats the example’s per-instance/request state isolation.
state.ts
import { reactive } from 'vue';
export const state = reactive({ count: 0 });
App.vue
<script setup lang="ts">
import { state } from './state';
</script>
<template>
<button @click="state.count++">{{ state.count }}</button>
</template>
Good
The module exports a factory, and App invokes it inside setup. Each instance obtains a fresh reactive count rather than the exported singleton.
state.ts
import { reactive } from 'vue';
export function createState() { return reactive({ count: 0 }); }
App.vue
<script setup lang="ts">
import { createState } from './state';
const state = createState();
</script>
<template>
<button @click="state.count++">{{ state.count }}</button>
</template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/reactivity-outside-setup
A reactive API is called outside setup.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
Vue permits ref/reactive/computed outside component setup. The risk here is unwanted ownership/sharing under an explicit instance-isolation policy, not API illegality. This contract has no current producer.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
App.vue
<script setup lang="ts">
import Counter from './Counter.vue';
</script>
<template>
<Counter /><Counter />
</template>
Counter.vue
<script setup lang="ts">
import { useCounter } from './use-counter';
const { count, doubled } = useCounter();
</script>
<template>
<button @click="count++">{{ count }}</button><p>{{ doubled }}</p>
</template>
Bad
Both reactive APIs run while the module loads. The two Counter instances therefore share one ref and computed value despite the intended independent counters.
use-counter.ts
import { computed, ref } from 'vue';
const count = ref(0);
const doubled = computed(() => count.value * 2);
export function useCounter() { return { count, doubled }; }
Good
useCounter creates the ref and computed synchronously inside each component setup call, giving each widget its own state and tracked derivation.
use-counter.ts
import { computed, ref } from 'vue';
export function useCounter() {
const count = ref(0);
const doubled = computed(() => count.value * 2);
return { count, doubled };
}
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/reassignment-breaks-reactivity
Reassigning a reactive binding replaces it with a plain value.
Default severity: error
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "croquis/cf/reassignment-breaks-reactivity": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./UserPage.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
The child creates a prop ref and then overwrites the variable with props.user, discarding that ref connection.
UserPage.vue
<script setup lang="ts">
import { reactive } from "vue";
import UserSummary from "./UserSummary.vue";
const user = reactive({ name: "Ada" });
</script>
<template>
<UserSummary :user="user" />
</template>
UserSummary.vue
<script setup lang="ts">
import { toRef } from "vue";
const props = defineProps<{ user: { name: string } }>();
let user = toRef(props, "user");
user = props.user;
</script>
Good
Keep the toRef in a const binding and remove the reassignment that replaces it.
UserPage.vue
<script setup lang="ts">
import { reactive } from "vue";
import UserSummary from "./UserSummary.vue";
const user = reactive({ name: "Ada" });
</script>
<template>
<UserSummary :user="user" />
</template>
UserSummary.vue
<script setup lang="ts">
import { toRef } from "vue";
const props = defineProps<{ user: { name: string } }>();
const user = toRef(props, "user");
</script>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/reference-escapes-scope
A reactive reference escapes the scope that owns its lifetime.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
Refs may legitimately be returned from composables or shared across scopes. This example explicitly requires a snapshot cache; it does not claim that unmount invalidates a ref. No current producer emits this contract.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
saved.ts
import type { Ref } from 'vue';
let saved: Ref<number> | number | undefined;
export function remember(value: Ref<number> | number): void { saved = value; }
export function remembered(): Ref<number> | number | undefined { return saved; }
Bad
The process-level cache retains the component’s live count ref. It can keep that instance state reachable after unmount and can observe later edits, although this cache is intended to store a snapshot.
App.vue
<script setup lang="ts">
import { ref } from 'vue';
import { remember } from './saved';
const count = ref(0);
remember(count);
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
Good
The cache receives the current plain number, so it keeps a snapshot without retaining the component-owned ref.
App.vue
<script setup lang="ts">
import { ref } from 'vue';
import { remember } from './saved';
const count = ref(0);
remember(count.value);
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/setup-context-violation
This experimental analyzer scenario places ref(0) at normal script module scope rather than per-instance setup. Vue permits this reactive state; the example requires each component instance to own its own count. The CLI does not emit this individual code.
Default severity: context-dependent
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
The experimental Rust CrossFileAnalyzer has a producer for this code. The CLI pass does not emit this individual code; configuring its ID does not enable that Rust pass. These scenarios describe the analyzer's supported graph/facts, not a Vite+ promise.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
ref(0) is created at normal script module scope, outside the per-instance setup context represented by this analyzer scenario.
App.vue
<script lang="ts">
import { ref } from "vue";
const count = ref(0);
export default {};
</script>
<template><p>Count</p></template>
Good
Move the binding into script setup, where each component instance owns its count and the template can read it.
App.vue
<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
</script>
<template><p>{{ count }}</p></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/shallow-deep-access
A deep property of a shallowReactive or shallowRef value is read as if it were tracked.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
App.vue
<script setup lang="ts">
import { makeProfile } from './profile';
const profile = makeProfile();
</script>
<template>
<p>{{ profile.user.name }}</p><button @click="profile.user.name = 'Grace'">Rename</button>
</template>
Bad
shallowReactive tracks the root user property but leaves the nested object raw. Changing profile.user.name does not notify the template as a tracked deep mutation.
profile.ts
import { shallowReactive } from 'vue';
export function makeProfile() { return shallowReactive({ user: { name: 'Ada' } }); }
Good
Deep reactive wraps the nested user object, so the same name assignment can trigger the displayed name’s update.
profile.ts
import { reactive } from 'vue';
export function makeProfile() { return reactive({ user: { name: 'Ada' } }); }
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/spread-breaks-reactivity
Spreading a reactive object copies its values and drops the tracking.
Default severity: error
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "croquis/cf/spread-breaks-reactivity": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./UserPage.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
UserSummary.vue spreads props.user into a new object, taking a snapshot of the incoming reactive data.
UserPage.vue
<script setup lang="ts">
import { reactive } from "vue";
import UserSummary from "./UserSummary.vue";
const user = reactive({ name: "Ada", role: "admin" });
</script>
<template>
<UserSummary :user="user" />
</template>
UserSummary.vue
<script setup lang="ts">
const props = defineProps<{ user: { name: string; role: string } }>();
const copiedUser = { ...props.user };
</script>
Good
toRef(props, "user") keeps a reference to the incoming prop instead of copying its fields.
UserPage.vue
<script setup lang="ts">
import { reactive } from "vue";
import UserSummary from "./UserSummary.vue";
const user = reactive({ name: "Ada", role: "admin" });
</script>
<template>
<UserSummary :user="user" />
</template>
UserSummary.vue
<script setup lang="ts">
import { toRef } from "vue";
const props = defineProps<{ user: { name: string; role: string } }>();
const user = toRef(props, "user");
</script>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/suspense-no-fallback
<Suspense> has no fallback content.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
Suspense without a fallback is valid Vue syntax. This is a chosen loading-UI convention, not a compiler error; the published contract has no current producer.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
AsyncCard.vue
<script setup lang="ts">
const message = await Promise.resolve('Ready');
</script>
<template>
<p>{{ message }}</p>
</template>
Bad
The Suspense boundary has an async child but no fallback content, leaving no loading content for this example’s pending state.
App.vue
<script setup lang="ts">
import { Suspense } from 'vue';
import AsyncCard from './AsyncCard.vue';
</script>
<template>
<Suspense><AsyncCard /></Suspense>
</template>
Good
The #fallback slot supplies an explicit loading paragraph until the async child resolves.
App.vue
<script setup lang="ts">
import { Suspense } from 'vue';
import AsyncCard from './AsyncCard.vue';
</script>
<template>
<Suspense><AsyncCard /><template #fallback><p>Loading…</p></template></Suspense>
</template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/template-ref-timing
A template ref is read before the component is mounted.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
focus-input.ts
export function focusInput(input: HTMLInputElement | null): void { input?.focus(); }
Bad
Setup reads the template ref before mounting, when its value is still null. The optional focus call therefore performs no focus action.
App.vue
<script setup lang="ts">
import { ref } from 'vue';
import { focusInput } from './focus-input';
const input = ref<HTMLInputElement | null>(null);
focusInput(input.value);
</script>
<template>
<input ref="input" aria-label="Name" />
</template>
Good
onMounted defers the read until Vue has assigned the input element to the template ref, allowing the focus helper to act on it.
App.vue
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { focusInput } from './focus-input';
const input = ref<HTMLInputElement | null>(null);
onMounted(() => { focusInput(input.value); });
</script>
<template>
<input ref="input" aria-label="Name" />
</template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/toraw-mutation
toRaw is used and the raw object is then mutated.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
App.vue
<script setup lang="ts">
import { makeProfile, rename } from './profile';
const profile = makeProfile();
</script>
<template>
<p>{{ profile.name }}</p><button @click="rename(profile)">Rename</button>
</template>
Bad
rename obtains the raw target and writes raw.name, bypassing the proxy setter that would notify the displayed reactive name.
profile.ts
import { reactive, toRaw } from 'vue';
export function makeProfile() { return reactive({ name: 'Ada' }); }
export function rename(profile: { name: string }): void {
const raw = toRaw(profile);
raw.name = 'Grace';
}
Good
Writing profile.name through the passed reactive proxy preserves the same rename while notifying its dependents.
profile.ts
import { reactive } from 'vue';
export function makeProfile() { return reactive({ name: 'Ada' }); }
export function rename(profile: { name: string }): void {
profile.name = 'Grace';
}
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/uncaught-error
A component can throw and no error boundary catches it.
Default severity: info
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "croquis/cf/uncaught-error": "warn" },
},
},
});
vp run lint
The current producer scans template expressions such as JSON.parse(input). It does not report a throw statement that exists only in the script block.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
The child's template calls JSON.parse on malformed input, and the reachable parent has no error-capture boundary.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child /></template>
Child.vue
<script setup lang="ts">
const input = "{";
</script>
<template><button @click="JSON.parse(input)">Parse</button></template>
Good
The parent registers onErrorCaptured around that child. Returning false stops propagation; a production boundary should also present useful recovery UI.
App.vue
<script setup lang="ts">
import { onErrorCaptured } from "vue";
import Child from "./Child.vue";
onErrorCaptured(() => false);
</script>
<template><Child /></template>
Child.vue
<script setup lang="ts">
const input = "{";
</script>
<template><button @click="JSON.parse(input)">Parse</button></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/undeclared-emit
The component emits an event that is not declared.
Default severity: error
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
The experimental Rust CrossFileAnalyzer has a producer for this code. The CLI pass does not emit this individual code; configuring its ID does not enable that Rust pass. These scenarios describe the analyzer's supported graph/facts, not a Vite+ promise.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
The child calls emit("save") but its defineEmits contract declares only cancel.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child /></template>
Child.vue
<script setup lang="ts">
const emit = defineEmits<{ cancel: [] }>();
emit("save");
</script>
<template><p>Content</p></template>
Good
Declare save with its empty argument tuple so the emitted event agrees with the component contract.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child /></template>
Child.vue
<script setup lang="ts">
const emit = defineEmits<{ save: [] }>();
emit("save");
</script>
<template><p>Content</p></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/undeclared-prop
A parent passes a prop the child does not declare.
Default severity: warning
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
The experimental Rust CrossFileAnalyzer has a producer for this code. The CLI pass does not emit this individual code; configuring its ID does not enable that Rust pass. These scenarios describe the analyzer's supported graph/facts, not a Vite+ promise.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
The parent passes typo even though the resolved child declares only title. This analyzer convention is separate from Vue's general fallthrough-attribute behavior.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child title="Hello" :typo="true" /></template>
Child.vue
<script setup lang="ts">
const props = defineProps<{ title: string }>();
</script>
<template><p>{{ props.title }}</p></template>
Good
Remove the unintended typo binding and retain the declared title prop.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child title="Hello" /></template>
Child.vue
<script setup lang="ts">
const props = defineProps<{ title: string }>();
</script>
<template><p>{{ props.title }}</p></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/undefined-slot
A parent fills a slot the child does not expose.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
Card.vue
<script setup lang="ts">
defineSlots<{ header(): unknown }>();
</script>
<template>
<article><header><slot name="header" /></header></article>
</template>
Bad
App supplies a footer slot, but Card declares and renders only header. The supplied Notice content has no matching slot outlet in this child.
App.vue
<script setup lang="ts">
import Card from './Card.vue';
</script>
<template>
<Card><template #footer>Notice</template></Card>
</template>
Good
App supplies header, matching both the child’s typed slot declaration and its rendered outlet, so Notice appears there.
App.vue
<script setup lang="ts">
import Card from './Card.vue';
</script>
<template>
<Card><template #header>Notice</template></Card>
</template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/unhandled-event
A child emits an event that no parent handles.
Default severity: info
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
The experimental Rust CrossFileAnalyzer has a producer for this code. The CLI pass does not emit this individual code; configuring its ID does not enable that Rust pass. These scenarios describe the analyzer's supported graph/facts, not a Vite+ promise.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
Child.vue emits save, but its immediate wrapper does not listen for it; component events do not automatically bubble through wrappers.
App.vue
<script setup lang="ts">
import Wrapper from "./Wrapper.vue";
</script>
<template><Wrapper /></template>
Wrapper.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child /></template>
Child.vue
<script setup lang="ts">
const emit = defineEmits<{ save: [] }>();
emit("save");
</script>
<template><p>Content</p></template>
Good
Wrapper.vue attaches a save listener to its direct child. The empty callback demonstrates handling for this rule, not a complete save implementation.
App.vue
<script setup lang="ts">
import Wrapper from "./Wrapper.vue";
</script>
<template><Wrapper /></template>
Wrapper.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child @save="() => {}" /></template>
Child.vue
<script setup lang="ts">
const emit = defineEmits<{ save: [] }>();
emit("save");
</script>
<template><p>Content</p></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/unmatched-inject
inject names a key that no ancestor provides.
Default severity: error / warning (with default)
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "croquis/cf/unmatched-inject": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
ThemeLabel.vue injects ThemeKey, but its reachable App.vue ancestor never provides that key.
keys/theme.ts
import type { InjectionKey, Ref } from "vue";
export interface Theme {
color: string;
}
export const ThemeKey: InjectionKey<Ref<Theme>> = Symbol("theme");
App.vue
<script setup lang="ts">
import ThemeLabel from "./ThemeLabel.vue";
</script>
<template>
<ThemeLabel />
</template>
ThemeLabel.vue
<script setup lang="ts">
import { inject } from "vue";
import { ThemeKey } from "./keys/theme";
const theme = inject(ThemeKey);
</script>
Good
App.vue provides a reactive theme using the same exported ThemeKey, before rendering the descendant that injects it.
keys/theme.ts
import type { InjectionKey, Ref } from "vue";
export interface Theme {
color: string;
}
export const ThemeKey: InjectionKey<Ref<Theme>> = Symbol("theme");
App.vue
<script setup lang="ts">
import { provide, ref } from "vue";
import ThemeLabel from "./ThemeLabel.vue";
import { ThemeKey, type Theme } from "./keys/theme";
const theme = ref<Theme>({ color: "blue" });
provide(ThemeKey, theme);
</script>
<template>
<ThemeLabel />
</template>
ThemeLabel.vue
<script setup lang="ts">
import { inject } from "vue";
import { ThemeKey } from "./keys/theme";
const theme = inject(ThemeKey);
</script>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/unmatched-listener
A parent listens for an event the child does not emit.
Default severity: warning
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
The experimental Rust CrossFileAnalyzer has a producer for this code. The CLI pass does not emit this individual code; configuring its ID does not enable that Rust pass. These scenarios describe the analyzer's supported graph/facts, not a Vite+ promise.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
The parent listens for save, while the resolved child declares only cancel.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child @save="() => {}" /></template>
Child.vue
<script setup lang="ts">
const emit = defineEmits<{ cancel: [] }>();
</script>
<template><p>Content</p></template>
Good
The child declares and emits save, matching the parent's listener name.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child @save="() => {}" /></template>
Child.vue
<script setup lang="ts">
const emit = defineEmits<{ save: [] }>();
emit("save");
</script>
<template><p>Content</p></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/unregistered-component
A template uses a component that is not registered or imported.
Default severity: error
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
The experimental Rust CrossFileAnalyzer has a producer for this code. The CLI pass does not emit this individual code; configuring its ID does not enable that Rust pass. These scenarios describe the analyzer's supported graph/facts, not a Vite+ promise.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
A Child.vue file exists, but the parent neither imports nor otherwise registers Child for its template.
App.vue
<template><Child /></template>
Child.vue
<template><p>Child</p></template>
Good
Import Child in the parent's script setup so the template resolves the component binding.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child /></template>
Child.vue
<template><p>Child</p></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/unresolved-import
An import does not resolve to a module.
Default severity: error
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
The experimental Rust CrossFileAnalyzer has a producer for this code. The CLI pass does not emit this individual code; configuring its ID does not enable that Rust pass. These scenarios describe the analyzer's supported graph/facts, not a Vite+ promise.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
The parent imports ./Missing.vue, but the project contains Child.vue rather than that path.
App.vue
<script setup lang="ts">
import Child from "./Missing.vue";
</script>
<template><Child /></template>
Child.vue
<template><p>Child</p></template>
Good
Point the import at the existing ./Child.vue file, retaining the same template binding.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child /></template>
Child.vue
<template><p>Child</p></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/unused-attrs
Fallthrough attributes are passed to a multi-root component that does not use them.
Default severity: info
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
The experimental Rust CrossFileAnalyzer has a producer for this code. The CLI pass does not emit this individual code; configuring its ID does not enable that Rust pass. These scenarios describe the analyzer's supported graph/facts, not a Vite+ promise.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
The parent's tracking-code is neither consumed as a prop nor forwarded by the multi-root child.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child tracking-code="notice" /></template>
Child.vue
<template><main>Content</main><aside>Help</aside></template>
Good
Binding $attrs on <main> gives that fallthrough attribute an explicit destination.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child tracking-code="notice" /></template>
Child.vue
<template><main v-bind="$attrs">Content</main><aside>Help</aside></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/unused-emit
A declared emit is never used.
Default severity: warning
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
The experimental Rust CrossFileAnalyzer has a producer for this code. The CLI pass does not emit this individual code; configuring its ID does not enable that Rust pass. These scenarios describe the analyzer's supported graph/facts, not a Vite+ promise.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
The child declares save but never calls the emitted-event function with that name.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child /></template>
Child.vue
<script setup lang="ts">
const emit = defineEmits<{ save: [] }>();
</script>
<template><p>Content</p></template>
Good
The example calls emit("save"), making the declared event used. Real interactions should emit it when the corresponding action occurs.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child /></template>
Child.vue
<script setup lang="ts">
const emit = defineEmits<{ save: [] }>();
emit("save");
</script>
<template><p>Content</p></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/unused-provide
A provided key is never injected.
Default severity: warning
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "croquis/cf/unused-provide": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
keys/theme.ts
import type { InjectionKey, Ref } from "vue";
export interface Theme { color: string; }
export const ThemeKey: InjectionKey<Ref<Theme>> = Symbol("theme");
Bad
App.vue provides ThemeKey, but its rendered Dashboard.vue subtree has no consumer of that key.
App.vue
<script setup lang="ts">
import { provide, ref } from "vue";
import Dashboard from "./Dashboard.vue";
import { ThemeKey, type Theme } from "./keys/theme";
const theme = ref<Theme>({ color: "blue" });
provide(ThemeKey, theme);
</script>
<template>
<Dashboard />
</template>
Dashboard.vue
<template>
<h1>Dashboard</h1>
</template>
Good
The dashboard now renders ThemeLabel.vue, which injects the ancestor's exact ThemeKey identity.
App.vue
<script setup lang="ts">
import { provide, ref } from "vue";
import Dashboard from "./Dashboard.vue";
import { ThemeKey, type Theme } from "./keys/theme";
const theme = ref<Theme>({ color: "blue" });
provide(ThemeKey, theme);
</script>
<template>
<Dashboard />
</template>
Dashboard.vue
<script setup lang="ts">
import ThemeLabel from "./ThemeLabel.vue";
</script>
<template>
<ThemeLabel />
</template>
ThemeLabel.vue
<script setup lang="ts">
import { inject } from "vue";
import { ThemeKey } from "./keys/theme";
const theme = inject(ThemeKey);
</script>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/value-extraction-breaks-reactivity
Reading a reactive value out into a local drops later updates.
Default severity: error
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "croquis/cf/value-extraction-breaks-reactivity": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./UserPage.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
Vue 3.5's reactive destructured item is read into itemSnapshot once; later prop replacement does not update that snapshot.
UserPage.vue
<script setup lang="ts">
import { reactive } from "vue";
import UserSummary from "./UserSummary.vue";
const user = reactive({ name: "Ada" });
</script>
<template>
<UserSummary :item="user" />
</template>
UserSummary.vue
<script setup lang="ts">
const { item } = defineProps<{ item: { name: string } }>();
const itemSnapshot = item;
</script>
Good
Read item inside computed, so Vue's reactive props-destructuring transform can track each evaluation.
UserPage.vue
<script setup lang="ts">
import { reactive } from "vue";
import UserSummary from "./UserSummary.vue";
const user = reactive({ name: "Ada" });
</script>
<template>
<UserSummary :item="user" />
</template>
UserSummary.vue
<script setup lang="ts">
import { computed } from "vue";
const { item } = defineProps<{ item: { name: string } }>();
const itemView = computed(() => item);
</script>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/watch-can-be-computed
A watcher only copies a value into state and can be a computed.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
This illustrates the published preference for purely derived state. Watchers remain appropriate for external effects or independently writable state; no current producer emits this contract.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
App.vue
<script setup lang="ts">
import { useDouble } from './use-double';
const { count, doubled } = useDouble();
</script>
<template>
<button @click="count++">{{ count }}</button><p>{{ doubled }}</p>
</template>
Bad
The watcher performs no external effect; it only keeps a second writable ref synchronized with twice count. This example has no independent writes to that derived value.
use-double.ts
import { ref, watch } from 'vue';
export function useDouble() {
const count = ref(0);
const doubled = ref(0);
watch(count, next => { doubled.value = next * 2; }, { immediate: true });
return { count, doubled };
}
Good
A computed getter expresses the same derivation directly and removes the manual synchronization and extra writable state.
use-double.ts
import { computed, ref } from 'vue';
export function useDouble() {
const count = ref(0);
const doubled = computed(() => count.value * 2);
return { count, doubled };
}
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/watcheffect-async
The async watchEffect shown here has no invalidation cleanup or stale-response guard. Vue supports cleanup for watchEffect; the Good example uses an explicit watch source and cancels invalidated work.
Default severity: error
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "croquis/cf/watcheffect-async": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./SearchPage.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
api.ts
export interface Result { items: string[]; }
export async function load(query: string, options?: { signal?: AbortSignal }): Promise<Result> {
const response = await fetch(`/search?q=${encodeURIComponent(query)}`, options);
return response.json();
}
Bad
The async watchEffect mixes implicit dependency collection with an awaited request and no invalidation guard.
SearchPage.vue
<script setup lang="ts">
import { ref } from "vue";
import SearchResults from "./SearchResults.vue";
const query = ref("");
</script>
<template>
<SearchResults :query="query" />
</template>
SearchResults.vue
<script setup lang="ts">
import { load, type Result } from "./api";
import { ref, watchEffect } from "vue";
const props = defineProps<{ query: string }>();
const result = ref<Result | null>(null);
watchEffect(async () => {
result.value = await load(props.query);
});
</script>
Good
An explicit watch(() => props.query, ...) declares the source, registers request cleanup, and refuses a stale response after invalidation.
SearchPage.vue
<script setup lang="ts">
import { ref } from "vue";
import SearchResults from "./SearchResults.vue";
const query = ref("");
</script>
<template>
<SearchResults :query="query" />
</template>
SearchResults.vue
<script setup lang="ts">
import { load, type Result } from "./api";
import { ref, watch } from "vue";
const props = defineProps<{ query: string }>();
const result = ref<Result | null>(null);
watch(
() => props.query,
async (value, _oldValue, onCleanup) => {
const controller = new AbortController();
let active = true;
onCleanup(() => {
active = false;
controller.abort();
});
const next = await load(value, { signal: controller.signal });
if (active) result.value = next;
},
);
</script>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vize:croquis/cf/watcher-outside-setup
watch or watchEffect is called outside setup.
Default severity: Not emitted
Applies to: Analyzed component graph and the supported facts described below
Automatic fix: None; review related files and apply the repair
Options: No per-code options; supported CLI findings accept severity overrides
This is a published diagnostic contract without a current producer. The Bad/Good scenario below explains the risk and repair; no flag currently makes this code trigger.
Module-scope watchers are valid when their owner keeps and calls a stop handle or intentionally gives them application lifetime. This example requires component-owned lifetimes; the contract has no current producer.
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
index.html
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><title>Contract scenario</title></head>
<body><div id="app"></div><script type="module" src="/main.ts"></script></body></html>
App.vue
<script setup lang="ts">
import Observer from './Observer.vue';
</script>
<template>
<Observer /><Observer />
</template>
Observer.vue
<script setup lang="ts">
import { useObserver } from './use-observer';
const { count, observed } = useObserver();
</script>
<template>
<button @click="count++">{{ count }}</button><p>{{ observed }}</p>
</template>
Bad
The watcher is created at module load, outside either Observer’s setup, and both instances share its refs. It is not automatically stopped when a particular Observer unmounts.
use-observer.ts
import { ref, watch } from 'vue';
const count = ref(0);
const observed = ref(0);
watch(count, next => { observed.value = next; });
export function useObserver() { return { count, observed }; }
Good
Each synchronous setup call creates its own refs and watcher inside useObserver. Vue associates that watcher with the calling component’s lifetime.
use-observer.ts
import { ref, watch } from 'vue';
export function useObserver() {
const count = ref(0);
const observed = ref(0);
watch(count, next => { observed.value = next; });
return { count, observed };
}
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.
vue/cross-file-attrs-fallthrough
A parent passes attributes to a resolved child whose root cannot inherit them and does not explicitly use $attrs.
Default severity: warning
Applies to: Reachable project declarations and imported components
Options: crossFile; rule severity (off/warn/error)
Automatic fix: None
import { defineConfig } from "@vizejs/vite-plugin/vite-plus";
export default defineConfig({
vue: {
linter: { preset: "incremental", crossFile: true,
rules: { "vue/cross-file-attrs-fallthrough": "warn" },
},
},
});
vp run lint
Shared project files
Use these unchanged files in both Bad and Good. Install the imported packages in the project: Vue, plus vue-router or Pinia where shown. Follow any version-specific support note. The entry root makes the component relationship explicit.
main.ts
import { createApp } from "vue";
import Root from "./App.vue";
createApp(Root).mount("#app");
index.html
<div id="app"></div>
<script type="module" src="/main.ts"></script>
Bad
The parent passes class="notice" to a resolved fragment child that has no automatic attribute target and never reads $attrs.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child class="notice" /></template>
Child.vue
<template><main>Content</main><aside>Help</aside></template>
Good
The child chooses <main> as its target by binding $attrs there; its sibling <aside> remains separate.
App.vue
<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child class="notice" /></template>
Child.vue
<template><main v-bind="$attrs">Content</main><aside>Help</aside></template>
The Good files demonstrate the change described above; other diagnostics can still apply to the complete project.