コンテンツにスキップ

CEMアナライザープラグイン

CEM(custom-elements.json)とは、パッケージが定義する要素(タグ、attribute、property、イベント、slot)を標準的な形式で記述したものであり、これによりツールはコンポーネントを実行することなく読み取ることができます。アナライザーとそのプラグインAPIについてはcustom-elements-manifest.open-wc.orgを、ファイル形式そのものについてはスキーマと仕様を参照してください。

CEMアナライザー(@custom-elements-manifest/analyzer)は、デフォルトでは static props を1つの静的フィールドとして読み取り、それに対してattributeを出力しません。これにより、Storybookやコードエディタの自動補完のようなツールには読み取るものが何もなくなってしまいます。

私たちの web-component-base/cem-plugin を使うと、CEMアナライザーは各propを型付きのマニフェストattributeと対応する公開フィールドに展開します。以下の手順では、CEMアナライザーをインストールし、ツールが wcb のカスタム要素を扱えるように設定する方法を説明します。

Terminal window
npm i -D @custom-elements-manifest/analyzer
custom-elements-manifest.config.mjs
import { wcbStaticProps } from 'web-component-base/cem-plugin'
export default {
globs: ['src/**/*.{js,ts}'],
outdir: '.',
plugins: [wcbStaticProps()],
}

そしてアナライザーを実行します。

Terminal window
npx cem analyze

次のようなコンポーネントがあるとします。

src/cozy-button.ts
import { WebComponent, html } from 'web-component-base'
type CozyButtonProps = {
variant: 'primary' | 'ghost'
disabled: boolean
maxCount: number
}
export class CozyButton extends WebComponent<CozyButtonProps> {
static props: CozyButtonProps = {
variant: 'primary',
disabled: false,
maxCount: 3,
}
static shadowRootInit = { mode: 'open' }
static styles = ':host { display: inline-block }'
get template() {
return html`<button>${this.props.variant}</button>`
}
}
customElements.define('cozy-button', CozyButton)

custom-elements.json は、propごとに型付きのattributeと対応する公開フィールドを得ます。

attributeフィールドデフォルト値
variantstringvariant'primary'
disabledbooleandisabledfalse
max-countnumbermaxCount3

…そして propsshadowRootInitstylesstrictPropsobservedAttributestemplate は公開サーフェスから取り除かれます。

知っておく価値のある2つの詳細があります。

  • 型はデフォルトのリテラルから決まります: true/falseboolean、数値 → number、object/array → object、それ以外はすべて → string。TypeScriptの型注釈は参照されないため、variant のユニオン型もマニフェストでは string になります。
  • attribute名はwcb自身の getKebabCase から生成されます。これは observedAttributes が使うのと同じ関数なので、マニフェスト上の名前がコンポーネントの実際の観測対象からずれることはありません。

Storybookのweb-componentsレンダラーは、Custom Elements Manifestからautodocsとcontrolsを構築します。

.storybook/preview.js
import { setCustomElementsManifest } from '@storybook/web-components-vite'
import manifest from '../custom-elements.json'
setCustomElementsManifest(manifest)
export default { tags: ['autodocs'] }

storyをタグ名に紐づけると、Storybookは残りを推測し、variant にはテキストフィールド、disabled にはトグル、maxCount には数値入力を提供します。

cozy-button.stories.js
import { html } from 'lit'
import '../src/cozy-button.ts'
export default {
title: 'Cozy/Button',
component: 'cozy-button', // ← argTypesは不要
render: ({ variant, disabled, maxCount }) => html`
<cozy-button
variant=${variant}
?disabled=${disabled}
max-count=${maxCount}
></cozy-button>
`,
}
export const Default = {
args: { variant: 'primary', disabled: false, maxCount: 3 },
}

完全に動作するセットアップについては、wcbリポジトリ内のstorybook/を参照してください。これはデモコンポーネントに対してこの設定を実際に実行しています。

custom-elements.json が存在すれば、エディタはプラグインが読み取るのと同じ static props に基づいて、コンポーネントのタグ名やattributeの自動補完を提供できます。そのため、ヒントがコードからずれることはありません。

まず、package.json にマニフェストファイルを宣言します。ルート2の言語サーバーはこのフィールドを通してそれを検出しますし、これは他のマニフェスト駆動のツールが探すエコシステムの慣習でもあります。

