Vize

Vue RFC Experimental Details

このページは Experimentals の Vue RFC 部分を詳しく説明します。Vize が ship している opt-in contract、各 flag で有効になる example、まだ RFC または tooling work として扱う boundary をまとめています。上流 PR は design source ですが、対応する Vize flag を明示的に on にしない限り RFC 機能は有効になりません。

Opt-in Contract

RFC flag はすべて独立しています。1 つの proposal を有効にしても、別の proposal は有効になりません。

RFC Vize config flag Direct compiler field 対象 flag が off の場合
#823 patterned templates experimentals.patternedTemplate experimentalPatternedTemplate DOM, SSR, Vapor, SFC, WASM compile API v-matchv-whenv-case は opt-in が必要だと報告
#831 in-tag comments experimentals.inTagComment experimentalInTagComments Parser, DOM, SSR, Vapor, SFC, WASM compile API opening tag 内の // は不正な tag syntax として parse
#833 self references experimentals.selfComponent experimentalSelfComponent DOM, SSR, Vapor, SFC, WASM compile API <Self> は通常の component tag
#734 strict slot children experimentals.strictSlotChildren experimentalStrictSlotChildren vize check, LSP, virtual-TS project API 追加の child-type assertion は生成しない

共有 config と plugin config では experimentals を使います。direct compiler field は、alias、precedence、{} switch object をすでに final boolean に解決した integration だけが使います。

import { defineConfig } from "vize";

export default defineConfig({
  experimentals: {
    inTagComment: true,
    patternedTemplate: true,
    selfComponent: true,
    strictSlotChildren: true,
  },
});
compile(templateSource, { experimentalPatternedTemplate: true });
compileVapor(templateSource, { experimentalSelfComponent: true, componentName: "TreeNode" });
parseTemplate(templateSource, { experimentalInTagComments: true });
compileSfc(sfcSource, { filename: "TreeNode.vue", experimentalStrictSlotChildren: true });

compilecompileVaporparseTemplatecompileSfc* の direct native field は pattenedTemplateintagComment のような alias を解釈せず、{} switch object も受け付けません。direct Vite plugin value は shared config より優先され、明示的な opt-out も優先されます。

Current Scope

RFC Vize が現在 ship しているもの 明示しておく boundary
#823 parser support、runtime lowering、branch-local binding、guard、rest/as pattern、flag-off と invalid placement の diagnostic Canon は narrowing・網羅性を検査。editor navigation 全体と compiler grammar の統一は deferred
#831 in-tag // parser support、root.comments の source text、AST comment を保持する compile pipeline runtime output なし、child comment node なし、browser in-DOM template support なし
#833 DOM、SSR、Vapor compilation の exact <Self> current-component resolution render-function / JSX macro なし。flag 有効時は local/imported component named Self が shadow される
#734 default slot と named slot の provided children に対する virtual TypeScript assertion template codegen change なし。open slot contract と any は TypeScript 側の permissive check に degrade

entry-point と proof checklist は Experimentals Reference にあります。

Patterned Templates

patternedTemplate は RFC #823 の long-form v-match / v-when syntax を実装します。subject expression は 1 回だけ評価され、direct branch child は source order で検査され、最初に match した branch だけが render されます。

inline HTML の SFC では最外周の <template> にも v-match を指定できます。 直下の v-when は DOM・SSR・Vapor で内側の match と同じように動作し、ヘッダーの式だけの変更も template HMR を無効化します。 parse 結果の template.content は引き続きブロック本文のみです。外部 src、プリプロセッサ言語、元の source metadata が失われた descriptor はこの形式では拒否します。 Canon は後述の opt-in で分岐内 narrowing・網羅性検査に対応します。結合したコンパイラ SFC の source map は既存の script-only の制限が残ります。

Croquis は analyzeSfc(source, { experimentalPatternedTemplate: true }) と Playground の チェックボックスで root・nested match を解析できます。RFC の parser で分岐内の宣言、 外側を参照する value pattern、guard、構文診断を記録し、HTML entity を含む元の位置を維持します。 DOM・Vapor・SSR もこの parser に統一し、pattern as name を使います。旧 as const name は拒否します。 Canon も同じ parser を使い、typeCheck(source, { experimentalPatternedTemplate: true, includeVirtualTs: true })、Playground Canon のチェックボックス、experimentals.patternedTemplate を有効にした vize check で型検査できます。Native Maestro も同じ workspace 設定を読み、構文・未網羅エラーと到達不能 branch の警告を報告し、未保存の編集を再検査します。Root pattern binding の hover は絞り込んだ型を、definition は元の宣言位置を返します。workspace flag を変更したら language server を再起動してください。Content Mapper への設定伝達、guard・nested closure・or-pattern binding を含む rename/completion の全組合せの検証は未完了です。 残作業は #6176 で追跡します。

pattern の property は各 arm の試行内で一度だけ読み、guard と描画側は rest copy を含めて同じ値を使います。rest copy は shape 全体の一致後に作り、shape が不一致の arm や後続の未試行 arm の guard は評価しません。v-whenv-ifv-else-ifv-elsev-forv-match と同じ要素には指定できません。

