-
Notifications
You must be signed in to change notification settings - Fork 39
Plugin Development Guide
This guide will help you quickly develop third-party plugins for Dataset Viewer to extend the application's file viewing capabilities.
- Quick Start
- Plugin Architecture
- Development Steps
- API Reference
- Best Practices
- Debugging & Testing
- Publishing Plugin
# 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-devCreate 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,
});Install β Activate β Initialize β Run β Cleanup β Uninstall
- Install: Plugin is downloaded to application directory
- Activate: User enables the plugin
-
Initialize: Execute
initialize()function (optional) - Run: Render plugin component to handle files
-
Cleanup: Execute
cleanup()function (optional) - Uninstall: Remove plugin from application
Dataset Viewer supports two plugin loading modes:
// Plugin will be bundled as CJS format
module.exports = {
metadata: { /* ... */ },
component: YourComponent,
};// Use ES Module during development
export default createPlugin({ /* ... */ });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
{
"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"
}
}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',
},
},
},
},
});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');
},
});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) |
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
}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>;
}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
}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;
};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
};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
};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>
);
};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,
});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);
};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 devInstall local plugin in Dataset Viewer:
- Build plugin:
npm run build - Open Dataset Viewer
- Go to Plugin Manager
- Select "Install from Local"
- Select plugin's
dist/index.jsfile
Use browser developer tools to view plugin logs:
const MyViewer: React.FC<PluginViewerProps> = ({ file }) => {
useEffect(() => {
console.log('[MyPlugin] Loading file:', file);
}, [file]);
// ...
};- 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
# Build production version
npm run build
# Package as zip file
cd dist
zip -r ../my-plugin-v1.0.0.zip .# Login to npm
npm login
# Publish
npm publish --access publicCreate 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"
}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,
});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>
);
};- Lucide Icons - Icon library
- Tailwind CSS - Styling framework
- @tanstack/react-virtual - Virtual scrolling
- i18next - Internationalization
- GitHub Issues - Report issues
- GitHub Discussions - Discussion forum
A: Any text or binary format can be supported, defined through the supportedExtensions field.
A: Use methods provided by fileAccessor, such as getTextContent() or getBinaryContent().
A: Yes, plugins run in the browser environment and can use Web APIs like fetch.
A: Use fileAccessor.getFileChunk() to read in chunks, avoiding loading entire content at once.
A: Yes, the SDK provides complete TypeScript type definitions.
A: Use npm link for testing in local development environment, or install the built plugin file directly.
A: Yes, but they need to be bundled into the plugin during build, or ensure they are peerDependencies (like react, react-dom).
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! π