Web Components のパッケージ配布戦略:types/CEM(Custom Elements Manifest)/ドキュメント自動
Web Components を作成したら、次に考えるべきはパッケージとして配布する方法ですよね。 ただコンポーネントを公開するだけではなく、TypeScript の型定義やドキュメントを充実させることで、開発者体験(DX)を大きく向上できます。
本記事では、Web Components を npm パッケージとして配布する際に必須となる 3 つの要素――型定義(types)、CEM(Custom Elements Manifest)、ドキュメント自動生成について詳しく解説していきます。 これらを適切に設定することで、あなたのコンポーネントライブラリは格段に使いやすくなるでしょう。
背景
Web Components パッケージ配布の重要性
Web Components は、フレームワークに依存しない再利用可能なコンポーネントを作成できる標準技術です。 そのため、npm パッケージとして配布することで、React、Vue、Angular、Svelte など、あらゆるフロントエンド環境で利用できるようになります。
しかし、単にコンポーネントのコードを公開するだけでは不十分なんですね。 利用者が快適に使えるようにするには、適切なメタデータやドキュメントの提供が欠かせません。
開発者体験を左右する 3 要素
モダンなパッケージ配布では、以下の 3 つが開発者体験の質を決定づけます。
**型定義(types)**は、TypeScript ユーザーにとって必須です。 エディタ上で補完が効き、型チェックが機能することで、バグを未然に防げますし、学習コストも下がります。
**CEM(Custom Elements Manifest)**は、Web Components 専用のメタデータフォーマットです。 プロパティ、イベント、スロット、CSS 変数などの情報を構造化して記述することで、ツールやエディタが自動的に情報を読み取れるようになります。
ドキュメント自動生成により、メンテナンスコストを抑えつつ、常に最新の API リファレンスを提供できるんですね。 手動でドキュメントを書くと、コードとドキュメントが乖離しがちですが、自動生成なら安心です。
下記の図は、Web Components パッケージ配布における各要素の関係性を示しています。
mermaidflowchart TD
source["Web Components<br/>ソースコード"] --> analyzer["Analyzer<br/>(CLI/Plugin)"]
analyzer --> types["型定義ファイル<br/>(*.d.ts)"]
analyzer --> cem["CEM<br/>(custom-elements.json)"]
cem --> docs["ドキュメント<br/>自動生成"]
types --> editor["エディタ補完<br/>型チェック"]
cem --> editor
cem --> storybook["Storybook<br/>連携"]
docs --> publish["公開<br/>(npm/GitHub)"]
types --> publish
cem --> publish
図で理解できる要点
- ソースコードから型定義と CEM を自動生成
- CEM は複数のツールと連携可能
- 最終的に npm パッケージとして公開される流れ
課題
手動メンテナンスの限界
Web Components を配布する際、型定義やドキュメントを手動で作成・更新するのは非常に大変です。 コンポーネントが増えるたびに、型定義ファイル(.d.ts)を書き、API ドキュメントを更新し、サンプルコードを追加しなければなりません。
特に、プロパティやメソッドに変更があった場合、すべての関連ファイルを同期させる必要があります。 これは人的ミスを招きやすく、コードとドキュメントが乖離する原因になってしまいますね。
TypeScript 環境での型情報不足
JavaScript で書かれた Web Components を npm で公開した場合、TypeScript ユーザーは型情報が得られず、エディタの補完が効きません。 これでは、せっかくのコンポーネントもその魅力を十分に発揮できないでしょう。
型定義を提供しないと、利用者は以下のような問題に直面します。
- エディタでプロパティ名が補完されない
- 型チェックが機能せず、実行時エラーが発生する
- JSDoc コメントが表示されず、使い方がわからない
ツール連携の困難さ
Web Components は HTML 標準として動作しますが、そのメタデータを機械的に読み取る標準的な方法がありませんでした。 そのため、Storybook や IDE プラグインなどのツールが、コンポーネントの情報を自動的に認識できないという問題があります。
例えば、Storybook でコントロールパネルを自動生成したくても、プロパティの型や初期値がわからなければ手動設定が必要になってしまいます。 こうした非効率な作業は、開発速度を著しく低下させる要因となります。
次の図は、適切なメタデータがない場合の課題を示しています。
mermaidflowchart LR
component["Web Component"] -.->|"型情報なし"| typescript["TypeScript<br/>ユーザー"]
component -.->|"メタデータなし"| storybook["Storybook"]
component -.->|"ドキュメントなし"| developer["開発者"]
typescript -->|"補完効かない"| problem1["課題1:<br/>DX低下"]
storybook -->|"手動設定必要"| problem2["課題2:<br/>効率悪化"]
developer -->|"使い方不明"| problem3["課題3:<br/>学習コスト増"]
図で理解できる要点
- 型情報がないと TypeScript ユーザーが困る
- メタデータがないとツール連携が困難
- ドキュメント不足は学習コストを増大させる
解決策
TypeScript 型定義の提供
TypeScript で型定義を提供する方法は、主に 2 つあります。
1 つ目は、TypeScript でコンポーネントを実装し、コンパイル時に.d.tsファイルを自動生成する方法です。
これが最もシンプルで確実な方法ですね。
2 つ目は、JavaScript で書かれたコンポーネントに対して JSDoc コメントを記述し、TypeScript コンパイラで型定義を生成する方法です。 既存の JavaScript コードベースを維持しながら型定義を提供できます。
どちらの方法でも、package.jsonに型定義ファイルの場所を明示することが重要です。
Custom Elements Manifest(CEM)の導入
CEM は、Web Components のメタデータを標準化した JSON 形式のマニフェストファイルです。 このファイルには、以下のような情報が含まれます。
- カスタム要素の名前とタグ名
- プロパティ(型、デフォルト値、説明)
- メソッド(引数、戻り値、説明)
- イベント(型、説明)
- スロット(名前、説明)
- CSS 変数(名前、デフォルト値、説明)
- CSS パーツ(名前、説明)
CEM を生成するツールとしては、@custom-elements-manifest/analyzerが広く使われています。
このツールは、ソースコードを解析して CEM を自動生成してくれるんですね。
ドキュメント自動生成の仕組み
CEM があれば、そこからドキュメントを自動生成できます。 主要なツールとして、以下があります。
web-component-analyzerは、コンポーネントを解析して Markdown ドキュメントを生成します。
Custom Elements Manifest to Markdownは、CEM ファイルから Markdown を生成する専用ツールです。
Storybookは、CEM を読み込んでコントロールパネルや API ドキュメントを自動生成できます。
こうした自動化により、コードを更新するだけでドキュメントも最新に保たれるようになります。
下記の図は、解決策の全体像を示しています。
mermaidflowchart TD
source["ソースコード<br/>(TS or JS+JSDoc)"] --> tsc["TypeScriptコンパイラ"]
source --> analyzer["CEM Analyzer"]
tsc --> dts["型定義ファイル<br/>(*.d.ts)"]
analyzer --> cem["custom-elements.json"]
cem --> md["Markdown生成"]
cem --> sb["Storybook連携"]
dts --> pkg["npm package"]
cem --> pkg
md --> pkg
pkg --> user["利用者"]
user -->|"補完/型チェック"| better_dx["DX向上"]
図で理解できる要点
- TypeScript コンパイラと CEM Analyzer を併用
- 型定義と CEM を同時に生成可能
- 生成されたメタデータからドキュメントを自動生成
- 最終的に npm パッケージとして配布
具体例
プロジェクトのセットアップ
まずは、プロジェクトの初期化から始めましょう。 ここでは Yarn を使ってプロジェクトを作成します。
bash# プロジェクトディレクトリを作成
mkdir my-web-components
cd my-web-components
# Yarnプロジェクトを初期化
yarn init -y
次に、必要なパッケージをインストールします。
bash# TypeScriptと開発用ツールをインストール
yarn add -D typescript @custom-elements-manifest/analyzer
これで基本的な環境が整いました。
TypeScript でコンポーネントを実装
TypeScript でシンプルなボタンコンポーネントを作成してみましょう。 まずは基本的なクラス定義から始めます。
typescript// src/my-button.ts
/**
* カスタマイズ可能なボタンコンポーネント
* @element my-button
* @fires click - ボタンがクリックされたときに発火
* @slot - ボタン内に表示するコンテンツ
* @csspart button - ボタン要素のスタイルをカスタマイズ
*/
export class MyButton extends HTMLElement {
// プロパティの内部状態を保持
private _variant: 'primary' | 'secondary' = 'primary';
private _disabled: boolean = false;
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
上記のコードでは、JSDoc コメントを使ってコンポーネントのメタデータを記述しています。
@elementタグでカスタム要素名を、@firesでイベントを、@slotでスロットを、@csspartで CSS パーツを定義しているんですね。
次に、プロパティを定義します。
typescript /**
* ボタンのスタイルバリアント
* @type {'primary' | 'secondary'}
* @default 'primary'
*/
get variant(): 'primary' | 'secondary' {
return this._variant;
}
set variant(value: 'primary' | 'secondary') {
this._variant = value;
this.setAttribute('variant', value);
this.render();
}
/**
* ボタンの無効化状態
* @type {boolean}
* @default false
*/
get disabled(): boolean {
return this._disabled;
}
set disabled(value: boolean) {
this._disabled = value;
if (value) {
this.setAttribute('disabled', '');
} else {
this.removeAttribute('disabled');
}
this.render();
}
プロパティにも JSDoc コメントで型と説明を記述します。 これにより、CEM 生成時に詳細な情報が含まれるようになるんですね。
次に、ライフサイクルメソッドとレンダリング処理を実装しましょう。
typescript connectedCallback() {
// 初期レンダリング
this.render();
// 属性からプロパティを初期化
if (this.hasAttribute('variant')) {
this.variant = this.getAttribute('variant') as 'primary' | 'secondary';
}
if (this.hasAttribute('disabled')) {
this.disabled = true;
}
}
/**
* ボタンをレンダリング
* @private
*/
private render() {
if (!this.shadowRoot) return;
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-block;
}
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button.primary {
background-color: #007bff;
color: white;
}
button.secondary {
background-color: #6c757d;
color: white;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
<button
part="button"
class="${this.variant}"
?disabled="${this.disabled}"
>
<slot></slot>
</button>
`;
}
}
renderメソッドで Shadow DOM の内容を構築します。
part="button"により、外部からスタイルをカスタマイズできるようになっています。
最後に、カスタム要素として登録します。
typescript// カスタム要素として登録
customElements.define('my-button', MyButton);
これで TypeScript によるコンポーネント実装は完了です。
TypeScript 設定ファイルの作成
型定義ファイルを自動生成するために、TypeScript の設定ファイルを作成します。
json{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
重要なのは"declaration": trueの設定です。
これにより、コンパイル時に.d.tsファイルが自動生成されます。
CEM 設定ファイルの作成
次に、Custom Elements Manifest 生成のための設定ファイルを作成しましょう。
javascript// custom-elements-manifest.config.js
export default {
// 解析対象のファイルパターン
globs: ['src/**/*.ts'],
// 除外するファイルパターン
exclude: ['src/**/*.spec.ts', 'src/**/*.test.ts'],
// 出力ファイル名(デフォルト: custom-elements.json)
outdir: '.',
// プラグイン設定
plugins: [],
};
この設定により、srcディレクトリ配下の TypeScript ファイルが解析対象となります。
テストファイルは除外しているんですね。
package.json の設定
npm 配布に必要なpackage.jsonの設定を行いましょう。
json{
"name": "my-web-components",
"version": "1.0.0",
"description": "カスタマイズ可能なWeb Componentsライブラリ",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"customElements": "custom-elements.json",
"files": ["dist", "custom-elements.json"],
"scripts": {
"build": "yarn build:ts && yarn build:cem",
"build:ts": "tsc",
"build:cem": "cem analyze --litelement",
"prepublishOnly": "yarn build"
},
"keywords": [
"web-components",
"custom-elements",
"typescript"
],
"author": "Your Name",
"license": "MIT",
"devDependencies": {
"@custom-elements-manifest/analyzer": "^0.9.0",
"typescript": "^5.3.0"
}
}
重要なポイントがいくつかあります。
"types"フィールドで型定義ファイルの場所を指定"customElements"フィールドで CEM ファイルの場所を指定"files"フィールドで配布に含めるファイルを明示"prepublishOnly"スクリプトで公開前に自動ビルド
これらの設定により、npm パッケージとして公開した際に、利用者が型定義と CEM を自動的に取得できるようになります。
ビルドと動作確認
それでは、実際にビルドしてみましょう。
bash# TypeScriptコンパイルとCEM生成を実行
yarn build
このコマンドを実行すると、以下のファイルが生成されます。
dist/my-button.js- コンパイルされた JavaScriptdist/my-button.d.ts- 型定義ファイルcustom-elements.json- Custom Elements Manifest
生成されたcustom-elements.jsonの一部を見てみましょう。
json{
"schemaVersion": "1.0.0",
"readme": "",
"modules": [
{
"kind": "javascript-module",
"path": "src/my-button.ts",
"declarations": [
{
"kind": "class",
"description": "カスタマイズ可能なボタンコンポーネント",
"name": "MyButton",
"members": [
{
"kind": "field",
"name": "variant",
"type": {
"text": "'primary' | 'secondary'"
},
"default": "'primary'",
"description": "ボタンのスタイルバリアント"
},
{
"kind": "field",
"name": "disabled",
"type": {
"text": "boolean"
},
"default": "false",
"description": "ボタンの無効化状態"
}
],
"events": [
{
"name": "click",
"type": {
"text": "Event"
},
"description": "ボタンがクリックされたときに発火"
}
],
"slots": [
{
"description": "ボタン内に表示するコンテンツ",
"name": ""
}
],
"cssProperties": [],
"cssParts": [
{
"description": "ボタン要素のスタイルをカスタマイズ",
"name": "button"
}
],
"tagName": "my-button"
}
],
"exports": [
{
"kind": "custom-element-definition",
"name": "my-button",
"declaration": {
"name": "MyButton"
}
}
]
}
]
}
この JSON 形式のメタデータには、コンポーネントの全情報が構造化されて含まれています。 プロパティ、イベント、スロット、CSS パーツなど、すべてが機械可読な形式で記述されているんですね。
Storybook との連携
CEM を活用すれば、Storybook との連携も簡単です。 まず、必要なパッケージをインストールしましょう。
bash# Storybookと関連パッケージをインストール
yarn add -D @storybook/web-components @storybook/addon-essentials
yarn add -D storybook-addon-custom-elements-manifest
次に、Storybook の設定ファイルで CEM を読み込みます。
javascript// .storybook/preview.js
import { setCustomElementsManifest } from '@storybook/web-components';
import customElements from '../custom-elements.json';
// CEMをStorybookに登録
setCustomElementsManifest(customElements);
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
この設定により、Storybook がコンポーネントのメタデータを自動的に読み取ります。
ストーリーファイルを作成してみましょう。
typescript// src/my-button.stories.ts
import type {
Meta,
StoryObj,
} from '@storybook/web-components';
import './my-button';
// Storybookのメタ設定
const meta: Meta = {
title: 'Components/MyButton',
component: 'my-button',
tags: ['autodocs'],
argTypes: {
// CEMから自動的に生成されるため、手動定義は不要
// ただし、カスタマイズしたい場合は上書き可能
variant: {
control: 'select',
options: ['primary', 'secondary'],
},
disabled: {
control: 'boolean',
},
},
};
export default meta;
type Story = StoryObj;
// デフォルトストーリー
export const Primary: Story = {
args: {
variant: 'primary',
disabled: false,
},
render: (args) => `
<my-button
variant="${args.variant}"
?disabled="${args.disabled}"
>
クリック
</my-button>
`,
};
// セカンダリバリアント
export const Secondary: Story = {
args: {
variant: 'secondary',
disabled: false,
},
render: (args) => `
<my-button
variant="${args.variant}"
?disabled="${args.disabled}"
>
キャンセル
</my-button>
`,
};
// 無効化状態
export const Disabled: Story = {
args: {
variant: 'primary',
disabled: true,
},
render: (args) => `
<my-button
variant="${args.variant}"
?disabled="${args.disabled}"
>
無効
</my-button>
`,
};
CEM があることで、Storybook のコントロールパネルやドキュメントが自動生成されます。 手動で設定を書く必要がほとんどなくなるんですね。
ドキュメント自動生成
最後に、CEM から Markdown ドキュメントを自動生成してみましょう。
cem-plugin-markdownプラグインを使います。
bash# プラグインをインストール
yarn add -D cem-plugin-markdown
CEM 設定ファイルにプラグインを追加します。
javascript// custom-elements-manifest.config.js
import { markdownPlugin } from 'cem-plugin-markdown';
export default {
globs: ['src/**/*.ts'],
exclude: ['src/**/*.spec.ts', 'src/**/*.test.ts'],
outdir: '.',
plugins: [
markdownPlugin({
// ドキュメント出力先
outdir: 'docs',
// コンポーネントごとにファイル分割
split: true,
}),
],
};
ビルドを実行すると、docsディレクトリ配下に Markdown ファイルが生成されます。
bash# ドキュメント生成
yarn build:cem
生成されたdocs/my-button.mdの例です。
markdown# my-button
カスタマイズ可能なボタンコンポーネント
# Properties
| Property | Attribute | Type | Default | Description |
| ---------- | ---------- | -------------------------- | ----------- | -------------------------- |
| `variant` | `variant` | `'primary' \| 'secondary'` | `'primary'` | ボタンのスタイルバリアント |
| `disabled` | `disabled` | `boolean` | `false` | ボタンの無効化状態 |
# Events
| Event | Type | Description |
| ------- | ------- | -------------------------------- |
| `click` | `Event` | ボタンがクリックされたときに発火 |
# Slots
| Name | Description |
| --------- | ---------------------------- |
| (default) | ボタン内に表示するコンテンツ |
# CSS Parts
| Part | Description |
| -------- | ---------------------------------- |
| `button` | ボタン要素のスタイルをカスタマイズ |
# Usage
```html
<my-button variant="primary">クリック</my-button>
<my-button variant="secondary" disabled>無効</my-button>
```
このように、コードの JSDoc コメントから完全な API ドキュメントが自動生成されます。 コードを更新すれば、ドキュメントも自動的に最新化されるため、メンテナンスコストが大幅に削減できるんですね。
下記の図は、具体例で構築したワークフロー全体を示しています。
mermaidflowchart LR
source["my-button.ts<br/>(TypeScript+JSDoc)"]
source --> tsc["tsc<br/>(TypeScriptコンパイラ)"]
source --> cem_cli["cem analyze<br/>(CEM生成)"]
tsc --> js["my-button.js"]
tsc --> dts["my-button.d.ts"]
cem_cli --> cem["custom-elements.json"]
cem_cli --> md["docs/my-button.md"]
cem --> storybook["Storybook"]
dts --> ide["IDE補完"]
md --> github["GitHub Pages"]
js --> npm["npm publish"]
dts --> npm
cem --> npm
図で理解できる要点
- 単一のソースコードから複数の成果物を生成
- TypeScript コンパイラと CEM Analyzer を並列実行
- 生成物がそれぞれ異なる用途で活用される
- 最終的に npm パッケージとして統合される
まとめ
Web Components を npm パッケージとして配布する際には、型定義、CEM、ドキュメント自動生成の 3 要素が不可欠です。
型定義を提供することで、TypeScript ユーザーがエディタ補完や型チェックの恩恵を受けられます。 CEM を生成することで、Storybook などのツールがコンポーネントのメタデータを自動的に読み取れるようになりますね。 そして、ドキュメント自動生成により、常に最新の API リファレンスを低コストで維持できるんです。
これらを適切に設定することで、あなたのコンポーネントライブラリは以下のメリットを得られます。
- 開発者体験の向上 - 補完、型チェック、ドキュメントが揃う
- メンテナンスコストの削減 - 自動生成により手動更新が不要
- ツール連携の強化 - Storybook、IDE など様々なツールと統合
- 採用促進 - 使いやすいライブラリは自然と広まる
初期設定には多少の手間がかかりますが、一度構築すれば長期的に大きな価値を生み出すでしょう。 ぜひ、あなたの Web Components プロジェクトでも実践してみてください。
関連リンク
- Custom Elements Manifest - CEM 公式リポジトリ
- @custom-elements-manifest/analyzer - CEM Analyzer 公式ドキュメント
- TypeScript 公式サイト - TypeScript 公式ドキュメント
- Storybook for Web Components - Storybook 公式ガイド
- Open Web Components - Web Components ベストプラクティス集
- MDN Web Components - Web Components 基礎ドキュメント
articleWeb Components イベント設計チート:`CustomEvent`/`composed`/`bubbles` 実例集
articleWeb Components のポリフィル戦略:@webcomponents 系を最小限で入れる判断基準
articleWeb Components と Declarative Shadow DOM の現在地:HTML だけで描くサーバー発 UI
articleWeb Components のパッケージ配布戦略:types/CEM(Custom Elements Manifest)/ドキュメント自動
articleWeb Components が “is not a constructor” で落ちる時:定義順序と複数登録の衝突を解決
articleWeb Components vs Lit:素の実装とフレームワーク補助の DX/サイズ/速度を実測比較
articleZod 合成パターン早見表:`object/array/tuple/record/map/set/intersection` 実例集
articleバックアップ戦略の決定版:WordPress の世代管理/災害復旧の型
articleYarn 運用ベストプラクティス:lockfile 厳格化・frozen-lockfile・Bot 更新方針
articleWebSocket のペイロード比較:JSON・MessagePack・Protobuf の速度とコスト検証
articleWeb Components イベント設計チート:`CustomEvent`/`composed`/`bubbles` 実例集
articleWebRTC SDP 用語チートシート:m=・a=・bundle・rtcp-mux を 10 分で総復習
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来