<script setup lang="ts">
type Result =
  | { status: "success"; data: { title: string; published: boolean } }
  | { status: "error"; error: { message: string; retriable: boolean } }
  | { status: "loading" }
  | { status: "empty" };

const result = ref<Result>({ status: "loading" });
</script>

<template v-match="result">
  <ArticleView
    v-when="{ status: 'success', data: const article } if (article.published)"
    :article="article"
  />
  <RetryBanner
    v-when="{ status: 'error', error: const error } if (error.retriable)"
    :error="error"
  />
  <p v-when="{ status: 'empty' } | { status: 'loading' }">Waiting for content.</p>
  <ErrorBanner v-when="{ status: 'error', error: const error }" :error="error" />
  <p v-when="_">Unpublished article.</p>
</template>

binding は branch-local です。v-when を持つ element、attribute/directive、guard expression、children から見えます。sibling branch からは読めません。

対応している branch pattern:

Pattern Example Runtime check
Literal v-when="'ready'", v-when="404" strict equality
Value v-when="Status.Ready" identifier または member expression と NaN を含む SameValueZero で比較
Wildcard v-when="_" 常に match し、binding は導入しない
Const binding v-when="const value" 常に match し、この branch で value を bind
Object v-when="{ kind: 'ok', value: const data }" open structural object match。余分な property は許容
Object shorthand v-when="{ kind: 'ok', const data }" { const data }{ data: const data }
Object rest v-when="{ kind: 'error', ...const payload }" 残りの own enumerable property を bind。lone ... も accepted
Array / tuple v-when="[const first, ...const rest]" Array.isArray を要求。rest は追加 item を許容
Or `v-when="'idle' 'loading'"`
As binding v-when="{ kind: 'ok' } as whole" matched value を whole として bind
Guard v-when="{ error: const e } if (e.retriable)" pattern 成功後に実行

Patterned Diagnostics

flag がない場合、Vize は unknown directive として通さず、必要な opt-in を報告します。

<template v-match="status">
  <p v-when="'ready'">Ready</p>
</template>

v-whenv-match container の direct child でなければなりません。

<template v-match="status">
  <section>
    <p v-when="'ready'">Ready</p>
  </section>
</template>

unguarded top-level fallback は一意で、最後でなければなりません。

<template v-match="status">
  <p v-when="_">Other</p>
  <p v-when="'ready'">Ready</p>
  <p v-when="(_)">Again</p>
</template>

v-match には少なくとも 1 つ direct branch が必要です。v-when は directive argument や modifier を使えません。

<template v-match="status"></template>

<template v-match="status">
  <p v-when:ready="'ready'">Ready</p>
  <p v-when.once="'ready'">Ready again</p>
</template>

letvar binding は rejected です。現時点で Vize が受け付ける binding declaration は const だけです。v-when は directive argument や modifier も拒否します。v-casev-case.default は古い Vize 実験の互換 alias としてだけ残しており、新しい template では v-when_ を使います。

Patterned Type Boundary

Canon は arm の binding と元の subject を narrowing し、網羅漏れを error、到達不能 arm を warning として報告します。型アルゴリズムは通常の検証済み pattern_matching.d.ts として TypeScript program ごとに共有し、各 template に複製しません。

guard 付き arm は網羅性の証明に使いません。optional property の存在検査は値の undefined と key が存在しない余地を維持します。有限の object・tuple union、readonly array、rest binding に対応します。open primitive、anyunknown、union 型の value pattern は保守的に扱い、網羅性を証明できない場合は guard なしの v-when="_" を使います。

網羅漏れは元の subject の位置に出て vize check を失敗させます。到達不能 arm の warning だけなら成功します。WASM 単体は構造診断と virtual TypeScript を提供し、完全な型診断は Playground の Monaco TypeScript worker または native checker が行います。他の virtual-TS host は virtualTsHelpers を ambient declaration として一度だけ登録してください。

RFC 全体の upstream type-tooling acceptance criteria に含まれる editor navigation・completion までは未認定です。or-pattern alternative 内の binding も deferred なので、binding が必要な場合は branch を分けます。?=|=~= は public branch syntax ではありません。

In-Tag Comments

inTagComment は RFC #831 の opening tag attribute list 内 compile-time-only // comment を実装します。Vize は parser/tooling fidelity のために source text を保持し、runtime code は emit しません。

<template>
  <LegacySelect
    :options="options"
    // @vue-expect-error legacy API accepts string IDs
    :selected-id="selectedId"
  />
</template>

置ける場所は tag name の後、complete attribute/directive の間、最後の attribute の後かつ > / /> の前です。trailing comment の後では closing delimiter を次の行に置いてください。>/> を同じ行に置くと // が closing delimiter まで comment として消費します。この syntax は tag name、attribute name、directive name、argument、modifier、attribute value の中では使えません。