{
"customElements": "custom-elements.json"
}

ルート1: 拡張機能なしのネイティブVS Code

Section titled “ルート1: 拡張機能なしのネイティブVS Code”

VS Codeの組み込みHTML言語サービスは、独自のcustom data形式を読み取ります。第2のアナライザープラグインがマニフェストをこの形式に変換するため、両方のファイルを1回の cem analyze 実行から得られます。

Terminal window
npm i -D cem-plugin-vs-code-custom-data-generator
custom-elements-manifest.config.mjs
import { wcbStaticProps } from 'web-component-base/cem-plugin'
import { generateCustomData } from 'cem-plugin-vs-code-custom-data-generator'
export default {
globs: ['src/**/*.{js,ts}'],
outdir: '.',
plugins: [wcbStaticProps(), generateCustomData()],
}
.vscode/settings.json
{
"html.customData": ["./vscode.html-custom-data.json"]
}

VS Codeを再起動すると、HTMLファイル内で <cozy- を入力すると補完され、variant / disabled / max-count がattributeとして提示され、ホバー時にはその型とデフォルト値が表示されます。

注意点: html.customData.html ファイルにしか適用されません。wcbのコンポーネントは .js / .ts 内の html タグ付きテンプレートでマークアップを記述しますが、これらはHTML言語サービスを通りません。このルートは、あなたのコンポーネントに対してプレーンなHTMLページを書く人にとっては役立ちますが、あなた自身のテンプレート内では機能しません。

ルート2: タグ付きテンプレート向けの言語サーバー拡張機能

Section titled “ルート2: タグ付きテンプレート向けの言語サーバー拡張機能”

タグ付きテンプレートの内側で同じ補完を得るには、それらを理解できる拡張機能が必要です。現時点で最も現行に沿った選択肢はCustom Elements Manifest Language Serverpwrs.cem-language-server-vscode)です。これはJSとTSの両方でテンプレートリテラル内のタグ名とattributeを自動補完し、attributeやデフォルト値のホバードキュメントを追加し、.vscode/settings.json を必要とせず、上記の customElements フィールドを通じてマニフェストを検出します。

もう2つの選択肢があり、それぞれの現状を知っておく価値があります。

  • wc-toolkit/wc-language-server: VS CodeとJetBrainsに対応し、こちらもマニフェスト駆動です。自称アルファ版かつ実験的とのことです。
  • Matsuuu.custom-elements-language-server-project: 古い記事で最もよく見かけるものです。アルファ版であり、そのリポジトリは2026年1月にアーカイブされました。そのため、上記の2つのいずれかを優先してください。

パッケージにマニフェストを同梱する: distPaths()

Section titled “パッケージにマニフェストを同梱する: distPaths()”

コンポーネントをnpmに公開する予定ですか? アナライザーは各モジュールの path を、スキャンした正確なファイルで刻印します。つまり globs: ['src/**/*.ts'] の場合、custom-elements.json 内のすべての path.ts ソースファイルになります。ほとんどのパッケージはビルド済みの dist/ 出力のみを公開します(そもそも実行時に .tsimport することもできません)。そのため、そのマニフェストを読み取る消費者は、tarballに含まれていないパスを解決してしまうことになります。

wcbStaticProps() の後に distPaths() を追加すると、それらのパスをビルド済み出力に書き換えます。ゼロコンフィグでは src/dist/ にマッピングし、TypeScriptの拡張子を出力されたJS形式に変換します(.ts.js.mts.mjs.cts.cjs)。それ以外の拡張子はそのまま通過するため、.js のソースはディレクトリだけが入れ替わります。

custom-elements-manifest.config.mjs
import { wcbStaticProps, distPaths } from 'web-component-base/cem-plugin'
export default {
globs: ['src/**/*.ts'],
outdir: '.',
plugins: [wcbStaticProps(), distPaths()],
}

異なるレイアウトの場合は、rootDir / outDir をオーバーライドしてください: distPaths({ rootDir: 'lib', outDir: 'dist/esm' })。書き換えられたパスが指す dist/ ファイルが実際に存在するよう、ビルドの後に cem analyze を実行してください。ext の拡張子リマップについてはAPIリファレンスを参照してください。マニフェストを公開しない場合(例えば、自分自身のソースに対するローカルのStorybookやエディタのセットアップなど)は、これらは一切不要です。