| name | tumiki-ee-ce-separation |
| description | Tumiki Manager アプリのEE/CE(Enterprise Edition/Community Edition)ライセンス分離の実装ガイド。
ビルド時のコード分離、Facadeパターン、新規EE機能追加の手順を提供。
「EE機能」「ライセンス分離」「CE版ビルド」などのリクエスト時にトリガー。
|
| sourcePatterns | ["apps/manager/src/**/*.ee.ts","apps/manager/src/**/*.ee.test.ts","apps/manager/src/features/ee/**","apps/manager/next.config.js","apps/manager/tsconfig.ce.json","apps/manager/src/lib/ee-stub.js"] |
EE/CE ライセンス分離 - 開発リファレンス
このスキルを使用する場面:
- 新しいEE機能の実装時
- CE版/EE版ビルドの動作確認時
- ライセンス分離パターンの理解が必要な時
- EE機能のFacadeパターン実装時
- ビルド設定のカスタマイズ時
アーキテクチャ概要
Tumiki Managerは、オープンソースのCommunity Edition(CE)と商用のEnterprise Edition(EE)に分離されています。
| Edition | ライセンス | 対象ユーザー |
|---|
| CE版 | Apache 2.0 | 個人開発者・OSS利用者 |
| EE版 | Elastic License 2.0 | 企業・商用利用者 |
ビルド時分離
┌─────────────────────────────────────────────────────────────┐
│ ソースコード │
│ ├── feature.ts (CE スタブ) │
│ └── feature.ee.ts (EE 実装) │
└─────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ CE版ビルド │ │ EE版ビルド │
│ pnpm build:ce │ │ pnpm build:ee │
│ │ │ │
│ .ee.ts → ee-stub.js │ │ .ee.ts → そのまま │
│ (空のモジュール) │ │ (実装を含む) │
└─────────────────────┘ └─────────────────────┘
ファイル命名規則
| パターン | 説明 |
|---|
*.ts | CE スタブ(FORBIDDEN エラーを返す) |
*.ee.ts | EE 実装(実際のロジック) |
*.ee.test.ts | EE 機能のテスト |
index.ts | CE Facade(動的インポートまたはスタブ) |
index.ee.ts | EE エントリーポイント |
SPDXライセンスヘッダー
全てのEEファイル(.ee.ts, .ee.test.ts)には以下のヘッダーを追加:
環境変数
| 変数名 | 説明 | 値 |
|---|
NEXT_PUBLIC_TUMIKI_EDITION | エディション判定 | ee/ce |
ビルドコマンド
pnpm build:ce
pnpm build:ee
pnpm dev
コンポーネント構成
apps/manager/
├── next.config.js # webpack プラグイン(CE版で.ee.ts除外)
├── tsconfig.ce.json # CE版TypeScript設定
├── package.json # ビルドスクリプト定義
├── src/
│ ├── lib/
│ │ └── ee-stub.js # CE版用空モジュール
│ ├── features/
│ │ └── ee/
│ │ └── config.ts # EE機能設定・判定関数
│ ├── components/
│ │ └── ee/
│ │ └── EEFeatureGate.tsx # EE機能ゲートコンポーネント
│ └── server/
│ └── api/
│ └── routers/
│ └── organization/
│ ├── inviteMembers.ts # CE スタブ
│ └── inviteMembers.ee.ts # EE 実装
EE機能設定
config.ts
import { isEE, hasFeature, type EEFeature } from "@tumiki/license";
export const EE_AVAILABLE = isEE();
export const ORG_CREATION_ENABLED = hasFeature("organization-creation");
export const isEEFeatureAvailable = (feature: EEFeature): boolean => {
return hasFeature(feature);
};
EEFeatureGate コンポーネント
import type { ReactNode } from "react";
import { isEEFeatureAvailable, type EEFeature } from "@/features/ee/config";
type EEFeatureGateProps = {
feature: EEFeature;
children: ReactNode;
fallback?: ReactNode;
};
export const EEFeatureGate = ({
feature,
children,
fallback,
}: EEFeatureGateProps) => {
const isAvailable = isEEFeatureAvailable(feature);
if (!isAvailable) return fallback ?? null;
return <>{children}</>;
};
Facadeパターン実装
CE スタブ(feature.ts)
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import type { ProtectedContext } from "@/server/api/trpc";
export const inviteMembersInputSchema = z.object({
emails: z.array(z.string().email()),
});
export const inviteMembersOutputSchema = z.object({
success: z.boolean(),
invitedCount: z.number(),
});
export type InviteMembersInput = z.infer<typeof inviteMembersInputSchema>;
export type InviteMembersOutput = z.infer<typeof inviteMembersOutputSchema>;
export const inviteMembers = async (_params: {
input: InviteMembersInput;
ctx: ProtectedContext;
}): Promise<InviteMembersOutput> => {
throw new TRPCError({
code: "FORBIDDEN",
message: "メンバー招待機能はEnterprise Editionでのみ利用可能です",
});
};
EE 実装(feature.ee.ts)
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import type { ProtectedContext } from "@/server/api/trpc";
import { validateOrganizationAccess } from "@/lib/auth/organization-access.ee";
export const inviteMembersInputSchema = z.object({
emails: z.array(z.string().email()),
});
export const inviteMembersOutputSchema = z.object({
success: z.boolean(),
invitedCount: z.number(),
});
export type InviteMembersInput = z.infer<typeof inviteMembersInputSchema>;
export type InviteMembersOutput = z.infer<typeof inviteMembersOutputSchema>;
export const inviteMembers = async (params: {
input: InviteMembersInput;
ctx: ProtectedContext;
}): Promise<InviteMembersOutput> => {
const { input, ctx } = params;
await validateOrganizationAccess(ctx);
const invitedCount = input.emails.length;
return {
success: true,
invitedCount,
};
};
ルーターでのインポート
import {
inviteMembers,
inviteMembersInputSchema,
inviteMembersOutputSchema,
} from "./inviteMembers.ee";
export const organizationRouter = createTRPCRouter({
inviteMembers: protectedProcedure
.input(inviteMembersInputSchema)
.output(inviteMembersOutputSchema)
.mutation(inviteMembers),
});
重要: EE機能は必ず.ee.tsからインポートしてください。
- EE版/開発環境:
.ee.ts(EE実装)が使用される
- CE版ビルド: webpackプラグインが
.ee.tsを.ts(CEスタブ)にリダイレクト
webpack プラグイン
const isEEBuild = process.env.NEXT_PUBLIC_TUMIKI_EDITION === "ee";
webpack: (config, { isServer }) => {
if (!isEEBuild) {
const cePlugin = {
apply(compiler) {
compiler.hooks.normalModuleFactory.tap("CEBuildPlugin", (nmf) => {
nmf.hooks.beforeResolve.tap("CEBuildPlugin", (resolveData) => {
if (resolveData.request && resolveData.request.includes(".ee")) {
resolveData.request = resolveData.request.replace(".ee", "");
}
});
});
},
};
config.plugins.push(cePlugin);
}
return config;
},
新しいEE機能追加手順
1. CEスタブファイル作成(feature.ts)
import { TRPCError } from "@trpc/server";
export const newFeatureInputSchema = z.object({
});
export const newFeatureOutputSchema = z.object({
});
export const newFeature = async () => {
throw new TRPCError({
code: "FORBIDDEN",
message: "この機能はEnterprise Editionでのみ利用可能です",
});
};
2. EE実装ファイル作成(feature.ee.ts)
export const newFeatureInputSchema = z.object({
});
export const newFeatureOutputSchema = z.object({
});
export const newFeature = async (params) => {
};
3. ルーターに追加
import { newFeature, newFeatureInputSchema } from "./newFeature";
export const router = createTRPCRouter({
newFeature: protectedProcedure
.input(newFeatureInputSchema)
.mutation(newFeature),
});
4. テスト作成(feature.ee.test.ts)
import { describe, test, expect } from "vitest";
import { newFeature } from "./newFeature.ee";
describe("newFeature", () => {
test("正常に動作する", async () => {
});
});
5. UI側でのゲート(必要な場合)
import { EEFeatureGate } from "@/components/ee/EEFeatureGate";
<EEFeatureGate feature="member-management" fallback={<UpgradePrompt />}>
<InviteMemberButton />
</EEFeatureGate>;
EE機能一覧(manager)
| 機能 | ファイル | 説明 |
|---|
| メンバー招待 | organization/inviteMembers.ee.ts | メール招待送信 |
| 招待一覧取得 | organization/getInvitations.ee.ts | 保留中の招待一覧 |
| 招待再送信 | organization/resendInvitation.ee.ts | 招待メール再送 |
| 招待キャンセル | organization/cancelInvitation.ee.ts | 招待の取り消し |
| メンバー削除 | organization/removeMember.ee.ts | 組織からメンバー削除 |
| ロール変更 | organization/updateMemberRole.ee.ts | メンバーのロール変更 |
| 組織作成 | v2/organization/createOrganization.ee.ts | 新規組織作成 |
ビルド検証
CE版の検証
pnpm build:ce
grep -r "validateOrganizationAccess" .next/server --include="*.js" | wc -l
EE版の検証
pnpm build:ee
grep -r "validateOrganizationAccess" .next/server --include="*.js" | wc -l
実装チェックリスト
新機能追加時
ビルド設定変更時
UI側でEE機能を使用する時
トラブルシューティング
CE版ビルドにEEコードが含まれる
- ルーターが
.ee.tsから直接インポートしていないか確認
next.config.js のwebpackプラグインが有効か確認
--webpack フラグがbuild:ceに含まれているか確認
EE版ビルドが失敗する
.ee.ts ファイルの構文エラーを確認
- EE専用の依存関係が不足していないか確認
NEXT_PUBLIC_TUMIKI_EDITION=ee が設定されているか確認
型エラーが発生する
- CEスタブとEE実装のスキーマが一致しているか確認
- 両ファイルで同じ型をエクスポートしているか確認
tsconfig.json の設定を確認
FORBIDDEN エラーが返される
- 正しいビルド(CE/EE)を使用しているか確認
- 環境変数
NEXT_PUBLIC_TUMIKI_EDITION が設定されているか確認
- サーバー再起動が必要か確認
関連ドキュメント