Vize

ファイル間ルール

このページに、各ルールの目的・前提・設定と、省略のない悪い例・良い例をまとめています。変更箇所は行の色で示します。コードをコピーすると、ソースコード全体を取得できます。各例に現在の対応範囲も明記しています。

全 66 件のプロジェクトの例について、共通ファイルと、省略のない悪い例・良い例をこのページにまとめています。共通ファイルは両方で使用します。解析器の 60 件の診断コードには、CLI の 19 件(検証済みのソース例 18 件と、参照関係を保持したリアクティブなグラフを含む説明用プロジェクト 1 件)、CLI が個別には生成しない実験的な Rust 解析器の 16 件、現在は診断を生成する処理のない契約 25 件があります。ID を設定しても、未対応の診断処理は有効になりません。

公開 CLI では vize lint --cross-file で同じ処理を使用できます。表示される vize:croquis/cf/* コードは、vue.linter.rules では croquis/cf/* と設定します(vize: を省略)。information/hint の診断は CLI では warning になります。関連箇所は生成元と利用側の関係を説明します。

ルール 例 目的 現在の対応
ecosystem/vue-router-extra-param 悪い · 良い パスにない tab は Vue Router に破棄されます。 プロジェクト固有の lint ID
ecosystem/vue-router-missing-param 悪い · 良い 必須の postId がありません。現在のルートに依存する遷移になります。 プロジェクト固有の lint ID
ecosystem/vue-router-param-type 悪い · 良い postId は繰り返しパラメーターではないため配列を渡せません。 プロジェクト固有の lint ID
ecosystem/vue-router-unknown-route 悪い · 良い 登録済み router に存在しない名前です。 プロジェクト固有の lint ID
html/cross-component-nesting 悪い · 良い import した子コンポーネントの要素を合成して HTML の入れ子を検査します。 プロジェクト固有の lint ID
vize:croquis/cf/array-mutation 悪い · 良い 現在の生成元がないこの契約では、旧 Vue 2.7 の配列のインデックス代入を例にしています。Vue 2 は直接のインデックス代入による更新を通知しませんが、Vue 3 はその代入を追跡します。悪い例・良い例は Vue 2 の修正を示すもので、Vue 3 の不具合を示す例ではありません。 契約(現在の生成元なし)
vize:croquis/cf/async-boundary 悪い · 良い reactive な状態が async 境界を越え、古い値が見えることがあります。 CLI
vize:croquis/cf/async-no-suspense 悪い · 良い async コンポーネントが Suspense 境界なしで描画されています。 実験的な Rust 解析器(CLI の個別コードではありません)
vize:croquis/cf/browser-api-ssr 悪い · 良い サーバーでも描画され得る場所で、ブラウザ専用 API を使っています。 CLI
vize:croquis/cf/circular-dep 悪い · 良い コンポーネントの import が循環しています。 契約(現在の生成元なし)
vize:croquis/cf/circular-reactive-dependency 悪い · 良い reactive な計算が互いに循環して依存しています。 CLI
vize:croquis/cf/closure-captures-reactive 悪い · 良い クロージャが reactive な値を捕捉しており、その後の更新が見えません。 契約(現在の生成元なし)
vize:croquis/cf/composable-outside-setup 悪い · 良い composable が setup の外で呼ばれています。 契約(現在の生成元なし)
vize:croquis/cf/computed-side-effects 悪い · 良い computed の getter が状態を書いたり、他の副作用を起こしています。 契約(現在の生成元なし)
vize:croquis/cf/deep-import 悪い · 良い import の連鎖がプロジェクトの許す深さより深くなっています。 契約(現在の生成元なし)
vize:croquis/cf/destructuring-breaks-reactivity 悪い · 良い reactive オブジェクトの分割代入はフィールドをコピーし、追跡を失います。 CLI
vize:croquis/cf/di-outside-setup 悪い · 良い provide または inject が setup の外で呼ばれています。 契約(現在の生成元なし)
vize:croquis/cf/dom-access-without-next-tick 悪い · 良い Vue が更新を反映する前に DOM を読んでいます。 契約(現在の生成元なし)
vize:croquis/cf/duplicate-id 悪い · 良い 同じ要素 id が複数のコンポーネントで使われています。 CLI
vize:croquis/cf/event-listener-leak 悪い · 良い イベントリスナーが登録されたまま、解除されていません。 契約(現在の生成元なし)
vize:croquis/cf/event-modifier 悪い · 良い emit が対応しない修飾子を、イベントリスナーが使っています。 実験的な Rust 解析器(CLI の個別コードではありません)
vize:croquis/cf/hydration-risk 悪い · 良い この診断コードは、prop を ref にコピーする操作など、リアクティビティに関する複数の検出結果をまとめています。ファイル間検査がすべての Date.now() 式を検出するという意味ではありません。 CLI
vize:croquis/cf/inherit-attrs-unused 悪い · 良い inheritAttrs: false が設定されているのに、属性が読まれていません。 実験的な Rust 解析器(CLI の個別コードではありません)
vize:croquis/cf/inject-without-symbol 悪い · 良い inject が InjectionKey のシンボルではなく素のキーを使っています。 CLI
vize:croquis/cf/injected-async-mutation-race 悪い · 良い inject した値が、競合し得る async タスクから変更されています。 CLI
vize:croquis/cf/lifecycle-outside-setup 悪い · 良い ライフサイクルフックが setup の外で登録されています。 契約(現在の生成元なし)
vize:croquis/cf/lifecycle-without-cleanup 悪い · 良い ライフサイクルフックが後片付けなしで処理を始めています。 実験的な Rust 解析器(CLI の個別コードではありません)
vize:croquis/cf/missing-required-prop 悪い · 良い 必須 prop が渡されていません。 実験的な Rust 解析器(CLI の個別コードではありません)
vize:croquis/cf/missing-suspense 悪い · 良い async な依存が Suspense 境界の外で使われています。 契約(現在の生成元なし)
vize:croquis/cf/module-scope-reactive 悪い · 良い reactive な状態がモジュールスコープで作られ、すべての呼び出し元で共有されます。 契約(現在の生成元なし)
vize:croquis/cf/multi-root-attrs 悪い · 良い 複数ルートのコンポーネントが属性を受け取りますが、付ける場所がありません。 実験的な Rust 解析器(CLI の個別コードではありません)
vize:croquis/cf/mutated-after-escape 悪い · 良い reactive オブジェクトが、所有者から漏れた後に変更されています。 契約(現在の生成元なし)
vize:croquis/cf/non-reactive-provide 悪い · 良い provide した値が reactive ではないため、子孫は更新を観測できません。 CLI
vize:croquis/cf/non-unique-id 悪い · 良い ループ内の要素 id が項目ごとに一意ではありません。 CLI
vize:croquis/cf/object-identity-comparison 悪い · 良い 元のオブジェクトとそのリアクティブな Proxy は別の同一性を持ち、toRaw() は元のオブジェクトを返します。この契約の例は、レコードとして同じものかを判定するために安定した ID を比較する方法を示します。 契約(現在の生成元なし)
vize:croquis/cf/pinia-getter 悪い · 良い この契約の例では、setup で Pinia の getter から現在のプリミティブ値をコピーし、更新に追従しないスナップショットを作っています。追跡される計算やテンプレートで getter を直接読む場合はリアクティビティを保てます。この例では storeToRefs() を使って値を取り出します。 契約(現在の生成元なし)
vize:croquis/cf/prop-type-mismatch 悪い · 良い 渡した prop の値が、宣言された型と一致しません。 実験的な Rust 解析器(CLI の個別コードではありません)
vize:croquis/cf/provide-inject-type 悪い · 良い provide した値と inject の型が一致しません。 CLI
vize:croquis/cf/provide-without-symbol 悪い · 良い provide が InjectionKey のシンボルではなく素のキーを使っています。 CLI
vize:croquis/cf/reactive-export 悪い · 良い reactive な状態がモジュールから export されています。 契約(現在の生成元なし)
vize:croquis/cf/reactivity-outside-setup 悪い · 良い reactive API が setup の外で呼ばれています。 契約(現在の生成元なし)
vize:croquis/cf/reassignment-breaks-reactivity 悪い · 良い reactive な束縛を再代入すると、ただの値に置き換わります。 CLI
vize:croquis/cf/reference-escapes-scope 悪い · 良い reactive な参照が、寿命を持つスコープの外へ漏れています。 契約(現在の生成元なし)
vize:croquis/cf/setup-context-violation 悪い · 良い 実験的な解析器の例では、インスタンスごとの setup ではなく通常の script のモジュールスコープに ref(0) を置いています。Vue ではこのようなリアクティブな状態を作れますが、この例ではインスタンスごとに count を持つことを前提とします。CLI はこの個別の診断コードを生成しません。 実験的な Rust 解析器(CLI の個別コードではありません)
vize:croquis/cf/shallow-deep-access 悪い · 良い shallowReactive または shallowRef の深いプロパティを、追跡されるものとして読んでいます。 契約(現在の生成元なし)
vize:croquis/cf/spread-breaks-reactivity 悪い · 良い reactive オブジェクトのスプレッドは値をコピーし、追跡を失います。 CLI
vize:croquis/cf/suspense-no-fallback 悪い · 良い <Suspense> に fallback がありません。 契約(現在の生成元なし)
vize:croquis/cf/template-ref-timing 悪い · 良い テンプレート ref が、コンポーネントのマウント前に読まれています。 契約(現在の生成元なし)
vize:croquis/cf/toraw-mutation 悪い · 良い toRaw した生のオブジェクトを変更しています。 契約(現在の生成元なし)
vize:croquis/cf/uncaught-error 悪い · 良い コンポーネントが例外を投げても、受け止めるエラー境界がありません。 CLI
vize:croquis/cf/undeclared-emit 悪い · 良い 宣言されていないイベントを emit しています。 実験的な Rust 解析器(CLI の個別コードではありません)
vize:croquis/cf/undeclared-prop 悪い · 良い 親が、子が宣言していない prop を渡しています。 実験的な Rust 解析器(CLI の個別コードではありません)
vize:croquis/cf/undefined-slot 悪い · 良い 親が、子が公開していないスロットを埋めています。 契約(現在の生成元なし)
vize:croquis/cf/unhandled-event 悪い · 良い 子が emit したイベントを、どの親も処理していません。 実験的な Rust 解析器(CLI の個別コードではありません)
vize:croquis/cf/unmatched-inject 悪い · 良い inject が、どの祖先も provide していないキーを指定しています。 CLI
vize:croquis/cf/unmatched-listener 悪い · 良い 親が、子が emit しないイベントを購読しています。 実験的な Rust 解析器(CLI の個別コードではありません)
vize:croquis/cf/unregistered-component 悪い · 良い テンプレートが、登録も import もされていないコンポーネントを使っています。 実験的な Rust 解析器(CLI の個別コードではありません)
vize:croquis/cf/unresolved-import 悪い · 良い import がモジュールへ解決できません。 実験的な Rust 解析器(CLI の個別コードではありません)
vize:croquis/cf/unused-attrs 悪い · 良い 複数ルートのコンポーネントに渡した fallthrough 属性が使われていません。 実験的な Rust 解析器(CLI の個別コードではありません)
vize:croquis/cf/unused-emit 悪い · 良い 宣言した emit が一度も使われていません。 実験的な Rust 解析器(CLI の個別コードではありません)
vize:croquis/cf/unused-provide 悪い · 良い provide したキーが一度も inject されていません。 CLI
vize:croquis/cf/value-extraction-breaks-reactivity 悪い · 良い reactive な値をローカルへ取り出すと、その後の更新が失われます。 CLI
vize:croquis/cf/watch-can-be-computed 悪い · 良い ウォッチャが値を状態へ写しているだけで、computed にできます。 契約(現在の生成元なし)
vize:croquis/cf/watcheffect-async 悪い · 良い この例の非同期の watchEffect には、無効化時のクリーンアップや古い応答を防ぐ処理がありません。Vue の watchEffect はクリーンアップを扱えます。良い例では watch で監視対象を明示し、無効化された処理を中止します。 CLI
vize:croquis/cf/watcher-outside-setup 悪い · 良い watch または watchEffect が setup の外で呼ばれています。 契約(現在の生成元なし)
vue/cross-file-attrs-fallthrough 悪い · 良い 属性を渡す親と、属性を自動継承できず $attrs も使っていない子コンポーネントの関係を検査します。 プロジェクト固有の lint ID

全ルール · ルールのオプション · ESLint 移行対応表 · プロジェクトの検査 · 子への属性の継承

ecosystem/vue-router-extra-param

パスにない tab は Vue Router に破棄されます。

既定の重大度: error
適用範囲: 到達可能なプロジェクトの宣言と import したコンポーネント
オプション: crossFile と重大度(off/warn/error)
自動修正: なし

アプリの createApp(...).use(router) から、ルート定義がそろった登録済みのルーターに到達できる構成が必要です。未確定または動的なルート定義だけでは、名前が未登録だと断定できません。パラメーターを省略しても現在のルートから値を継承する場合があるため、省略についての診断は warning です。

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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 }],
});

悪い

user-post のパスで宣言されているパラメーターは userId と postId ですが、遷移時に、宣言されていない tab もパスパラメーターとして渡しています。

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>

良い

params から tab を除き、ルートのパスで宣言されたキーだけを渡します。タブ選択が必要なら、別途 query を使います。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

ファイル間ルール一覧

ecosystem/vue-router-missing-param

必須の postId がありません。現在のルートに依存する遷移になります。

既定の重大度: warning
適用範囲: 到達可能なプロジェクトの宣言と import したコンポーネント
オプション: crossFile と重大度(off/warn/error)
自動修正: なし

アプリの createApp(...).use(router) から、ルート定義がそろった登録済みのルーターに到達できる構成が必要です。未確定または動的なルート定義だけでは、名前が未登録だと断定できません。パラメーターを省略しても現在のルートから値を継承する場合があるため、省略についての診断は warning です。

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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 }],
});

悪い

user-post の必須パラメーター postId を省略しています。Vue Router は現在のルートから値を継承する場合があるため、診断の重大度は warning です。

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>

良い

userId と postId を両方明示し、現在のルートのパラメーター値に依存せず遷移できるようにします。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

ファイル間ルール一覧

ecosystem/vue-router-param-type

postId は繰り返しパラメーターではないため配列を渡せません。

既定の重大度: error
適用範囲: 到達可能なプロジェクトの宣言と import したコンポーネント
オプション: crossFile と重大度(off/warn/error)
自動修正: なし

アプリの createApp(...).use(router) から、ルート定義がそろった登録済みのルーターに到達できる構成が必要です。未確定または動的なルート定義だけでは、名前が未登録だと断定できません。パラメーターを省略しても現在のルートから値を継承する場合があるため、省略についての診断は warning です。

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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 }],
});

悪い

postId は単一の値を取るパスパラメーターですが、配列 ["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>

良い

繰り返し指定を許さないパスの postId 部分に、配列ではなく一つの値 "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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

ファイル間ルール一覧

ecosystem/vue-router-unknown-route

登録済み router に存在しない名前です。

既定の重大度: error
適用範囲: 到達可能なプロジェクトの宣言と import したコンポーネント
オプション: crossFile と重大度(off/warn/error)
自動修正: なし

アプリの createApp(...).use(router) から、ルート定義がそろった登録済みのルーターに到達できる構成が必要です。未確定または動的なルート定義だけでは、名前が未登録だと断定できません。パラメーターを省略しても現在のルートから値を継承する場合があるため、省略についての診断は warning です。

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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 }],
});

悪い

解析で参照できる登録済みルーターには user-post が登録されていますが、遷移時のルート名を user-posts と誤記しています。

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>

良い

宣言済みの二つのパスパラメーターはそのまま渡し、ルート名を登録済みの user-post に合わせます。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

ファイル間ルール一覧

html/cross-component-nesting

import した子コンポーネントの要素を合成して HTML の入れ子を検査します。

既定の重大度: warning
適用範囲: 到達可能なプロジェクトの宣言と import したコンポーネント
オプション: crossFile と重大度(off/warn/error)
自動修正: なし

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

親の <p> の内側に、ルートが <div> の子コンポーネントを合成しています。その結果、段落の中にブロック要素が入り、HTML の入れ子が不正になります。

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>

良い

子のブロック要素を含められる <section> を親のコンテナーに使います。子コンポーネントは変更しません。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

ファイル間ルール一覧

vize:croquis/cf/array-mutation

現在の生成元がないこの契約では、旧 Vue 2.7 の配列のインデックス代入を例にしています。Vue 2 は直接のインデックス代入による更新を通知しませんが、Vue 3 はその代入を追跡します。悪い例・良い例は Vue 2 の修正を示すもので、Vue 3 の不具合を示す例ではありません。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

旧 Vue 2.7 に限る例です。対応する Vue 2.7 と SFC コンパイラーを使うことを前提とします。Vue 3 の Proxy は配列のインデックス代入を追跡するため、items[0] = next は Vue 3 ではリアクティブであり、不具合ではありません。この公開コードには現在の生成元がありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

この旧 Vue 2.7 プロジェクトでは、items[0] = next が配列の変更を監視する Vue 2 の仕組みに通知せず配列を変更するため、表示中の最初の要素が更新されない場合があります。

replace-first.ts

export function replaceFirst(items: string[], next: string): void {
  items[0] = next;
}

良い

splice(0, 1, next) で Vue 2 が監視する配列の変更メソッドを使い、同じ置換を画面に反映します。

replace-first.ts

export function replaceFirst(items: string[], next: string): void {
  items.splice(0, 1, next);
}

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/async-boundary

reactive な状態が async 境界を越え、古い値が見えることがあります。

既定の重大度: error
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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();
}

悪い

ウォッチャーに無効化時のクリーンアップ処理がないため、古い検索への応答が新しい検索への応答より遅く届き、result を上書きする可能性があります。

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>

良い

await より前にクリーンアップ処理を登録します。以前のリクエストを中止して active を無効にし、まだ有効なリクエストの応答だけを代入します。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/async-no-suspense

async コンポーネントが Suspense 境界なしで描画されています。

既定の重大度: warning
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

実験的な Rust CrossFileAnalyzer には、この診断コードの生成元があります。CLI の検査はこの個別コードを生成せず、ID を設定しても Rust 側の検査は有効になりません。以下の例は解析器が対応するグラフ構造や情報を説明するもので、Vite+ での検出を保証するものではありません。

現在の対応状況: no-source-async-fact

生成元は macros.is_async() を読みますが、現在のソース解析はトップレベルの await を script setup のスコープに記録します。そのため、以下の完全な悪い例・良い例のどちらからも、現在の CLI は async-no-suspense を生成しません。これは Suspense の使い方を説明する例であり、必要なマクロ情報を渡す処理は今後の実装課題です。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

子にトップレベルの await がありますが、親には <Suspense> の境界がありません。現在のソース解析は、この診断コードの生成に必要なマクロ情報を渡しません。

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>

良い

親は同じ非同期の子を <Suspense> で包み、読み込み中の代替表示を用意します。これは使い方を示す例であり、現在の検査は悪い例・良い例のどちらからもこの診断コードを生成しません。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/browser-api-ssr

サーバーでも描画され得る場所で、ブラウザ専用 API を使っています。

既定の重大度: warning
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

setup 中に window.innerWidth を読み、ブラウザの window がない SSR 環境でも実行されます。

App.vue

<script setup lang="ts">
const width = window.innerWidth;
</script>
<template><p>Content</p></template>

良い

サーバーでも安全に使える初期値で ref を作り、クライアントでのマウント後に実行される onMounted 内で window を読みます。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/circular-dep

コンポーネントの import が循環しています。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

即時初期化による具体的な循環の例です。再帰する Vue コンポーネントや、あらゆる循環 import が必ず誤りになるわけではありません。現在この契約コードを生成する検査はありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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';

悪い

a.ts が b.ts を読み、b.ts が a.ts を読み返します。両方が相手の未初期化の定数から即座に定数を作るため、初期化前のアクセスが発生します。

a.ts

import { bLabel } from './b';
export const aLabel = 'A' + bLabel;

b.ts

import { aLabel } from './a';
export const bLabel = 'B' + aLabel;

良い

両モジュールが独立した labels.ts の初期化済みの接頭辞を読み、循環と初期化前の相互参照を取り除きます。

a.ts

import { bPrefix } from './labels';
export const aLabel = 'A' + bPrefix;

b.ts

import { aPrefix } from './labels';
export const bLabel = 'B' + aPrefix;

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/circular-reactive-dependency

reactive な計算が互いに循環して依存しています。

既定の重大度: context-dependent
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

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

例の検証状況: illustrative-source-pair

以下の完全な Vue プロジェクトは、更新の循環とその修正を具体的に示します。このソースによる CLI の検出は検証済みではありません。診断の生成元には、併記したグラフのように保持された参照の ID と流れが必要です。このソースだけで現在の処理がこの診断コードを生成すると断定しません。参照 ID を保持したグラフに対する専用の検出検証と、ソースの構文検査は分けて扱います。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

App が count(A)を保持して provide します。CycleView は nextCount(B)を computed で求め、watch でその値を同じ inject 済みの count に書き戻します。immediate の実行後も計算の入力が更新され続けるため、A → B → A の更新の循環になります。下の参照グラフはこの二つの参照を表し、同名の無関係な変数をつなぐものではありません。

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

良い

B の値を A に書き戻す watch を取り除きます。count は App が保持し、明示的な Increment 操作でだけ変更します。CycleView は nextCount を読み取るだけで計算結果を書き戻しません。同じ二つの参照には A → B の依存だけが残ります。

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

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/closure-captures-reactive

クロージャが reactive な値を捕捉しており、その後の更新が見えません。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

makeReader がクロージャーを作る前に count.value をコピーします。computed の読取処理はリアクティブな依存を読まず、最初の数値を返し続けます。

reader.ts

import type { Ref } from 'vue';
export function makeReader(count: Ref<number>): () => number {
  const captured = count.value;
  return () => captured;
}

良い

呼び出すたびにクロージャー内で count.value を読み、computed が ref を追跡して増加後の shown を更新できるようにします。

reader.ts

import type { Ref } from 'vue';
export function makeReader(count: Ref<number>): () => number {
  return () => count.value;
}

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/composable-outside-setup

composable が setup の外で呼ばれています。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

対象はライフサイクルに依存するこの composable です。通常のユーティリティ関数や、setup 外のあらゆる Composition API 呼び出しを禁止する例ではありません。この契約には現在の生成元がありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

use-title.ts の import 時点で、コンポーネントの setup が有効になる前に onMounted を登録します。後から公開関数を呼んでもモジュール直下の ref を返すだけで、ライフサイクルフックは、コンポーネントに登録されないままです。

use-title.ts

import { onMounted, ref } from 'vue';
const title = ref('Before mount');
onMounted(() => { title.value = 'Mounted'; });
export function useTitle() { return title; }

良い

状態作成とフック登録を useTitle の中へ移し、App の setup 内から同期的に呼びます。mounted フックがその App インスタンスに所属するようになります。

use-title.ts

import { onMounted, ref } from 'vue';
export function useTitle() {
  const title = ref('Before mount');
  onMounted(() => { title.value = 'Mounted'; });
  return title;
}

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/computed-side-effects

computed の getter が状態を書いたり、他の副作用を起こしています。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

doubled の評価が lastCalculated を書き換え、computed の値を読む操作が別の状態を変更しています。副作用が遅延評価の getter を読むタイミングに依存します。

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 };
}

良い

getter は導出した数値だけを返します。count の変更時に lastCalculated を書く処理は、初期値も含めて別の watcher に任せます。

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 };
}

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/deep-import

import の連鎖がプロジェクトの許す深さより深くなっています。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

明示的に選んだプロジェクトの配置方針を示す例です。対応する深さのしきい値やオプションがあると主張するものではありません。この契約には現在の診断生成元がありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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';

悪い

entry.ts が単純な値を level-one、level-two、level-three 経由で参照しています。公開部分への参照を短い経路に保ちたいプロジェクトでは、不要に深い import の連鎖になります。

entry.ts

export { label } from './level-one';

良い

値を直接再公開する public-api.ts を entry から使います。使用側の import 名を保ったまま、参照の列を短くします。

entry.ts

export { label } from './public-api';

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/destructuring-breaks-reactivity

reactive オブジェクトの分割代入はフィールドをコピーし、追跡を失います。

既定の重大度: error
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

props オブジェクトを通常の分割代入で展開すると、現在の item の値を取り出します。Vue 3.5 の defineProps() から直接行う分割代入とは別です。

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>

良い

toRef(props, "item") を使い、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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

生成元

生成元

ファイル間ルール一覧

vize:croquis/cf/di-outside-setup

provide または inject が setup の外で呼ばれています。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

コンポーネントの provide/inject を使う例です。app.provide や対応する app.runWithContext 内の inject は別の有効な所属先であり、この例で禁止するものではありません。現在この契約コードの生成元はありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

main.ts が有効なコンポーネントインスタンスなしで component の provide を呼びます。子の inject は意図した祖先の値を受け取れず、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>

良い

App の setup で子を描画する前に provider を呼び、子が祖先コンポーネントの dark を受け取れるようにします。

App.vue

<script setup lang="ts">
import ThemedText from './ThemedText.vue';
import { provideTheme } from './theme';
provideTheme();
</script>

<template>
<ThemedText />
</template>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/dom-access-without-next-tick

Vue が更新を反映する前に DOM を読んでいます。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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 ?? '';
}

悪い

クリック処理が count を増やしてすぐ段落の DOM を読み、Vue による更新の反映を待っていません。sampled に前の 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>

良い

状態を更新した後に nextTick() を待ち、Vue が段落を更新してから readLabel でテキストを読みます。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/duplicate-id

同じ要素 id が複数のコンポーネントで使われています。

既定の重大度: warning
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

描画する ShippingAddress.vue と BillingAddress.vue の両方が id="postal-code" を使い、label の文書内の参照先が重複します。

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>

良い

各コンポーネントが useId() を呼び、その戻り値を label の for と input の id にバインドします。ラベルと入力欄の対応を保ちながら、固定 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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/event-listener-leak

イベントリスナーが登録されたまま、解除されていません。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

マウント時に width の ref を参照する resize リスナーを window に追加しますが、アンマウント時に削除しません。再マウントを繰り返すと不要なリスナーや状態を保持し得ます。

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;
}

良い

onUnmounted で、マウント時に登録したものと同一の resize 関数を使ってリスナーを解除します。これで、そのインスタンスに属する外部リスナーの有効期間が終わります。

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;
}

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/event-modifier

emit が対応しない修飾子を、イベントリスナーが使っています。

既定の重大度: info
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

実験的な Rust CrossFileAnalyzer には、この診断コードの生成元があります。CLI の検査はこの個別コードを生成せず、ID を設定しても Rust 側の検査は有効になりません。以下の例は解析器が対応するグラフ構造や情報を説明するもので、Vite+ での検出を保証するものではありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

子のカスタムイベント save に .stop を付け、ネイティブイベントの伝播を止めるメソッドがあることを前提にしています。しかし、このイベントで渡される値が 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>

良い

カスタムイベントのリスナーから .stop を取り除きます。ネイティブイベントの伝播を止める必要がある場合は、実際の DOM 要素に付けたリスナーで制御します。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/hydration-risk

この診断コードは、prop を ref にコピーする操作など、リアクティビティに関する複数の検出結果をまとめています。ファイル間検査がすべての Date.now() 式を検出するという意味ではありません。

既定の重大度: error
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

子の ref(props.count) は初期値を一度コピーし、後の親の prop 変更には追従しません。現在の prop-to-ref 生成元の例で、非決定的な SSR 全般の例ではありません。

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>

良い

初期値を独立した状態にコピーせず、toRef(props, "count") で prop を参照します。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

生成元

ファイル間ルール一覧

vize:croquis/cf/inherit-attrs-unused

inheritAttrs: false が設定されているのに、属性が読まれていません。

既定の重大度: warning
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

実験的な Rust CrossFileAnalyzer には、この診断コードの生成元があります。CLI の検査はこの個別コードを生成せず、ID を設定しても Rust 側の検査は有効になりません。以下の例は解析器が対応するグラフ構造や情報を説明するもので、Vite+ での検出を保証するものではありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

子が inheritAttrs: false にして、親の class="notice" をどこにも渡していません。

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>

良い

明示的な継承制御を保ち、適用先の <main> に $attrs をバインドします。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/inject-without-symbol

inject が InjectionKey のシンボルではなく素のキーを使っています。

既定の重大度: warning
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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");

悪い

使用側は型の付いていない文字列キー "theme" を inject しており、提供側と共有するシンボルの同一性を利用していません。

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>

良い

使用側と提供側で同じ ThemeKey を import し、文字列名の重複に頼らない接続にします。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/injected-async-mutation-race

inject した値が、競合し得る async タスクから変更されています。

既定の重大度: error
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

CountLoader.vue が非同期処理の結果を、CountSummary.vue と共有する inject 済みのストアへ直接書き込みます。そのため、古い処理の結果が両方の使用側に影響する可能性があります。

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>

良い

CountLoader.vue は無効化された処理を中止し、まだ有効な処理の結果だけを emit します。ストアの変更は、提供側の 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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/lifecycle-outside-setup

ライフサイクルフックが setup の外で登録されています。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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'; });
}

悪い

エントリーファイルの main.ts がアプリのマウント前に installTitle() を呼び、コンポーネントの setup が有効でない状態で onMounted を登録しています。

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>

良い

同じヘルパーを App の setup から同期的に呼び、ライフサイクルのコールバックをそのインスタンスのマウントに結び付けます。

App.vue

<script setup lang="ts">
import { installTitle } from './install-title';
installTitle();
</script>

<template>
<p>Application</p>
</template>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/lifecycle-without-cleanup

ライフサイクルフックが後片付けなしで処理を始めています。

既定の重大度: warning
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

実験的な Rust CrossFileAnalyzer には、この診断コードの生成元があります。CLI の検査はこの個別コードを生成せず、ID を設定しても Rust 側の検査は有効になりません。以下の例は解析器が対応するグラフ構造や情報を説明するもので、Vite+ での検出を保証するものではありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

マウント時に window の resize リスナーを登録しますが、アンマウント時に同じコールバックを解除していません。

App.vue

<script setup lang="ts">
import { onMounted } from "vue";
const resize = () => {};
onMounted(() => { window.addEventListener("resize", resize); });
</script>
<template><p>Content</p></template>

良い

onUnmounted で、登録時と同じイベント名・関数の参照を使ってリスナーを解除します。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/missing-required-prop

必須 prop が渡されていません。

既定の重大度: error
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

実験的な Rust CrossFileAnalyzer には、この診断コードの生成元があります。CLI の検査はこの個別コードを生成せず、ID を設定しても Rust 側の検査は有効になりません。以下の例は解析器が対応するグラフ構造や情報を説明するもので、Vite+ での検出を保証するものではありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

親が <Child /> を描画し、子の必須 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>

良い

title="Hello" で、解決した子が宣言する必須 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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/missing-suspense

async な依存が Suspense 境界の外で使われています。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

AsyncCard のトップレベルの await が setup を非同期にしますが、App は非同期処理の完了を待つ Suspense の境界なしで描画しています。

App.vue

<script setup lang="ts">
import AsyncCard from './AsyncCard.vue';
</script>

<template>
<AsyncCard />
</template>

良い

App が非同期の子を Suspense で包み、子の setup が完了するまで読み込み中の代替表示を出します。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/module-scope-reactive

reactive な状態がモジュールスコープで作られ、すべての呼び出し元で共有されます。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

アプリケーション全体の store を意図する場合、モジュール直下のリアクティブ状態は有効です。この例はコンポーネントやリクエスト間の分離を前提とし、この公開契約には現在の生成元がありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

モジュールが count を一度だけ初期化し、二つの Counter が同じ ref を受け取ります。この例では独立した状態を意図していますが、一方をクリックすると両方が変わります。

counter.ts

import { ref } from 'vue';
const count = ref(0);
export function createCounter() { return { count }; }

良い

createCounter の中で ref を作り、setup からの同期的な呼び出しごとに別の状態を渡します。それぞれのボタンが個別のカウンターを持つようになります。

counter.ts

import { ref } from 'vue';
export function createCounter() {
  const count = ref(0);
  return { count };
}

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/multi-root-attrs

複数ルートのコンポーネントが属性を受け取りますが、付ける場所がありません。

既定の重大度: warning
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

実験的な Rust CrossFileAnalyzer には、この診断コードの生成元があります。CLI の検査はこの個別コードを生成せず、ID を設定しても Rust 側の検査は有効になりません。以下の例は解析器が対応するグラフ構造や情報を説明するもので、Vite+ での検出を保証するものではありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

子に <main> と <aside> の二つのルートがあり、親の 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>

良い

二つ目のルートを保ったまま、<main> に $attrs を明示的に渡します。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/mutated-after-escape

reactive オブジェクトが、所有者から漏れた後に変更されています。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

変更しない履歴という明示的な所有方針を示す例です。リアクティブなオブジェクトを渡したり、後で変更したりすることを一般に禁止するものではありません。現在この契約の生成元はありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

archive は publish に渡した同じオブジェクトを保持します。その後で所有側が名前を変更し、過去の記録まで Grace に変わります。TypeScript の Readonly 引数はオブジェクトをコピーしません。

profile.ts

import { reactive } from 'vue';
import { publish } from './archive';
export function publishProfile(): void {
  const profile = reactive({ name: 'Ada' });
  publish(profile);
  profile.name = 'Grace';
}

良い

通常のコピーを公開し、保存した Ada の記録と後のリアクティブな profile の変更を分離します。archive のスナップショット方針を保てるようになります。

profile.ts

import { reactive } from 'vue';
import { publish } from './archive';
export function publishProfile(): void {
  const profile = reactive({ name: 'Ada' });
  publish({ ...profile });
  profile.name = 'Grace';
}

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/non-reactive-provide

provide した値が reactive ではないため、子孫は更新を観測できません。

既定の重大度: context-dependent
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

ThemeProvider.vue が通常のオブジェクトを provide しています。そのフィールドを変更しても、inject 側が追跡できる Vue のリアクティブな依存関係にはなりません。

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>

良い

提供する theme を ref で包み、inject する同じ参照で後の変更を追跡します。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/non-unique-id

ループ内の要素 id が項目ごとに一意ではありません。

既定の重大度: error
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

v-for の各反復で固定の result-title ID が繰り返されます。key があっても DOM の ID は一意になりません。

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>

良い

見出しの ID に result の識別子を含め、各項目の文書 ID を区別します。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/object-identity-comparison

元のオブジェクトとそのリアクティブな Proxy は別の同一性を持ち、toRaw() は元のオブジェクトを返します。この契約の例は、レコードとして同じものかを判定するために安定した ID を比較する方法を示します。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

ID がレコードを一意に識別することを前提とします。同じリアクティブ Proxy の参照同士を比較することは有効であり、この契約には現在の生成元がありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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) };
}

悪い

proxy === raw はラッパーの同一性を比較するため、同じユーザーを表していても false になります。アプリが意図するレコードの同一性と、オブジェクトの同一性が異なっています。

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>

良い

安定したレコードの id を比較し、raw か Proxy かに依存せず意図した同一性を判定します。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/pinia-getter

この契約の例では、setup で Pinia の getter から現在のプリミティブ値をコピーし、更新に追従しないスナップショットを作っています。追跡される計算やテンプレートで getter を直接読む場合はリアクティビティを保てます。この例では storeToRefs() を使って値を取り出します。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

Pinia のインストールを前提とし、main.ts がマウント前にプラグインを登録します。追跡される計算やテンプレートの中で store.doubled を直接読むことは有効です。ここでの問題は通常の値としてスナップショットを取ることであり、現在この契約の生成元はありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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 },
});

悪い

const doubled = store.doubled が setup 時点の getter の数値をコピーし、後の store.count 更新に追従しなくなります。

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>

良い

storeToRefs(store) からリアクティブな getter の ref を受け取り、分割代入やテンプレートでの自動アンラップ後もストアとのつながりを保ちます。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/prop-type-mismatch

渡した prop の値が、宣言された型と一致しません。

既定の重大度: error
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

実験的な Rust CrossFileAnalyzer には、この診断コードの生成元があります。CLI の検査はこの個別コードを生成せず、ID を設定しても Rust 側の検査は有効になりません。以下の例は解析器が対応するグラフ構造や情報を説明するもので、Vite+ での検出を保証するものではありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

親が数値式 42 を、解決した子の 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>

良い

文字列の title="Hello" を渡し、子の宣言と合わせます。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/provide-inject-type

provide した値と inject の型が一致しません。

既定の重大度: warning
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

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

この検査は提供元と使用側の明示的な型注釈を比較し、リテラルからの型推論は使いません。この例の提供元の as string 注釈を残してください。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

提供側の title は明示的な string ですが、子孫が同じキーを inject<number> で要求します。

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>

良い

使用側の型注釈を inject<string> にし、提供側の注釈と一致させます。この生成元は推論されたリテラル型ではなく明示的な注釈を比較するため、as string を残します。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/provide-without-symbol

provide が InjectionKey のシンボルではなく素のキーを使っています。

既定の重大度: warning
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

両コンポーネントが文字列 "theme" を使い、無関係な機能とキーが衝突する可能性があります。

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>

良い

型付きのシンボル ThemeKey を一つだけ export し、provide 側と inject 側で同じ値を import します。説明文字列が同じでも、別々に作ったシンボルでは両者を結び付けられません。

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");

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/reactive-export

reactive な状態がモジュールから export されています。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

共有するアプリケーションの store を意図する場合、リアクティブな状態の export は有効です。この例は状態の分離を必要とするもので、あらゆる export が不正という意味ではありません。現在この契約の生成元はありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

モジュールが初期化済みのリアクティブなオブジェクトを一つだけ公開するため、それを import する側はすべて同じ count を受け取ります。リクエスト間でモジュールを共有する SSR では、この例で必要とする、インスタンスやリクエストごとの状態の分離を保てません。

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>

良い

モジュールから状態を作る関数を公開し、App が setup 内で呼びます。共有する一つのインスタンスを公開する形から、コンポーネントのインスタンスごとに新しいリアクティブな count を作る形に変更します。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/reactivity-outside-setup

reactive API が setup の外で呼ばれています。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

Vue では component setup の外でも ref・reactive・computed を使えます。ここでのリスクは API の禁止ではなく、インスタンスを分離する明示的な方針に反した共有や所属です。この契約には現在の生成元がありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

二つのリアクティブ API がモジュールの読み込み時に実行されます。独立したカウンターを意図していても、二つの Counter が一つの ref と computed を共有します。

use-counter.ts

import { computed, ref } from 'vue';
const count = ref(0);
const doubled = computed(() => count.value * 2);
export function useCounter() { return { count, doubled }; }

良い

各コンポーネントの setup から同期的に useCounter を呼び、その中で ref と computed を作ります。これにより、各カウンターがそれぞれの状態と、依存を追跡する計算結果を持ちます。

use-counter.ts

import { computed, ref } from 'vue';
export function useCounter() {
  const count = ref(0);
  const doubled = computed(() => count.value * 2);
  return { count, doubled };
}

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/reassignment-breaks-reactivity

reactive な束縛を再代入すると、ただの値に置き換わります。

既定の重大度: error
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

子が作った prop の ref を props.user で上書きし、ref としての接続を失います。

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>

良い

toRef を const に保持し、参照を置き換える再代入を除きます。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/reference-escapes-scope

reactive な参照が、寿命を持つスコープの外へ漏れています。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

ref を composable から返したり、スコープをまたいで共有したりすることは有効です。この例では、キャッシュにスナップショットを保存することを前提としています。アンマウントで ref 自体が無効になるという意味ではありません。現在この契約の生成元はありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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; }

悪い

プロセス全体のキャッシュがコンポーネントの count の ref をそのまま保持し、アンマウント後もインスタンスの状態を参照できる状態にしています。スナップショットを保存する意図にもかかわらず、後の変更も観測できます。

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>

良い

キャッシュに現在の数値を渡し、コンポーネントが持つ 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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/setup-context-violation

実験的な解析器の例では、インスタンスごとの setup ではなく通常の script のモジュールスコープに ref(0) を置いています。Vue ではこのようなリアクティブな状態を作れますが、この例ではインスタンスごとに count を持つことを前提とします。CLI はこの個別の診断コードを生成しません。

既定の重大度: context-dependent
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

実験的な Rust CrossFileAnalyzer には、この診断コードの生成元があります。CLI の検査はこの個別コードを生成せず、ID を設定しても Rust 側の検査は有効になりません。以下の例は解析器が対応するグラフ構造や情報を説明するもので、Vite+ での検出を保証するものではありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

通常の script ブロックのモジュールスコープで ref(0) を作っており、この解析器の例で想定する、インスタンスごとの setup コンテキストには含まれません。

App.vue

<script lang="ts">
import { ref } from "vue";
const count = ref(0);
export default {};
</script>
<template><p>Count</p></template>

良い

script setup に移し、各インスタンスが count を持ち、テンプレートから読み取れる形にします。

App.vue

<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
</script>
<template><p>{{ count }}</p></template>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/shallow-deep-access

shallowReactive または shallowRef の深いプロパティを、追跡されるものとして読んでいます。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

shallowReactive が追跡するのはルートの user プロパティであり、内側のオブジェクトは raw のままです。profile.user.name の変更は、追跡された深い変更としてテンプレートへ通知されません。

profile.ts

import { shallowReactive } from 'vue';
export function makeProfile() { return shallowReactive({ user: { name: 'Ada' } }); }

良い

reactive で内側の user オブジェクトまでリアクティブにし、同じ代入で表示が更新されるようにします。

profile.ts

import { reactive } from 'vue';
export function makeProfile() { return reactive({ user: { name: 'Ada' } }); }

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/spread-breaks-reactivity

reactive オブジェクトのスプレッドは値をコピーし、追跡を失います。

既定の重大度: error
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

UserSummary.vue が props.user を新しいオブジェクトに展開し、渡されたリアクティブな値をその時点でコピーします。

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>

良い

toRef(props, "user") でプロパティの値をコピーせず、受け取る prop の参照を維持します。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/suspense-no-fallback

<Suspense> に fallback がありません。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

fallback のない Suspense は有効な Vue の構文です。ローディング UI を置くという方針の例であり、コンパイルエラーではありません。この公開契約には現在の生成元がありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

Suspense の境界に非同期の子がありますが fallback がなく、この例の待機中の状態に表示する内容がありません。

App.vue

<script setup lang="ts">
import { Suspense } from 'vue';
import AsyncCard from './AsyncCard.vue';
</script>

<template>
<Suspense><AsyncCard /></Suspense>
</template>

良い

#fallback slot にローディングの段落を指定し、非同期の子が完了するまで表示します。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/template-ref-timing

テンプレート ref が、コンポーネントのマウント前に読まれています。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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(); }

悪い

マウント前の setup でテンプレート ref を読み取るため、その値はまだ null です。input?.focus() はオプショナルチェーンによって呼び出しが省略され、入力欄にフォーカスしません。

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>

良い

onMounted で Vue が input 要素をテンプレート ref に設定するまで参照を遅らせ、ヘルパーが実際の要素をフォーカスできるようにします。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/toraw-mutation

toRaw した生のオブジェクトを変更しています。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

rename は Proxy の元のオブジェクトを取り出し、raw.name に代入しています。そのため、表示中の名前の変更を通知する Proxy の setter を通りません。

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';
}

良い

渡されたリアクティブ Proxy の profile.name を書き、同じ名前の変更を依存先に通知します。

profile.ts

import { reactive } from 'vue';
export function makeProfile() { return reactive({ name: 'Ada' }); }
export function rename(profile: { name: string }): void {
  profile.name = 'Grace';
}

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/uncaught-error

コンポーネントが例外を投げても、受け止めるエラー境界がありません。

既定の重大度: info
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

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

現在の生成元は JSON.parse(input) などのテンプレート式を検査します。script ブロックだけにある throw 文は対象外です。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

子のテンプレートが不正な input を JSON.parse し、到達する親にエラーを受け止める境界がありません。

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>

良い

親で子のエラーを受ける onErrorCaptured を登録します。false を返すと伝播が止まります。実際に使うエラー境界では、復旧に役立つ 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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/undeclared-emit

宣言されていないイベントを emit しています。

既定の重大度: error
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

実験的な Rust CrossFileAnalyzer には、この診断コードの生成元があります。CLI の検査はこの個別コードを生成せず、ID を設定しても Rust 側の検査は有効になりません。以下の例は解析器が対応するグラフ構造や情報を説明するもので、Vite+ での検出を保証するものではありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

子が emit("save") を呼びますが、defineEmits の宣言は 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>

良い

引数なしの save を宣言し、emit するイベントをコンポーネントの契約に合わせます。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/undeclared-prop

親が、子が宣言していない prop を渡しています。

既定の重大度: warning
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

実験的な Rust CrossFileAnalyzer には、この診断コードの生成元があります。CLI の検査はこの個別コードを生成せず、ID を設定しても Rust 側の検査は有効になりません。以下の例は解析器が対応するグラフ構造や情報を説明するもので、Vite+ での検出を保証するものではありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

親が typo を渡しますが、解決した子の宣言は title だけです。この解析器の規約は、Vue の属性自動継承の一般的な動作とは別です。

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>

良い

意図しない typo のバインドを取り除き、宣言済みの title を残します。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/undefined-slot

親が、子が公開していないスロットを埋めています。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

App が footer スロットを渡しますが、Card が宣言して描画するのは header だけです。渡した Notice を表示するスロットの挿入位置が子にありません。

App.vue

<script setup lang="ts">
import Card from './Card.vue';
</script>

<template>
<Card><template #footer>Notice</template></Card>
</template>

良い

子の型付きスロット宣言とスロットの挿入位置に合わせて header を渡し、Notice を表示できるようにします。

App.vue

<script setup lang="ts">
import Card from './Card.vue';
</script>

<template>
<Card><template #header>Notice</template></Card>
</template>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/unhandled-event

子が emit したイベントを、どの親も処理していません。

既定の重大度: info
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

実験的な Rust CrossFileAnalyzer には、この診断コードの生成元があります。CLI の検査はこの個別コードを生成せず、ID を設定しても Rust 側の検査は有効になりません。以下の例は解析器が対応するグラフ構造や情報を説明するもので、Vite+ での検出を保証するものではありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

Child.vue が save を emit しますが、直接の親である Wrapper.vue はそのイベントを購読していません。コンポーネントのイベントは、ラッパーを越えて自動的に親へ伝播しません。

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>

良い

Wrapper.vue が直接の子に save のリスナーを付けます。空のコールバックは、このルールでイベントを受け取る例を示すためのもので、保存処理の実装全体ではありません。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/unmatched-inject

inject が、どの祖先も provide していないキーを指定しています。

既定の重大度: error / warning (with default)
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

ThemeLabel.vue は ThemeKey を inject しますが、解析対象の構成で祖先にあたる App.vue は同じキーを provide していません。

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>

良い

App.vue は、公開された同じ ThemeKey を使ってリアクティブなテーマを provide してから、その値を inject する子孫を描画します。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/unmatched-listener

親が、子が emit しないイベントを購読しています。

既定の重大度: warning
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

実験的な Rust CrossFileAnalyzer には、この診断コードの生成元があります。CLI の検査はこの個別コードを生成せず、ID を設定しても Rust 側の検査は有効になりません。以下の例は解析器が対応するグラフ構造や情報を説明するもので、Vite+ での検出を保証するものではありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

親は save のリスナーを登録していますが、解決済みの子コンポーネントが宣言するイベントは 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>

良い

子が save を宣言して emit し、親が登録するリスナーのイベント名と合わせます。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/unregistered-component

テンプレートが、登録も import もされていないコンポーネントを使っています。

既定の重大度: error
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

実験的な Rust CrossFileAnalyzer には、この診断コードの生成元があります。CLI の検査はこの個別コードを生成せず、ID を設定しても Rust 側の検査は有効になりません。以下の例は解析器が対応するグラフ構造や情報を説明するもので、Vite+ での検出を保証するものではありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

Child.vue は存在しますが、親がテンプレートの Child を import / 登録していません。

App.vue

<template><Child /></template>

Child.vue

<template><p>Child</p></template>

良い

親の script setup で Child を import し、テンプレートがそのコンポーネントの識別子を解決できるようにします。

App.vue

<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child /></template>

Child.vue

<template><p>Child</p></template>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/unresolved-import

import がモジュールへ解決できません。

既定の重大度: error
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

実験的な Rust CrossFileAnalyzer には、この診断コードの生成元があります。CLI の検査はこの個別コードを生成せず、ID を設定しても Rust 側の検査は有効になりません。以下の例は解析器が対応するグラフ構造や情報を説明するもので、Vite+ での検出を保証するものではありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

親の import は ./Missing.vue ですが、プロジェクトにあるファイルは Child.vue です。

App.vue

<script setup lang="ts">
import Child from "./Missing.vue";
</script>
<template><Child /></template>

Child.vue

<template><p>Child</p></template>

良い

テンプレートで使う識別子は変えず、存在する ./Child.vue を import します。

App.vue

<script setup lang="ts">
import Child from "./Child.vue";
</script>
<template><Child /></template>

Child.vue

<template><p>Child</p></template>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/unused-attrs

複数ルートのコンポーネントに渡した fallthrough 属性が使われていません。

既定の重大度: info
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

実験的な Rust CrossFileAnalyzer には、この診断コードの生成元があります。CLI の検査はこの個別コードを生成せず、ID を設定しても Rust 側の検査は有効になりません。以下の例は解析器が対応するグラフ構造や情報を説明するもので、Vite+ での検出を保証するものではありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

親の tracking-code は prop として使われず、複数ルートの子から転送もされません。

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>

良い

<main> に $attrs をバインドし、継承属性の適用先を明示します。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/unused-emit

宣言した emit が一度も使われていません。

既定の重大度: warning
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

実験的な Rust CrossFileAnalyzer には、この診断コードの生成元があります。CLI の検査はこの個別コードを生成せず、ID を設定しても Rust 側の検査は有効になりません。以下の例は解析器が対応するグラフ構造や情報を説明するもので、Vite+ での検出を保証するものではありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

子が save を宣言していますが、その名前で emit 関数を呼んでいません。

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>

良い

例では emit("save") を呼び、宣言したイベントを使います。実際の実装では、対応する操作が行われたときに通知します。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/unused-provide

provide したキーが一度も inject されていません。

既定の重大度: warning
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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");

悪い

App.vue に ThemeKey の provide がありますが、描画する Dashboard.vue の下に使用側がありません。

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>

良い

Dashboard.vue が ThemeLabel.vue を描画するようにし、ThemeLabel.vue で祖先が使うものと同一の ThemeKey を inject します。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/value-extraction-breaks-reactivity

reactive な値をローカルへ取り出すと、その後の更新が失われます。

既定の重大度: error
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

Vue 3.5 の reactive な props 分割代入で得た item を、itemSnapshot に一度だけ読み取っています。後の prop の置き換えは、そのスナップショットに反映されません。

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>

良い

computed 内で item を読み取ります。Vue がリアクティブな 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 { computed } from "vue";

const { item } = defineProps<{ item: { name: string } }>();
const itemView = computed(() => item);
</script>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/watch-can-be-computed

ウォッチャが値を状態へ写しているだけで、computed にできます。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

純粋な派生状態についての公開された推奨を示す例です。外部への副作用や独立して書き込む状態には watcher が適しており、現在この契約の生成元はありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

watcher は外部への副作用を行わず、二つ目の書き込み可能な ref を count の二倍へ同期するだけです。この例に派生値への独立した書き込みはありません。

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 };
}

良い

computed の getter で同じ導出を直接表し、手作業の同期と追加の書き込み可能な状態を取り除きます。

use-double.ts

import { computed, ref } from 'vue';
export function useDouble() {
  const count = ref(0);
  const doubled = computed(() => count.value * 2);
  return { count, doubled };
}

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vize:croquis/cf/watcheffect-async

この例の非同期の watchEffect には、無効化時のクリーンアップや古い応答を防ぐ処理がありません。Vue の watchEffect はクリーンアップを扱えます。良い例では watch で監視対象を明示し、無効化された処理を中止します。

既定の重大度: error
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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();
}

悪い

非同期の watchEffect で、暗黙の依存関係の収集と await によるリクエストの待機を行っています。無効化後に届く応答への対処はありません。

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>

良い

watch(() => props.query, ...) で監視対象を明示し、リクエストのクリーンアップ処理を登録します。無効化されたリクエストの古い応答は反映しません。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

生成元

ファイル間ルール一覧

vize:croquis/cf/watcher-outside-setup

watch または watchEffect が setup の外で呼ばれています。

既定の重大度: 現在は生成されません
適用範囲: 解析対象のコンポーネント構成と、下記に示す対応済みの情報
自動修正: なし。関連ファイルを確認して修正してください
オプション: コード固有のオプションはありません。対応済みの CLI の診断は、重大度を変更できます

この公開診断コードには現在の生成元がありません。悪い例・良い例はリスクと修正の説明です。このコードを検出させる設定は現在ありません。

所有側が停止関数を保持して呼び出す場合や、アプリケーション全体で動かし続けることを意図する場合、モジュール直下のウォッチャーは有効です。この例ではウォッチャーの有効期間をコンポーネントの生存期間に結び付けることを前提としています。この契約には現在の生成元がありません。

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

ウォッチャーを各 Observer の setup 内ではなく、モジュールの読み込み時に作っています。そのため、両インスタンスが ref を共有し、個々の Observer をアンマウントしてもウォッチャーは自動停止しません。

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 }; }

良い

setup からの同期的な呼び出しごとに、useObserver 内で個別の ref とウォッチャーを作ります。Vue がウォッチャーの有効期間を呼び出し元のコンポーネントの生存期間に結び付けます。

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 };
}

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

公開の説明

ファイル間ルール一覧

vue/cross-file-attrs-fallthrough

属性を渡す親と、属性を自動継承できず $attrs も使っていない子コンポーネントの関係を検査します。

既定の重大度: warning
適用範囲: 到達可能なプロジェクトの宣言と import したコンポーネント
オプション: crossFile と重大度(off/warn/error)
自動修正: なし

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

共通のプロジェクト ファイル

以下のファイルは、悪い例・良い例の両方で変更せずに使います。プロジェクトには Vue をインストールし、例で import している場合は vue-router や Pinia もインストールしてください。バージョン固有の対応条件がある場合は、その注意に従ってください。エントリーファイルによって、コンポーネント間の関係を明示しています。

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>

悪い

親が class="notice" を複数のルートを持つ子コンポーネントに渡しますが、属性の自動適用先がなく、子も $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>

良い

子が <main> に $attrs をバインドして属性の適用先を明示します。隣の <aside> には適用しません。

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>

良い例のファイルは、上で説明した変更を示すものです。プロジェクト全体を検査すると、ほかの診断が出る場合があります。

ファイル間ルール一覧