Skip to content

Extension Structure ​

Status: this page describes the designed sandboxed extension API, which does not run today. Only native extensions run. See what actually runs.

This document covers the directory layout, file organization, and project setup for BluePLM extensions.

Directory Layout ​

Minimal Extension ​

my-extension/
├── extension.json      # Manifest (required)
├── LICENSE             # Open source license (required)
└── client/
    └── index.js        # Entry point

Full Extension ​

my-extension/
├── extension.json          # Manifest
├── README.md               # Documentation
├── CHANGELOG.md            # Version history
├── LICENSE                 # License file
├── icon.png                # 128x128 icon
├── package.json            # Build configuration
├── tsconfig.json           # TypeScript config
├── client/                 # Client-side code
│   ├── index.ts            # Entry point
│   ├── commands/           # Command handlers
│   │   ├── sync.ts
│   │   └── configure.ts
│   ├── components/         # React components
│   │   ├── Panel.tsx
│   │   ├── Settings.tsx
│   │   └── StatusBar.tsx
│   ├── views/              # View components
│   │   └── Sidebar.tsx
│   ├── services/           # Business logic
│   │   ├── syncService.ts
│   │   └── apiClient.ts
│   ├── hooks/              # React hooks
│   │   └── useConfig.ts
│   └── utils/              # Utilities
│       ├── formatting.ts
│       └── validation.ts
├── server/                 # Server-side code
│   ├── sync.ts             # Route handler
│   ├── status.ts           # Route handler
│   ├── webhook.ts          # Public handler
│   └── utils/              # Server utilities
│       └── helpers.ts
├── types/                  # Type definitions
│   └── index.ts
├── test/                   # Tests
│   ├── client/
│   └── server/
└── scripts/                # Build scripts
    └── package.js

File Purposes ​

Root Files ​

FilePurposeRequired
extension.jsonExtension manifest✓
LICENSEOpen source license✓
README.mdUser documentationRecommended
CHANGELOG.mdVersion historyRecommended
icon.pngExtension iconRecommended
package.jsonnpm dependencies and scriptsFor development
tsconfig.jsonTypeScript configurationFor TypeScript

Client Directory ​

File/FolderPurpose
index.tsEntry point with activate and deactivate exports
commands/Command handler implementations
components/React components for UI
views/View components (sidebar, panel, dialog)
services/Business logic and API clients
hooks/Custom React hooks
utils/Utility functions

Server Directory ​

File/FolderPurpose
*.tsRoute handlers (one per API route)
utils/Shared server utilities

Entry Point ​

Client Entry (index.ts) ​

typescript
import type { ExtensionContext, ExtensionClientAPI } from '@blueplm/extension-api'

// Required export
export async function activate(
  context: ExtensionContext,
  api: ExtensionClientAPI
): Promise<void> {
  // Called when extension is activated
}

// Optional export
export function deactivate(): void {
  // Called when extension is deactivated
}

Server Entry (handler.ts) ​

typescript
import type { ExtensionServerAPI } from '@blueplm/extension-api'

// Default export required
export default async function handler(
  api: ExtensionServerAPI
): Promise<ExtensionResponse> {
  // Handler implementation
  return api.response.json({ success: true })
}

Project Setup ​

package.json ​

json
{
  "name": "my-extension",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "npm run build:client && npm run build:server",
    "build:client": "esbuild client/index.ts --bundle --outfile=dist/client/index.js --format=esm --platform=browser --external:react --external:react-dom --minify",
    "build:server": "esbuild server/*.ts --bundle --outdir=dist/server --format=esm --minify",
    "watch": "npm run build -- --watch",
    "package": "npm run build && node scripts/package.js",
    "lint": "eslint client server",
    "typecheck": "tsc --noEmit",
    "test": "jest"
  },
  "devDependencies": {
    "@blueplm/extension-api": "^1.0.0",
    "@types/react": "^18.0.0",
    "archiver": "^6.0.0",
    "esbuild": "^0.19.0",
    "eslint": "^8.0.0",
    "jest": "^29.0.0",
    "typescript": "^5.0.0"
  }
}

