Skip to content

Plugin Development Guide

徒言 edited this page Sep 30, 2025 · 1 revision

This guide will help you quickly develop third-party plugins for Dataset Viewer to extend the application's file viewing capabilities.

πŸ“‹ Table of Contents


πŸš€ Quick Start

Environment Setup

# 1. Install Dataset Viewer SDK
npm install @dataset-viewer/sdk
# or
pnpm add @dataset-viewer/sdk

# 2. Setup development environment
npm install react react-dom typescript vite --save-dev

Minimal Plugin Example

Create a simple text viewer plugin:

// src/index.tsx
import { createPlugin } from '@dataset-viewer/sdk';
import type { PluginViewerProps } from '@dataset-viewer/sdk';
import { FileText } from 'lucide-react';

const TextViewer: React.FC<PluginViewerProps> = ({ file, fileAccessor }) => {
  const [content, setContent] = React.useState('');

  React.useEffect(() => {
    fileAccessor.getTextContent().then(setContent);
  }, [file.path]);

  return (
    <div className="p-4 font-mono whitespace-pre-wrap">
      {content}
    </div>
  );
};

export default createPlugin({
  metadata: {
    id: 'text-viewer',
    name: 'Text Viewer',
    version: '1.0.0',
    description: 'A simple text file viewer',
    author: 'Your Name',
    supportedExtensions: ['.txt', '.log'],
    mimeTypes: {
      '.txt': 'text/plain',
      '.log': 'text/plain',
    },
    icon: <FileText className="text-blue-500" />,
    category: 'viewer',
    minAppVersion: '1.5.0',
  },
  component: TextViewer,
});

πŸ—οΈ Plugin Architecture

Plugin Lifecycle

Install β†’ Activate β†’ Initialize β†’ Run β†’ Cleanup β†’ Uninstall
  1. Install: Plugin is downloaded to application directory
  2. Activate: User enables the plugin
  3. Initialize: Execute initialize() function (optional)
  4. Run: Render plugin component to handle files
  5. Cleanup: Execute cleanup() function (optional)
  6. Uninstall: Remove plugin from application

Plugin Loading Mechanism

Dataset Viewer supports two plugin loading modes:

1. CommonJS (CJS) Format - Production

// Plugin will be bundled as CJS format
module.exports = {
  metadata: { /* ... */ },
  component: YourComponent,
};

2. ES Module - Development (npm link)

// Use ES Module during development
export default createPlugin({ /* ... */ });

πŸ“ Development Steps

Step 1: Project Structure

my-plugin/
β”œβ”€β”€ package.json
β”œβ”€β”€ tsconfig.json
β”œβ”€β”€ vite.config.ts
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ index.tsx        # Plugin entry
β”‚   β”œβ”€β”€ Viewer.tsx       # Main component
β”‚   └── utils/           # Utility functions (optional)
└── dist/                # Build output
    └── index.js         # CJS format

Step 2: Configure package.json

{
  "name": "dataset-viewer-plugin-example",
  "version": "1.0.0",
  "description": "Example plugin for Dataset Viewer",
  "main": "dist/index.js",
  "type": "commonjs",
  "scripts": {
    "build": "vite build",
    "dev": "vite build --watch"
  },
  "peerDependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  },
  "dependencies": {
    "@dataset-viewer/sdk": "^0.1.2"
  },
  "devDependencies": {
    "@types/react": "^18.2.15",
    "typescript": "^5.1.6",
    "vite": "^7.1.5"
  }
}

Step 3: Configure Build Tools

vite.config.ts:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    lib: {
      entry: 'src/index.tsx',
      formats: ['cjs'],
      fileName: () => 'index.js',
    },
    rollupOptions: {
      external: ['react', 'react-dom', 'react/jsx-runtime'],
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM',
          'react/jsx-runtime': 'ReactJSXRuntime',
        },
      },
    },
  },
});

Step 4: Implement Plugin Component

import React, { useState, useEffect } from 'react';
import { createPlugin } from '@dataset-viewer/sdk';
import type { PluginViewerProps } from '@dataset-viewer/sdk';

const MyViewer: React.FC<PluginViewerProps> = ({
  file,
  fileAccessor,
  isLargeFile,
  onError,
}) => {
  const [content, setContent] = useState<string>('');
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const loadFile = async () => {
      try {
        setLoading(true);
        
        // For large files, load in chunks
        if (isLargeFile) {
          const chunk = await fileAccessor.getFileChunk(0, 1024 * 1024); // 1MB
          const text = new TextDecoder().decode(chunk);
          setContent(text);
        } else {
          const text = await fileAccessor.getTextContent();
          setContent(text);
        }
      } catch (error) {
        onError?.(error as Error);
      } finally {
        setLoading(false);
      }
    };

    loadFile();
  }, [file.path]);

  if (loading) {
    return <div className="flex items-center justify-center h-full">Loading...</div>;
  }

  return (
    <div className="h-full overflow-auto p-4">
      <pre className="whitespace-pre-wrap">{content}</pre>
    </div>
  );
};

