Vendure開発ハンドブック¶
このドキュメントは、Vendureでの開発に必要なコアコンセプトと高度な実装パターンを体系的にまとめた知識ベースです。プロジェクト固有のセットアップについては Vendure開発ガイド を参照してください。
目次¶
ヘッドレスアーキテクチャとGraphQL¶
Vendureは完全なヘッドレスアーキテクチャを採用しており、すべての機能はGraphQL APIを通じて提供されます。
2つのAPIエンドポイント¶
- Shop API (
/shop-api): ストアフロント(顧客向け)用のAPI - 認証: Customer認証(Cookie/Bearer Token)
- 用途: 商品閲覧、カート操作、注文作成
-
権限: 顧客がアクセス可能なデータのみ
-
Admin API (
/admin-api): 管理画面(管理者向け)用のAPI - 認証: Administrator認証(Cookie/Bearer Token)
- 用途: 商品管理、注文管理、顧客管理、設定変更
- 権限: Role-Based Access Control (RBAC)
GraphQLスキーマの自動生成¶
Vendureはプラグインで定義されたGraphQLスキーマを自動的に統合し、単一のスキーマとして提供します。
import { gql } from "graphql-tag";
const myGraphqlSchema = gql`
extend type Query {
myQuery: String!
}
extend type Mutation {
myMutation(input: String!): Boolean!
}
`;
@VendurePlugin({
shopApiExtensions: {
schema: myGraphqlSchema,
resolvers: [MyResolver],
},
})
export class MyPlugin {}
オニオンアーキテクチャとサービスレイヤーパターン¶
Vendureはオニオンアーキテクチャ(Onion Architecture)を採用し、依存関係の方向を明確にしています。
レイヤー構造¶
┌─────────────────────────────────────┐
│ GraphQL Resolvers (API Layer) │
├─────────────────────────────────────┤
│ Services (Business Logic) │
├─────────────────────────────────────┤
│ Entities (Domain Models) │
├─────────────────────────────────────┤
│ TypeORM (Data Access) │
└─────────────────────────────────────┘
依存関係の原則¶
- 外側から内側への依存: Resolver → Service → Entity
- 内側から外側への依存は禁止: EntityがServiceやResolverに依存しない
- DIコンテナ: NestJSの依存性注入システムを使用
プラグインシステムの全体像¶
Vendureのすべての機能はプラグインとして実装されます。プラグインは以下の要素で構成されます:
プラグインの構成要素¶
@VendurePlugin({
// 1. エンティティ定義
entities: [MyCustomEntity],
// 2. サービス・プロバイダー
imports: [PluginCommonModule],
providers: [MyService],
// 3. GraphQL API拡張
shopApiExtensions: {
schema: shopSchema,
resolvers: [ShopResolver],
},
adminApiExtensions: {
schema: adminSchema,
resolvers: [AdminResolver],
},
// 4. 設定の拡張
configuration: (config) => {
config.customFields.Customer.push({
/* ... */
});
return config;
},
})
export class MyPlugin {}
コアコンセプト詳細¶
Request Context (ctx)¶
RequestContextは、すべてのVendure操作の中心となるオブジェクトです。リクエストごとに作成され、言語、チャネル、ユーザー情報などのコンテキストを保持します。
RequestContextの主要プロパティ¶
interface RequestContext {
// チャネル情報
channel: Channel;
channelId: ID;
// 言語情報
languageCode: LanguageCode;
// ユーザー情報
user?: User;
activeUserId?: ID;
// 認証情報
isAuthorized: boolean;
apiType: "shop" | "admin";
// セッション情報
session?: Session;
sessionCache?: SessionCache;
}
RequestContextの使用方法¶
すべてのサービスメソッドは、最初の引数としてRequestContextを受け取ります:
@Injectable()
export class MyService {
constructor(private connection: TransactionalConnection) {}
async getData(ctx: RequestContext, id: string): Promise<MyEntity> {
// ctx.channelId でチャネルを識別
// ctx.languageCode で言語を識別
// ctx.user でユーザー情報を取得
return this.connection
.getRepository(ctx, MyEntity)
.findOne({ where: { id, channelId: ctx.channelId } });
}
}
RequestContextのライフサイクル¶
- リクエスト受信: HTTPリクエストが到着
- Context作成: チャネル、言語、ユーザー情報から
RequestContextを生成 - Resolver実行: GraphQL Resolverに
ctxが渡される - Service呼び出し: Serviceメソッドに
ctxが渡される - トランザクション管理:
ctxに紐づくトランザクションが管理される
トランザクション管理¶
RequestContextは、リクエスト全体を通じて同じデータベーストランザクションを共有します:
async function processOrder(ctx: RequestContext, orderId: string) {
// すべての操作が同じトランザクション内で実行される
const order = await this.orderService.findOne(ctx, orderId);
await this.paymentService.process(ctx, order);
await this.fulfillmentService.create(ctx, order);
// エラーが発生した場合、すべてロールバックされる
}
Event Bus¶
Vendureはイベント駆動アーキテクチャを採用しており、EventBusを通じて非同期処理や拡張ポイントを提供します。
イベントの種類¶
- 同期イベント: リクエスト処理中に同期的に発火(例:
OrderPlacedEvent) - 非同期イベント: バックグラウンドで処理(例:
OrderStateTransitionEvent)
イベントの購読¶
import { EventBus, OrderPlacedEvent } from "@vendure/core";
@Injectable()
export class MyService {
constructor(private eventBus: EventBus) {
// イベントの購読
this.eventBus.ofType(OrderPlacedEvent).subscribe((event) => {
console.log("Order placed:", event.entity.id);
// カスタム処理を実行
});
}
}
カスタムイベントの作成¶
import { VendureEvent } from "@vendure/core";
export class CustomOrderEvent extends VendureEvent {
constructor(
public ctx: RequestContext,
public order: Order,
public customData: any,
) {
super();
}
}
// イベントの発火
this.eventBus.publish(new CustomOrderEvent(ctx, order, data));
イベントのベストプラクティス¶
- 重い処理は非同期: メール送信、外部API呼び出しなどは非同期イベントを使用
- エラーハンドリング: イベントハンドラ内のエラーは適切にキャッチ
- 順序保証: イベントの処理順序は保証されないため、依存関係に注意
Worker System¶
Vendureは非同期ジョブキューシステムを提供し、重い処理をバックグラウンドで実行できます。
Job Queue Plugin¶
// vendure-config.ts
import { DefaultJobQueuePlugin } from "@vendure/core";
export const config: VendureConfig = {
plugins: [
DefaultJobQueuePlugin.init({
pollInterval: 2000, // ポーリング間隔(ミリ秒)
}),
],
};
ジョブの作成と実行¶
import { JobQueue, JobQueueService } from "@vendure/core";
@Injectable()
export class MyService {
constructor(private jobQueueService: JobQueueService) {}
async processLargeData(ctx: RequestContext, data: any) {
// ジョブをキューに追加
const job = await this.jobQueueService.add({
name: "process-large-data",
data: { dataId: data.id },
retries: 3,
});
return job;
}
}
カスタムジョブプロセッサー¶
import { Job, JobQueue, JobQueueService } from "@vendure/core";
@Injectable()
export class MyJobProcessor {
async process(job: Job): Promise<void> {
const { dataId } = job.data;
// 重い処理を実行
await this.processData(dataId);
}
}
// プラグインで登録
@VendurePlugin({
providers: [MyJobProcessor],
jobQueueOptions: {
process: async (job) => {
const processor = app.get(MyJobProcessor);
return processor.process(job);
},
},
})
export class MyPlugin {}
Strategies¶
VendureはConfigurableOperationDefを使用して、特定のロジックを差し替え可能にする「Strategy」パターンを提供します。
Money 単位¶
Ritsubi では Vendure の Money 値を JPY でも円の 100 倍整数として扱います。業務設定・SMILE・GA4 は円単位、Vendure API と Order / OrderLine / ShippingLine の Money field は Vendure Money 値です。境界変換は @ritsubi/utils の toVendureMoneyFromYen() / toYenFromVendureMoney() / formatVendurePriceValue() を使い、金額文脈で * 100 / / 100 を直書きしないでください。
詳細は Vendure Money 単位運用 を参照してください。
Strategyの例: Shipping Calculator¶
import { toVendureMoneyFromYen } from "@ritsubi/utils";
import { ShippingCalculator, ShippingCalculatorArgs } from "@vendure/core";
export class CustomShippingCalculator implements ShippingCalculator {
code = "custom-shipping";
description = [{ languageCode: LanguageCode.ja, value: "カスタム配送計算" }];
calculate(ctx: RequestContext, args: ShippingCalculatorArgs): number | Promise<number> {
// カスタム配送料計算ロジック
const basePrice = 500;
const feePerWeightUnit = 100;
const weight = args.orderLines.reduce((sum, line) => sum + line.productVariant.weight, 0);
return toVendureMoneyFromYen(basePrice + weight * feePerWeightUnit);
}
}
// 設定で登録
export const config: VendureConfig = {
shippingOptions: {
shippingCalculators: [CustomShippingCalculator],
},
};
Strategyの実装パターン¶
- インターフェース実装:
ShippingCalculator、PaymentMethodHandlerなどのインターフェースを実装 - 設定で登録:
vendure-config.tsでStrategyを登録 - 動的選択: 管理者がDashboardでStrategyを選択可能
TransactionalConnection¶
TransactionalConnectionは、Vendureが提供するトランザクションセーフなデータベースアクセス層です。
基本的な使用方法¶
import { TransactionalConnection } from "@vendure/core";
@Injectable()
export class MyService {
constructor(private connection: TransactionalConnection) {}
async getEntity(ctx: RequestContext, id: string): Promise<MyEntity> {
// Repositoryを取得
const repo = this.connection.getRepository(ctx, MyEntity);
// クエリ実行
return repo.findOne({ where: { id } });
}
async createEntity(ctx: RequestContext, data: Partial<MyEntity>): Promise<MyEntity> {
const repo = this.connection.getRepository(ctx, MyEntity);
const entity = repo.create(data);
return repo.save(entity);
}
}
トランザクションの明示的管理¶
async function complexOperation(ctx: RequestContext) {
// 新しいトランザクションを開始
return this.connection.withTransaction(ctx, async (transactionCtx) => {
const entity1 = await this.createEntity1(transactionCtx, data1);
const entity2 = await this.createEntity2(transactionCtx, data2);
// エラーが発生した場合、すべてロールバック
return { entity1, entity2 };
});
}
クエリビルダーの使用¶
async function complexQuery(ctx: RequestContext) {
const repo = this.connection.getRepository(ctx, Order);
return repo
.createQueryBuilder("order")
.leftJoinAndSelect("order.lines", "line")
.leftJoinAndSelect("line.productVariant", "variant")
.where("order.state = :state", { state: "PaymentSettled" })
.andWhere("order.createdAt > :date", { date: new Date("2024-01-01") })
.getMany();
}
Custom Fields¶
Custom Fieldsを使用すると、Vendureの標準エンティティに独自のフィールドを追加できます。
Custom Fieldsの定義¶
// vendure-config.ts
export const config: VendureConfig = {
customFields: {
Customer: [
{
name: "customerStatus",
type: "string",
defaultValue: "general",
label: [{ languageCode: LanguageCode.ja, value: "顧客ステータス" }],
options: [
{
value: "general",
label: [{ languageCode: LanguageCode.ja, value: "一般" }],
},
{
value: "premium",
label: [{ languageCode: LanguageCode.ja, value: "プレミアム" }],
},
],
},
{
name: "discountRate",
type: "float",
defaultValue: 0,
min: 0,
max: 100,
},
],
ProductVariant: [
{
name: "directShippingEligible",
type: "boolean",
defaultValue: false,
},
],
},
};
プラグインでのCustom Fields追加¶
@VendurePlugin({
configuration: (config) => {
config.customFields.Customer = config.customFields.Customer ?? [];
config.customFields.Customer.push({
name: "customField",
type: "string",
});
return config;
},
})
export class MyPlugin {}
TypeScript型定義¶
Custom Fieldsを使用する場合、TypeScriptの型定義を拡張する必要があります:
// src/types/vendure-custom-fields.ts
declare module "@vendure/core" {
interface CustomCustomerFields {
customerStatus?: string;
discountRate?: number;
}
interface CustomProductVariantFields {
directShippingEligible?: boolean;
}
}
Custom Fieldsの使用¶
// 読み取り
const customer = await this.customerService.findOne(ctx, customerId);
const status = customer.customFields.customerStatus;
// 更新
await this.customerService.update(ctx, customerId, {
customFields: {
customerStatus: "premium",
discountRate: 10,
},
});
Entity Translation¶
Vendureは多言語対応エンティティをサポートしており、Translationエンティティを使用して複数言語のデータを管理します。
翻訳可能なエンティティ¶
ProductProductVariantCollectionFacetFacetValueShippingMethodPaymentMethodPromotion
翻訳データの作成¶
await this.productService.create(ctx, {
translations: [
{
languageCode: LanguageCode.ja,
name: "商品名(日本語)",
description: "説明(日本語)",
},
{
languageCode: LanguageCode.en,
name: "Product Name (English)",
description: "Description (English)",
},
],
});
翻訳データの取得¶
// RequestContextのlanguageCodeに基づいて自動的に翻訳が選択される
const product = await this.productService.findOne(ctx, productId);
// product.name は ctx.languageCode に応じた翻訳が返される
// 特定の言語の翻訳を取得
const japaneseTranslation = product.translations.find((t) => t.languageCode === LanguageCode.ja);
プラグイン開発実践¶
ライフサイクルフック¶
プラグインは、Vendureの起動・停止時に実行されるライフサイクルフックを提供します。
利用可能なフック¶
@VendurePlugin({})
export class MyPlugin {
// プラグインの初期化時(サーバー起動時)
static async onBootstrap(app: INestApplication): Promise<void> {
// 初期化処理
}
// Workerプロセスの作成時
static async onWorkerCreation(app: INestApplication): Promise<void> {
// Worker固有の初期化処理
}
// プラグインの終了時
static async onClose(app: INestApplication): Promise<void> {
// クリーンアップ処理
}
}
実装例¶
@VendurePlugin({})
export class MyPlugin {
static async onBootstrap(app: INestApplication): Promise<void> {
const configService = app.get(ConfigService);
const logger = app.get(Logger);
logger.log("MyPlugin initialized", "MyPlugin");
// 外部サービスへの接続など
await this.connectToExternalService(configService);
}
static async onClose(app: INestApplication): Promise<void> {
// 接続の切断など
await this.disconnectFromExternalService();
}
}
依存性注入 (DI) のベストプラクティス¶
VendureはNestJSの依存性注入システムを使用します。
プロバイダーの登録¶
@VendurePlugin({
imports: [PluginCommonModule], // Vendureの共通モジュール
providers: [MyService, MyRepository],
})
export class MyPlugin {}
サービスの注入¶
@Injectable()
export class MyService {
constructor(
private connection: TransactionalConnection,
private eventBus: EventBus,
private logger: Logger,
) {}
}
スコープ管理¶
- Singleton (デフォルト): アプリケーション全体で1つのインスタンス
- Request: リクエストごとに新しいインスタンス
@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {
// リクエストごとに新しいインスタンスが作成される
}
プラグインの構造¶
推奨ディレクトリ構造¶
packages/plugins/my-plugin/
├── src/
│ ├── index.ts # プラグイン定義
│ ├── entities/ # カスタムエンティティ
│ │ └── my-entity.entity.ts
│ ├── services/ # ビジネスロジック
│ │ └── my.service.ts
│ ├── api/ # GraphQL Resolver
│ │ ├── my.resolver.ts
│ │ └── my.schema.ts
│ └── types.ts # 型定義
└── package.json
実装例¶
// src/index.ts
import { PluginCommonModule, VendurePlugin } from "@vendure/core";
import { MyEntity } from "./entities/my-entity.entity";
import { MyService } from "./services/my.service";
import { MyResolver } from "./api/my.resolver";
import { myGraphqlSchema } from "./api/my.schema";
@VendurePlugin({
imports: [PluginCommonModule],
entities: [MyEntity],
providers: [MyService],
shopApiExtensions: {
schema: myGraphqlSchema,
resolvers: [MyResolver],
},
compatibility: "^3.5.0",
})
export class MyPlugin {}
Dashboard開発 (React)¶
Vendure DashboardはReactベースの管理画面です。旧Admin UI(Angular)は使用せず、Dashboardのみを使用します。
upstream UI変更メモ¶
- 本リポジトリの採用バージョンである Vendure
3.6.xでは、Option GroupsがCatalog配下の専用管理ページとして表示される。 - これはリツビ固有の Dashboard 拡張ではなく、Vendure upstream の
v3.6.0で追加されたShared Product Option Groups/Option Groups management pageに由来する。 - 旧版(少なくとも
v3.5.x)では option group / option 編集機能は存在したが、現行のようなCatalog > Option Groupsの独立ナビゲーション追加はv3.6.0系で入った扱いとみなす。 - 画面導線の差異を調査するときは、まずローカルの
@vendure/dashboardバージョンと upstream release note を照合し、プロジェクト固有改修と混同しないこと。
参考:
- Vendure v3.6.0 release: https://github.com/vendurehq/vendure/releases/tag/v3.6.0
- Vendure v3.5.0 release: https://github.com/vendurehq/vendure/releases/tag/v3.5.0
基本構造¶
Dashboard Pluginの設定¶
本プロジェクトでは、React Dashboard の frontend は standalone の Vite dev server
/ build artifact として扱い、Vendure 側には backend companion として
DashboardPlugin を読み込みます。
// vendure-config.ts
import { DashboardPlugin } from "@vendure/dashboard/plugin";
import { AdminExtensionsPlugin } from "@ritsubi/plugins";
import { DashboardExtensionsPlugin } from "./plugins/ritsubi-admin-extensions/dashboard-extensions.plugin";
import { settingsStoreFields } from "./config/settings-store-fields";
export const vendureConfig: VendureConfig = {
settingsStoreFields,
plugins: [DashboardPlugin, DashboardExtensionsPlugin, AdminExtensionsPlugin],
};
DashboardPluginは persisted settings / saved views / insights metrics 用の backend を提供します。- これが無いと React Dashboard の console に
DashboardPlugin is not configuredwarning が出ます。 - 依存は別パッケージではなく、
@vendure/dashboard/pluginの subpath export を使います。 - 静的配信した Dashboard を同一プロセスに載せる場合のみ、
DashboardPlugin.init({ route, appDir })方式を検討します。
ディレクトリ構造¶
apps/vendure-server/
├── src/
│ ├── plugins/
│ │ └── ritsubi-admin-extensions/
│ │ └── dashboard/ # カスタムDashboard拡張
│ │ ├── index.tsx # Dashboard拡張のエントリーポイント
│ │ ├── components/ # カスタムコンポーネント
│ │ ├── utils/ # 共通ユーティリティ
│ │ │ └── download-helper.ts # ファイルダウンロードヘルパー
│ │ └── i18n/ # 翻訳ファイル
│ │ ├── ja.po
│ │ └── en.po
│ ├── dashboard/ # カスタムDashboard拡張(旧構成、移行予定)
│ │ ├── index.tsx
│ │ └── i18n/
│ │ ├── ja.po
│ │ └── en.po
│ └── vendure-config.ts
├── lingui.config.js # Lingui設定
└── vite.config.mts # Vite設定
拡張方法¶
ページブロックの追加¶
既存のページにカスタムブロックを追加できます:
// src/dashboard/index.tsx
import { defineDashboardExtension } from "@vendure/dashboard";
import { CustomerPasswordAdminBlock } from "./components/customer-password-admin";
export default defineDashboardExtension({
pageBlocks: [
{
id: "customer-password-admin",
title: "初期パスワード設定",
location: {
pageId: "customer-detail",
column: "side",
position: { blockId: "groups", order: "after" },
},
component: CustomerPasswordAdminBlock,
shouldRender: (context) => Boolean(context.entity?.id),
requiresPermission: ["UpdateCustomer"],
},
],
});
新しいページの追加¶
export default defineDashboardExtension({
routes: [
{
path: '/my-custom-page',
component: () => <MyCustomPage />,
loader: () => ({ breadcrumb: 'カスタムページ' }),
navMenuItem: {
sectionId: 'settings',
title: 'カスタム設定',
},
},
],
});
ナビゲーションメニューの追加¶
export default defineDashboardExtension({
navSections: [
{
id: 'integrations',
title: '連携',
icon: Cable, // lucide-reactのアイコン
},
],
routes: [
{
path: '/smile-sync',
component: () => <SmileSyncPage />,
navMenuItem: {
sectionId: 'integrations',
title: 'SMILE連携',
},
},
],
});
公式UIコンポーネント¶
Dashboard拡張では、@vendure/dashboardの公式UIコンポーネントを優先的に使用します。
利用可能なコンポーネント¶
import {
Button,
Input,
Select,
Card,
Table,
Form,
Badge,
Dialog,
// ... その他多数
} from "@/vdb/components/ui/...";
レイアウトコンポーネント¶
import {
Page,
PageTitle,
PageLayout,
PageBlock,
} from '@/vdb/framework/...';
export function MyCustomPage() {
return (
<Page>
<PageTitle>カスタムページ</PageTitle>
<PageLayout>
<PageBlock>
{/* コンテンツ */}
</PageBlock>
</PageLayout>
</Page>
);
}
国際化 (i18n)¶
Dashboard拡張では、LinguiJSを使用して国際化を実装します。
翻訳キーの定義¶
import { Trans, useLingui } from '@lingui/react/macro';
export function MyComponent() {
const { t } = useLingui();
return (
<div>
<Trans>こんにちは</Trans>
<p>{t`現在の日時: ${new Date().toLocaleString()}`}</p>
</div>
);
}
翻訳ファイルの管理¶
# 翻訳キーの抽出
npx lingui extract
# 翻訳のコンパイル
npx lingui compile
翻訳ファイルは src/dashboard/i18n/ に配置されます:
src/dashboard/i18n/
├── ja.po
└── en.po
翻訳ファイルの編集¶
# ja.po
msgid "Hello"
msgstr "こんにちは"
msgid "Current time: {time}"
msgstr "現在の日時: {time}"
データ連携¶
GraphQL APIの使用¶
Dashboard拡張では、@vendure/dashboardが提供するapiオブジェクトを使用してGraphQL
APIにアクセスします。
import { api } from "@/vdb/graphql/api";
const query = /* GraphQL */ `
query GetCustomers($options: CustomerListOptions) {
customers(options: $options) {
items {
id
emailAddress
firstName
lastName
}
totalItems
}
}
`;
const result = await api.query(query, { options: { take: 10 } });
React Queryの使用¶
Dashboard拡張では、React Queryを使用してデータフェッチを管理します。
import { useQuery, useMutation } from "@tanstack/react-query";
import { api } from "@/vdb/graphql/api";
export function MyComponent() {
const { data, isLoading } = useQuery({
queryKey: ["customers"],
queryFn: () => api.query(customersQuery, {}),
});
const { mutateAsync } = useMutation({
mutationFn: (variables) => api.mutate(updateCustomerMutation, variables),
});
// ...
}
共通ユーティリティ¶
ファイルダウンロードヘルパー¶
Dashboard拡張でファイルダウンロード機能(CSVエクスポート、PDF生成等)を実装する際は、共通ユーティリティ関数を使用すること。
場所:
apps/vendure-server/src/plugins/ritsubi-admin-extensions/dashboard/utils/download-helper.ts
提供関数:
downloadBlob(blob: Blob, filename: string): BlobデータをダウンロードdownloadBase64(base64: string, filename: string, mimeType: string): Base64文字列をデコードしてダウンロードdownloadText(content: string, filename: string, mimeType?: string): テキストデータをダウンロード
使用例:
import { downloadBase64, downloadText } from "./utils/download-helper.js";
// GraphQLからBase64エンコードされたCSVデータを受け取る場合
const response = await api.mutate(exportMutation, variables);
downloadBase64(response.exportSmileOrders.csvData, response.exportSmileOrders.filename, "text/csv");
// テキストデータ(CSV文字列など)を直接ダウンロードする場合
const csv = toCsv([header, ...rows]);
downloadText(csv, "export.csv", "text/csv");
重要な原則:
- DOM操作の一元化:
document.createElement('a')やdocument.body.appendChild()などのDOM操作は、このヘルパー関数内に集約されている。コンポーネント内で直接DOM操作を行わないこと。 - Vendure標準の不在: VendureのDashboardライブラリには、ファイルダウンロード用の標準ユーティリティが提供されていないため、プロジェクト固有の実装としてこのヘルパーを使用する。
- ブラウザ互換性: Firefox等の互換性のために
document.body.appendChildを使用し、確実にクリーンアップ(removeChild、URL.revokeObjectURL)を行う実装となっている。
実装の背景:
Vendureはヘッドレスコマースフレームワークとして、フロントエンドの実装方法に対して中立な立場を取るため、特定のブラウザAPI(DOM操作など)に依存するヘルパーをCoreに含めない方針です。そのため、ファイルダウンロード機能が必要な場合は、プロジェクト固有のユーティリティとして実装する必要があります。
6.2 Vitestによるテスト¶
Vendure v3プロジェクトでは、高速なテストランナーであるVitestが推奨されています。Jestの代わりにVitestを使用することで、設定の簡素化と実行速度の向上が見込めます。
テストの種類¶
-
単体テスト (
.spec.ts): サービスやヘルパー関数のロジックを検証します。DB接続は行わず、依存関係はモック化します。 -
E2Eテスト (
.e2e-spec.ts): 実際のデータベースを使用して、APIエンドポイント(GraphQL)からデータベースまでのフロー全体を検証します。@vendure/testingパッケージのcreateTestEnvironmentを使用します。
Vitest設定例¶
vitest.config.tsを作成し、NestJSのデコレータをサポートするためにunplugin-swcプラグインを使用します。
import { defineConfig } from "vitest/config";
import swc from "unplugin-swc";
export default defineConfig({
plugins: [
swc.vite({
jsc: {
transform: {
useDefineForClassFields: false,
legacyDecorator: true,
decoratorMetadata: true,
},
},
}),
],
test: {
globals: true,
environment: "node",
},
});
テスト実行コマンド¶
# 全テスト実行
pnpm test
# UIモードで実行
pnpm run test:ui
# カバレッジ計測
pnpm run test:coverage
# 関連テストの実行(変更されたファイルのみ)
pnpm run test:related src/my-changed-file.ts
6.3 統合テスト基盤(バックエンド)¶
バックエンド統合テスト環境が apps/vendure-server/tests/integration
配下に整備されています。Postgres を使用し、実際のVendureサーバーを起動してGraphQL
API経由でテストを実行します。
セットアップの特徴¶
- DB: Postgres/pg_trgm(docker compose dev)を使用
- Initializer:
PostgresInitializer(@vendure/testing)を使用 - Client:
SimpleGraphQLClient(@vendure/testing)を使用
実行方法¶
# Vendureサーバーの統合テストのみ実行
pnpm exec nx run ritsubi-vendure-server:test:integration
# ルートから実行(Nx)
pnpm test:integration
テストの作成方法¶
tests/integration/setup/test-environment.ts から createTestEnv
をインポートして使用します。
import { createTestEnv, SimpleGraphQLClient, TestServer } from "./setup/test-environment";
import { initialData } from "./fixtures/initial-data";
import gql from "graphql-tag";
describe("My Feature", () => {
let server: TestServer;
let adminClient: SimpleGraphQLClient;
beforeAll(async () => {
const env = await createTestEnv();
server = env.server;
await server.init({ initialData, logging: false });
adminClient = env.adminClient;
await adminClient.asSuperAdmin();
});
afterAll(async () => {
await server.destroy();
});
it("should work", async () => {
const result = await adminClient.query(gql`...`);
expect(result).toBeDefined();
});
});
参照リンク¶
プロジェクト内ドキュメント¶
- Vendure開発ガイド: プロジェクト固有のセットアップと開発ガイド
- Vendureプラグイン実装ガイド: カスタムプラグインの実装詳細
- Vendure Dashboard 日本語化手順書: Dashboardの国際化設定
公式ドキュメント¶
- Vendure公式ドキュメント: Vendureの公式ドキュメント(英語)
- Vendure GitHub: ソースコードとイシュー
更新日: 2025-01-XX
メンテナー: Ritsubi開発チーム