tsconfig.json ​

json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "declaration": false,
    "outDir": "dist",
    "rootDir": ".",
    "jsx": "react-jsx",
    "lib": ["ES2020", "DOM"],
    "types": ["node"]
  },
  "include": ["client/**/*", "server/**/*", "types/**/*"],
  "exclude": ["node_modules", "dist", "test"]
}

.gitignore ​

node_modules/
dist/
*.bpx
.env
.DS_Store

Code Organization Patterns ​

Commands Module ​

client/commands/
├── index.ts          # Export all commands
├── sync.ts           # Sync command
└── configure.ts      # Configure command
typescript
// client/commands/index.ts
export { registerSyncCommand } from './sync'
export { registerConfigureCommand } from './configure'

// client/commands/sync.ts
import type { ExtensionContext, ExtensionClientAPI } from '@blueplm/extension-api'

export function registerSyncCommand(
  context: ExtensionContext,
  api: ExtensionClientAPI
) {
  return api.commands.registerCommand('myext.sync', async () => {
    // Implementation
  })
}

Services Module ​

client/services/
├── index.ts          # Export all services
├── syncService.ts    # Sync logic
└── apiClient.ts      # API wrapper
typescript
// client/services/apiClient.ts
import type { ExtensionClientAPI } from '@blueplm/extension-api'

export class ApiClient {
  constructor(private api: ExtensionClientAPI) {}

  async sync(vaultId: string) {
    return this.api.callOrgApi('/extensions/myext/sync', {
      method: 'POST',
      body: { vaultId }
    })
  }

  async getStatus() {
    return this.api.callOrgApi('/extensions/myext/status')
  }
}

Components Module ​

client/components/
├── index.ts          # Export all components
├── Panel.tsx         # Main panel
├── Settings.tsx      # Settings page
└── shared/           # Shared components
    ├── Button.tsx
    └── StatusBadge.tsx

Types Module ​

types/
└── index.ts          # All type definitions
typescript
// types/index.ts
export interface SyncResult {
  fileCount: number
  duration: number
  errors: string[]
}

export interface ExtensionConfig {
  autoSync: boolean
  syncInterval: number
  excludePatterns: string[]
}

export const DEFAULT_CONFIG: ExtensionConfig = {
  autoSync: true,
  syncInterval: 300,
  excludePatterns: ['*.tmp']
}

Build Output ​

After building, the dist/ directory should mirror the source structure:

dist/
├── client/
│   ├── index.js              # Bundled client entry
│   └── components/
│       ├── Panel.js          # Bundled components
│       └── Settings.js
└── server/
    ├── sync.js               # Bundled handlers
    └── status.js

The .bpx package includes:

my-extension-1.0.0.bpx
├── extension.json
├── README.md
├── LICENSE
├── icon.png
├── client/                   # From dist/client/
│   └── ...
└── server/                   # From dist/server/
    └── ...

Naming Conventions ​

TypeConventionExample
Extension IDpublisher.namemycompany.cloud-sync
Command IDextension.actioncloud-sync.syncNow
View IDextension.viewnamecloud-sync.panel
Storage keyscamelCaselastSyncTime
File namescamelCase.tssyncService.ts
Component namesPascalCase.tsxSettingsPanel.tsx
Server handlerslowercase.tssync.ts

Best Practices ​

1. Separate Concerns ​

  • Commands in commands/
  • Business logic in services/
  • UI in components/
  • Types in types/

2. Single Responsibility ​

Each file should have one purpose:

  • One command handler per file
  • One component per file
  • One service per file

3. Index Files ​

Use index files for clean imports:

typescript
// client/commands/index.ts
export * from './sync'
export * from './configure'

// Usage in index.ts
import { registerSyncCommand, registerConfigureCommand } from './commands'

4. Type Everything ​

typescript
// Strong typing for better AI assistance and IDE support
interface SyncOptions {
  vaultId: string
  force?: boolean
}

async function performSync(
  api: ExtensionClientAPI,
  options: SyncOptions
): Promise<SyncResult> {
  // Implementation
}

← Getting Started | Manifest Reference →