Vize

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.

Cross-file index

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.

Cross-file index

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.

Cross-file index

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.

Cross-file index

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.

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Producer

Producer

Producer

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Producer

Producer

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Cross-file index

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.

Public explanation

Producer

Cross-file index

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.

Public explanation

Cross-file index

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.

Cross-file index