Skip to content

Module Template

This document describes the minimal scaffold for creating a new Purple Cat platform module. Use the @purple-cat-games/genai module as a working reference.

See also: Module Author Guide for the full API reference.


Directory Structure

my-module/
├── package.json
├── tsconfig.json
├── src/
│   ├── index.ts               # Package root export (optional)
│   ├── client/
│   │   └── index.ts           # Client entry — exports register()
│   └── server/
│       └── index.ts           # Server entry — exports router + initialize
└── dist/                      # Build output (gitignored)

package.json

{
  "name": "@purple-cat-games/my-module",
  "version": "0.1.0",
  "description": "My custom module for Purple Cat Platform",
  "license": "UNLICENSED",
  "type": "module",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": ["dist", "package.json"],
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    },
    "./client": {
      "types": "./dist/client/index.d.ts",
      "import": "./dist/client/index.js"
    }
  },
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch",
    "test": "vitest run",
    "clean": "rm -rf dist"
  },
  "purple-cat-module": {
    "type": "integration",
    "client": "dist/client/index.js",
    "server": "dist/server/index.js"
  },
  "peerDependencies": {
    "@purple-cat/shared": "*",
    "express": "^4.0.0",
    "react": "^18.0.0"
  },
  "devDependencies": {
    "@purple-cat/shared": "workspace:*",
    "@types/express": "^4.17.0",
    "@types/react": "^18.0.0",
    "typescript": "^5.0.0",
    "vitest": "^4.0.0"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022", "DOM"],
    "jsx": "react-jsx",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

src/client/index.ts — Client Entry

import { moduleRegistry } from '@purple-cat/shared';
import { MyArtworkSource } from './MyArtworkSource';

export function register(): void {
  moduleRegistry.registerArtworkSource({
    id: 'my-source',
    label: 'My Source',
    icon: 'Sparkles',
    moduleId: 'my-module',
    component: MyArtworkSource,
  });
}

src/server/index.ts — Server Entry

import { Router } from 'express';
import type { ModuleInitializeContext } from '@purple-cat/shared';

export const router = Router();

router.get('/status', (_req, res) => {
  res.json({ ok: true, module: 'my-module' });
});

export async function initialize(context: ModuleInitializeContext): Promise<void> {
  // `context.dataDir` is a per-module persistent directory created by the platform.
  console.log(`my-module: initialized; data dir: ${context.dataDir}`);
}

src/index.ts — Package Root (Optional)

// Re-export shared types or utilities if other packages need them
export type { MyModuleConfig } from './shared/types.js';

Registering With the Platform

Add the module to modules.local.json in the platform root:

{
  "modules": [
    { "package": "@purple-cat-games/my-module", "path": "../my-module" }
  ]
}

Then rebuild. The platform's Vite plugin will auto-generate the client import, and the server loader will mount the API routes at /api/modules/my-module/.

A first-party module

The platform's own modules — dnd5e, system8, scenes and system5 — live in this repository under modules/ (since v0.118.0). Each is a pnpm workspace package ("private": true, @purple-cat/shared as workspace:*), built by pnpm build:packages, linted and tested by pnpm verify with its own coverage floor, and listed in modules.hosted.json by directory:

{ "package": "@purple-cat-games/my-module", "dir": "modules/my-module" }

Nothing is published. Every environment builds the module from the commit it deploys: pnpm modules:bundle packs it and writes the resolved manifest the install scripts read, and a plain pnpm dev loads it straight from modules/. A module change ships with the platform change it goes with, in the same version. Only modules kept in other repositories (genai, vtt-assets) are pinned by version and installed from GitHub Packages.


Asset-Only Module Variant

For modules that only provide static assets (textures, token art, etc.), the scaffold is simpler:

{
  "name": "@purple-cat-games/my-assets",
  "version": "0.1.0",
  "purple-cat-module": {
    "type": "assets",
    "assetPaths": {
      "textures": "dist/textures"
    }
  },
  "scripts": {
    "build": "node scripts/build.mjs"
  }
}

No client or server entries needed. The platform uploads the asset files during initialization and serves them via the configured storage provider.