export default createPlugin({
  metadata: {
    id: 'custom-viewer',
    name: 'Custom Viewer',
    version: '1.0.0',
    description: 'Custom file viewer',
    author: 'Your Name',
    supportedExtensions: ['.custom'],
    mimeTypes: {
      '.custom': 'application/x-custom',
    },
    category: 'viewer',
    minAppVersion: '1.5.0',
  },
  component: MyViewer,
  initialize: async (context) => {
    console.log('Plugin initialized with base path:', context.pluginBasePath);
  },
  cleanup: async () => {
    console.log('Plugin cleaned up');
  },
});

πŸ“š API Reference

PluginMetadata

Plugin metadata configuration:

Field Type Required Description
id string βœ… Unique identifier, suggested format: product-name or file-type-viewer (e.g., pdf-viewer, cad-viewer)
name string βœ… Display name
version string βœ… Semantic version (e.g., 1.0.0)
description string βœ… Plugin description
author string βœ… Author name
supportedExtensions string[] βœ… Supported file extensions (e.g., ['.txt', '.md'])
mimeTypes Record<string, string> βœ… Extension to MIME type mapping
category 'viewer' | 'editor' | 'converter' | 'analyzer' βœ… Plugin category
minAppVersion string βœ… Minimum required app version
icon ReactNode ❌ Plugin icon (Lucide Icons recommended)
iconMapping Record<string, ReactNode> ❌ Extension to icon mapping
official boolean ❌ Whether official plugin (auto-detected)

PluginViewerProps

Props received by plugin component:

interface PluginViewerProps {
  file: {
    name: string;      // File name
    size: number;      // File size (bytes)
    path: string;      // File path
  };
  content?: Uint8Array;         // Preloaded file content (optional)
  fileAccessor: FileAccessor;   // File accessor
  isLargeFile: boolean;         // Whether large file (>10MB)
  onError?: (error: Error) => void;  // Error handler callback
}

FileAccessor API

Methods provided by file accessor:

interface FileAccessor {
  // Get complete text content
  getTextContent(encoding?: string): Promise<string>;
  
  // Get binary content
  getBinaryContent(): Promise<Uint8Array>;
  
  // Read file in chunks (for large files)
  getFileChunk(start: number, length: number): Promise<Uint8Array>;
  
  // Get file size
  getFileSize(): Promise<number>;
  
  // Check if file exists
  exists(): Promise<boolean>;
}

Lifecycle Functions

interface PluginBundle {
  metadata: PluginMetadata;
  component: React.ComponentType<PluginViewerProps>;
  
  // Plugin initialization (optional)
  initialize?: (context: PluginInitializeContext) => Promise<void>;
  
  // Plugin cleanup (optional)
  cleanup?: () => Promise<void>;
  
  // i18n resources (optional)
  i18nResources?: Record<string, any>;
}

interface PluginInitializeContext {
  pluginBasePath: string;  // Plugin resource base path
}

πŸ’‘ Best Practices

1. Handling Large Files

For large files (>10MB), use chunked loading:

const loadLargeFile = async (fileAccessor: FileAccessor) => {
  const chunkSize = 1024 * 1024; // 1MB
  let offset = 0;
  const chunks: Uint8Array[] = [];

  while (true) {
    const chunk = await fileAccessor.getFileChunk(offset, chunkSize);
    if (chunk.length === 0) break;
    
    chunks.push(chunk);
    offset += chunk.length;
    
    // Handle progress
    console.log(`Loaded ${offset} bytes`);
  }

  return chunks;
};

2. Error Handling

Provide user-friendly error messages:

const MyViewer: React.FC<PluginViewerProps> = ({ fileAccessor, onError }) => {
  const [error, setError] = useState<string | null>(null);

  const handleError = (err: Error) => {
    const message = `Failed to load file: ${err.message}`;
    setError(message);
    onError?.(err);
  };

  if (error) {
    return (
      <div className="flex items-center justify-center h-full text-red-500">
        <div>
          <p className="text-lg font-semibold">Error</p>
          <p className="text-sm">{error}</p>
        </div>
      </div>
    );
  }

  // ... normal rendering
};

3. Loading State

Provide loading indicator to improve user experience:

const MyViewer: React.FC<PluginViewerProps> = ({ file, fileAccessor }) => {
  const [loading, setLoading] = useState(true);
  const [progress, setProgress] = useState(0);

  if (loading) {
    return (
      <div className="flex flex-col items-center justify-center h-full">
        <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
        <p className="mt-4 text-gray-600">Loading {file.name}...</p>
        {progress > 0 && <p className="text-sm text-gray-500">{progress}%</p>}
      </div>
    );
  }

  // ... normal rendering
};

4. Responsive Design

Ensure plugin works properly on different screen sizes:

const MyViewer: React.FC<PluginViewerProps> = ({ content }) => {
  return (
    <div className="h-full w-full overflow-auto">
      <div className="max-w-7xl mx-auto p-4 sm:p-6 lg:p-8">
        {/* Content */}
      </div>
    </div>
  );
};

5. Internationalization Support

Add multi-language support to your plugin:

// src/i18n.ts
export const resources = {
  en: {
    translation: {
      loading: 'Loading...',
      error: 'Failed to load file',
    },
  },
  zh: {
    translation: {
      loading: '加载中...',
      error: 'εŠ θ½½ζ–‡δ»Άε€±θ΄₯',
    },
  },
};

// src/index.tsx
export default createPlugin({
  metadata: { /* ... */ },
  component: MyViewer,
  i18nResources: resources,
});

6. Using External Resources

Load plugin's own resource files:

const initialize = async (context: PluginInitializeContext) => {
  const { pluginBasePath } = context;
  
  // Load CSS file
  const link = document.createElement('link');
  link.rel = 'stylesheet';
  link.href = `${pluginBasePath}styles.css`;
  document.head.appendChild(link);
  
  // Load configuration file
  const response = await fetch(`${pluginBasePath}config.json`);
  const config = await response.json();
  
  console.log('Plugin config loaded:', config);
};

πŸ› Debugging & Testing

Local Development Debugging

Use npm link for local development:

# In plugin project directory
npm link

# In Dataset Viewer project directory
npm link dataset-viewer-plugin-example

# Build plugin (watch mode)
cd path/to/your-plugin
npm run dev

Install Local Plugin

Install local plugin in Dataset Viewer:

  1. Build plugin: npm run build
  2. Open Dataset Viewer
  3. Go to Plugin Manager
  4. Select "Install from Local"
  5. Select plugin's dist/index.js file

Debug Logging

Use browser developer tools to view plugin logs:

const MyViewer: React.FC<PluginViewerProps> = ({ file }) => {
  useEffect(() => {
    console.log('[MyPlugin] Loading file:', file);
  }, [file]);

  // ...
};

Testing Checklist

  • Normal size files (<1MB) display correctly
  • Large files (>10MB) use chunked loading
  • Error cases show user-friendly messages
  • Loading state is displayed
  • Works properly on different screen sizes
  • Language switching works correctly
  • Resources are cleaned up after plugin uninstall

πŸ“¦ Publishing Plugin

Package Plugin

# Build production version
npm run build

# Package as zip file
cd dist
zip -r ../my-plugin-v1.0.0.zip .

Publish to npm

# Login to npm
npm login

# Publish
npm publish --access public

Plugin Manifest File

Create plugin.json to describe plugin information:

{
  "id": "custom-viewer",
  "name": "Custom Viewer",
  "version": "1.0.0",
  "description": "Custom file viewer for Dataset Viewer",
  "author": "Your Name",
  "homepage": "https://github.com/yourusername/dataset-viewer-plugin-custom",
  "repository": {
    "type": "git",
    "url": "https://github.com/yourusername/dataset-viewer-plugin-custom.git"
  },
  "license": "MIT",
  "keywords": ["dataset-viewer", "plugin", "viewer"],
  "entryPoint": "index.js"
}

πŸ”§ Advanced Topics

Custom Icon Mapping

Provide different icons for different file extensions:

import { FileText, FileCode } from 'lucide-react';

export default createPlugin({
  metadata: {
    // ...
    icon: <FileText className="text-blue-500" />,
    iconMapping: {
      '.txt': <FileText className="text-gray-600" />,
      '.log': <FileCode className="text-green-600" />,
    },
  },
  component: MyViewer,
});

Performance Optimization

Use virtual scrolling to handle large amounts of content:

import { useVirtualizer } from '@tanstack/react-virtual';

const MyViewer: React.FC<PluginViewerProps> = ({ content }) => {
  const parentRef = React.useRef<HTMLDivElement>(null);
  const lines = content.split('\n');

  const virtualizer = useVirtualizer({
    count: lines.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 24, // Height per line
  });

  return (
    <div ref={parentRef} className="h-full overflow-auto">
      <div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
        {virtualizer.getVirtualItems().map((item) => (
          <div
            key={item.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${item.size}px`,
              transform: `translateY(${item.start}px)`,
            }}
          >
            {lines[item.index]}
          </div>
        ))}
      </div>
    </div>
  );
};

πŸ“– Resources

Official Resources

Recommended Libraries

Community


πŸ’¬ FAQ

Q: What file formats does the plugin support?

A: Any text or binary format can be supported, defined through the supportedExtensions field.

Q: How to access file content?

A: Use methods provided by fileAccessor, such as getTextContent() or getBinaryContent().

Q: Can plugins call external APIs?

A: Yes, plugins run in the browser environment and can use Web APIs like fetch.

Q: How to handle large files?

A: Use fileAccessor.getFileChunk() to read in chunks, avoiding loading entire content at once.

Q: Does the plugin support TypeScript?

A: Yes, the SDK provides complete TypeScript type definitions.

Q: How to test plugins?

A: Use npm link for testing in local development environment, or install the built plugin file directly.

Q: Can plugins depend on other npm packages?

A: Yes, but they need to be bundled into the plugin during build, or ensure they are peerDependencies (like react, react-dom).


πŸŽ‰ Start Developing

Now that you've mastered the basics of developing Dataset Viewer plugins, start creating your first plugin!

If you have questions, feel free to ask on GitHub Issues.

Happy Coding! πŸš€

Clone this wiki locally