comment は template root の comments list に CommentKind::InTag として保存されます。element prop ではなく、child <!-- ... --> node でもなく、通常の template comments option にも左右されません。comments: false でも tooling 用の in-tag comment は保持されます。attribute value 内の // は通常の text のままです。

<template>
  <a href="https://example.test/a//b">Link</a>
</template>

この syntax は template を自分で parse する SFC/tooling pipeline 向けです。raw in-DOM template は先に browser に parse されるため、そこで in-tag comment に依存しないでください。

Self Component

selfComponent は RFC #833 の exact tag <Self> を current component 用に予約します。

<template>
  <li>
    {{ node.id }}
    <ul v-if="node.children.length">
      <Self v-for="child in node.children" :key="child.id" :node="child" />
    </ul>
  </li>
</template>

resolution rule:

Context Vize が <Self> を解決する方法
compiler metadata がある SFC build metadata の current component name を使う
metadata がない SFC build filename から component name を derive
direct template API componentName が渡されていれば使う
name がない 通常の component resolution semantics を維持

flag が有効な場合、<Self> は local、imported、global component named Self より先に解決されます。別 component を使いたい場合は import 名を Self にしないでください。props、attrs、events、directives、refs、v-ifv-forv-show、slot は通常の component usage と同じです。recursive template には termination condition が必要です。

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

<template>
  <Self />
  <OtherSelf />
</template>

tag は exact かつ case-sensitive です。flag が有効な場合だけ <Self> が予約されます。<self> は特別ではありません。render function と JSX はこの flag の対象外です。

SFC filename がない low-level template call では、明示的に name を渡してください。

import { compile } from "@vizejs/native";

compile("<Self :node=\"node\" />", {
  componentName: "TreeNode",
  experimentalSelfComponent: true,
});

Strict Slot Children

strictSlotChildren は RFC #734 の tooling 側を実装します。parent が component slot に渡す child node に対して virtual TypeScript assertion を追加します。template codegen や runtime rendering は変えません。

import TabItem from "./TabItem.vue";

declare const Tabs: {
  readonly __vizeSlots?: {
    default: () => [typeof TabItem, HTMLButtonElement];
    footer: () => [HTMLButtonElement];
  };
};

同じ public contract は SFC の defineSlots からも得られます。

defineSlots<{
  default(): [typeof TabItem, HTMLButtonElement];
  footer(): [HTMLButtonElement];
}>();
<template>
  <Tabs>
    <TabItem />
    <button>Open</button>

    <template #footer>
      <button>Save</button>
    </template>
  </Tabs>
</template>

flag が有効な場合、Vize は provided default slot を __VizeProvidedSlotChildren<[typeof TabItem, HTMLButtonElement]> と表現し、component の __vizeSlots.default return type と比較するよう TypeScript に渡します。mismatch した child は TypeScript diagnostic になります。

<template>
  <Tabs>
    <div />
  </Tabs>
</template>

child mapping:

Provided child Virtual TypeScript type
<button> などの native element HTMLButtonElement
SVG または MathML element 狭い DOM type が分からない場合は SVGElement または MathMLElement fallback
<TabItem> などの imported component typeof TabItem
text または interpolation string
named <template #footer> named slot と比較
comment と structural-only wrapper child value を持たない場合は skip

RFC #734 の return shape は TypeScript cardinality contract として保持します。

Slot return contract Vize virtual TS での意味
() => HTMLInputElement input child 1 つ。single-child shorthand を受け付ける
() => HTMLInputElement[] input child 0 個以上
() => [HTMLInputElement, HTMLInputElement] tuple order どおり input child 2 つ
() => (typeof TabItem)[] TabItem component child 0 個以上
() => [typeof TabItem, HTMLButtonElement] TabItem child 1 つ、その後に button child 1 つ

__VizeProvidedSlotChildren<__T> は、slot contract の accepted child type が 1 つの場合、single child を Only または [Only] のどちらとしても受け付けます。multi-child contract では tuple/array shape を保ちます。open slot index signature と anyany に degrade するため、TypeScript は strict child diagnostic を出しません。built-in、dynamic component、KeepAliveTeleportTransitionTransitionGroupSuspense は現時点では strict component child type に寄与しません。

slot contract が props に依存する advanced component library は __vizeResolveSlots を expose できます。defineSlots を使う component は parent のために __vizeSlots marker を export します。required slot-name check は別の仕組みです。この flag は既存の virtual-TS slot model に child node type check を追加します。

Verification Checklist

新しい experimental behavior を document する前に、次の事実を test で固定してください。

  • flag の default は off
  • shared config と direct plugin option は document された precedence で解決される
  • direct compiler field は final boolean
  • low-level docs は実在 entry point の compilecompileVaporparseTemplatecompileSfc* を名前で示す
  • bad syntax は黙って compile されず diagnostic になる
  • DOM、SSR、Vapor、SFC、WASM、または vize check coverage が surface matrix と一致する
  • docs には good example、flag-off または invalid-placement example、implementation boundary がある