Deployment Best Practices
This guide covers best practices for packaging, distributing, and deploying Qirvo plugins. Learn about version management, marketplace submission, and deployment strategies.
Table of Contents
- Plugin Packaging
- Version Management
- Marketplace Submission
- Distribution Channels
- Update Strategies
- Monitoring and Analytics
Plugin Packaging
Build Optimization
// webpack.config.js - Optimized build configuration
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
mode: 'production',
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'plugin.js',
library: {
type: 'commonjs2'
},
clean: true
},
externals: {
// Don't bundle Qirvo SDK - it's provided by the runtime
'@qirvo/plugin-sdk': 'commonjs2 @qirvo/plugin-sdk',
'react': 'commonjs2 react',
'react-dom': 'commonjs2 react-dom'
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // Remove console.log in production
drop_debugger: true
},
mangle: {
keep_fnames: false
}
}
})
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
},
plugins: [
// Analyze bundle size in development
process.env.ANALYZE && new BundleAnalyzerPlugin()
].filter(Boolean),
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx']
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
}
]
}
};
Package.json Configuration
{
"name": "@qirvo/weather-plugin",
"version": "1.2.3",
"description": "Comprehensive weather plugin for Qirvo dashboard",
"main": "dist/plugin.js",
"types": "dist/index.d.ts",
"files": [
"dist/",
"manifest.json",
"README.md",
"LICENSE",
"CHANGELOG.md"
],
"scripts": {
"build": "webpack --mode=production",
"build:dev": "webpack --mode=development",
"build:analyze": "ANALYZE=true npm run build",
"test": "jest",
"test:coverage": "jest --coverage",
"lint": "eslint src --ext .ts,.tsx",
"lint:fix": "eslint src --ext .ts,.tsx --fix",
"type-check": "tsc --noEmit",
"prepublishOnly": "npm run test && npm run lint && npm run build",
"release": "semantic-release"
},
"keywords": [
"qirvo",
"plugin",
"weather",
"dashboard",
"widget"
],
"author": {
"name": "Your Name",
"email": "your.email@example.com",
"url": "https://github.com/yourusername"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/yourusername/qirvo-weather-plugin.git"
},
"bugs": {
"url": "https://github.com/yourusername/qirvo-weather-plugin/issues"
},
"homepage": "https://github.com/yourusername/qirvo-weather-plugin#readme",
"peerDependencies": {
"@qirvo/plugin-sdk": "^1.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"typescript": "^5.0.0",
"webpack": "^5.0.0",
"ts-loader": "^9.0.0"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
}
}
Manifest Validation
// scripts/validate-manifest.ts
import { readFileSync } from 'fs';
import { z } from 'zod';
const ManifestSchema = z.object({
manifest_version: z.literal(1),
name: z.string().min(1).max(50),
version: z.string().regex(/^\d+\.\d+\.\d+$/),
description: z.string().min(10).max(200),
author: z.object({
name: z.string(),
email: z.string().email(),
url: z.string().url().optional()
}),
permissions: z.array(z.enum([
'storage-read', 'storage-write', 'network-access',
'notifications', 'filesystem-access', 'clipboard-read',
'clipboard-write', 'geolocation', 'camera', 'microphone',
'calendar', 'contacts'
])),
main: z.string(),
type: z.enum(['dashboard-widget', 'cli-tool', 'background-service', 'hybrid']),
category: z.enum(['productivity', 'utilities', 'entertainment', 'development', 'business']),
tags: z.array(z.string()).max(10),
homepage: z.string().url().optional(),
repository: z.string().url().optional(),
license: z.string(),
configSchema: z.object({}).optional(),
ui: z.object({
component: z.string().optional(),
styles: z.string().optional()
}).optional()
});
export function validateManifest(): void {
try {
const manifestContent = readFileSync('manifest.json', 'utf-8');
const manifest = JSON.parse(manifestContent);
const result = ManifestSchema.safeParse(manifest);
if (!result.success) {
console.error('❌ Manifest validation failed:');
result.error.issues.forEach(issue => {
console.error(` - ${issue.path.join('.')}: ${issue.message}`);
});
process.exit(1);
}
console.log('✅ Manifest validation passed');
} catch (error) {
console.error('❌ Failed to validate manifest:', error.message);
process.exit(1);
}
}
if (require.main === module) {
validateManifest();
}
Version Management
Semantic Versioning
// scripts/version-check.ts
import { readFileSync } from 'fs';
import semver from 'semver';
interface VersionInfo {
current: string;
previous: string;
type: 'major' | 'minor' | 'patch';
}
export function analyzeVersionChange(): VersionInfo {
const packageJson = JSON.parse(readFileSync('package.json', 'utf-8'));
const manifest = JSON.parse(readFileSync('manifest.json', 'utf-8'));
// Ensure package.json and manifest.json versions match
if (packageJson.version !== manifest.version) {
throw new Error('Version mismatch between package.json and manifest.json');
}
const currentVersion = packageJson.version;
const previousVersion = getPreviousVersion(); // From git tags or registry
if (!semver.valid(currentVersion)) {
throw new Error(`Invalid version format: ${currentVersion}`);
}
const versionType = semver.diff(previousVersion, currentVersion);
return {
current: currentVersion,
previous: previousVersion,
type: versionType as 'major' | 'minor' | 'patch'
};
}
export function generateChangelog(versionInfo: VersionInfo): string {
const { current, previous, type } = versionInfo;
const changelogTemplate = `
## [${current}] - ${new Date().toISOString().split('T')[0]}
### ${type === 'major' ? 'Breaking Changes' : type === 'minor' ? 'Added' : 'Fixed'}
- TODO: Add changelog entries
### Migration Guide
${type === 'major' ? '- TODO: Add migration instructions for breaking changes' : '- No migration required'}
[${current}]: https://github.com/yourusername/plugin/compare/v${previous}...v${current}
`;
return changelogTemplate.trim();
}
Release Automation
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Run linting
run: npm run lint
- name: Type check
run: npm run type-check
- name: Validate manifest
run: npm run validate-manifest
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build plugin
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: plugin-build
path: dist/
release:
if: github.ref == 'refs/heads/main'
needs: [test, build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: plugin-build
path: dist/
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release
Marketplace Submission
Submission Checklist
// scripts/pre-submission-check.ts
import { readFileSync, existsSync, statSync } from 'fs';
import { execSync } from 'child_process';
interface SubmissionCheck {
name: string;
check: () => boolean;
fix?: string;
}
const submissionChecks: SubmissionCheck[] = [
{
name: 'Manifest file exists and is valid',
check: () => {
try {
const manifest = JSON.parse(readFileSync('manifest.json', 'utf-8'));
return manifest.manifest_version === 1 && manifest.name && manifest.version;
} catch {
return false;
}
},
fix: 'Create a valid manifest.json file'
},
{
name: 'README.md exists with proper content',
check: () => {
if (!existsSync('README.md')) return false;
const readme = readFileSync('README.md', 'utf-8');
return readme.length > 200 && readme.includes('## Installation');
},
fix: 'Create a comprehensive README.md with installation instructions'
},
{
name: 'LICENSE file exists',
check: () => existsSync('LICENSE'),
fix: 'Add a LICENSE file'
},
{
name: 'CHANGELOG.md exists',
check: () => existsSync('CHANGELOG.md'),
fix: 'Create a CHANGELOG.md file documenting version changes'
},
{
name: 'Built files exist',
check: () => existsSync('dist/plugin.js'),
fix: 'Run npm run build to generate distribution files'
},
{
name: 'Bundle size is reasonable (<500KB)',
check: () => {
if (!existsSync('dist/plugin.js')) return false;
const stats = statSync('dist/plugin.js');
return stats.size < 500 * 1024; // 500KB
},
fix: 'Optimize bundle size by removing unused dependencies'
},
{
name: 'All tests pass',
check: () => {
try {
execSync('npm test', { stdio: 'pipe' });
return true;
} catch {
return false;
}
},
fix: 'Fix failing tests'
},
{
name: 'No linting errors',
check: () => {
try {
execSync('npm run lint', { stdio: 'pipe' });
return true;
} catch {
return false;
}
},
fix: 'Fix linting errors with npm run lint:fix'
},
{
name: 'TypeScript compiles without errors',
check: () => {
try {
execSync('npm run type-check', { stdio: 'pipe' });
return true;
} catch {
return false;
}
},
fix: 'Fix TypeScript compilation errors'
},
{
name: 'Screenshots exist (if UI plugin)',
check: () => {
const manifest = JSON.parse(readFileSync('manifest.json', 'utf-8'));
if (manifest.type === 'cli-tool' || manifest.type === 'background-service') {
return true; // Not required for non-UI plugins
}
return existsSync('screenshots') || existsSync('assets/screenshots');
},
fix: 'Add screenshots showing your plugin in action'
}
];
export function runSubmissionChecks(): void {
console.log('🔍 Running pre-submission checks...\n');
let allPassed = true;
const failed: SubmissionCheck[] = [];
for (const check of submissionChecks) {
const passed = check.check();
const status = passed ? '✅' : '❌';
console.log(`${status} ${check.name}`);
if (!passed) {
allPassed = false;
failed.push(check);
}
}
if (allPassed) {
console.log('\n🎉 All checks passed! Your plugin is ready for submission.');
} else {
console.log('\n❌ Some checks failed. Please fix the following issues:\n');
failed.forEach(check => {
console.log(`- ${check.name}`);
if (check.fix) {
console.log(` Fix: ${check.fix}`);
}
console.log();
});
process.exit(1);
}
}
if (require.main === module) {
runSubmissionChecks();
}
Plugin Metadata
{
"manifest_version": 1,
"name": "Weather Dashboard Widget",
"version": "1.2.3",
"description": "Beautiful weather widget with forecasts, alerts, and customizable themes",
"author": {
"name": "Your Name",
"email": "your.email@example.com",
"url": "https://github.com/yourusername"
},
"permissions": [
"storage-read",
"storage-write",
"network-access",
"notifications",
"geolocation"
],
"main": "dist/plugin.js",
"type": "dashboard-widget",
"category": "utilities",
"tags": ["weather", "forecast", "dashboard", "widget", "alerts"],
"homepage": "https://github.com/yourusername/weather-plugin",
"repository": "https://github.com/yourusername/weather-plugin.git",
"license": "MIT",
"pricing": {
"type": "free"
},
"compatibility": {
"qirvo_version": ">=1.0.0",
"node_version": ">=16.0.0"
},
"screenshots": [
"screenshots/main-widget.png",
"screenshots/settings.png",
"screenshots/forecast-view.png"
],
"configSchema": {
"type": "object",
"properties": {
"apiKey": {
"type": "string",
"title": "Weather API Key",
"description": "API key from your weather service provider"
},
"units": {
"type": "string",
"enum": ["metric", "imperial"],
"default": "metric",
"title": "Temperature Units"
}
},
"required": ["apiKey"]
}
}
Distribution Channels
NPM Package Distribution
// scripts/publish-npm.ts
import { execSync } from 'child_process';
import { readFileSync } from 'fs';
export function publishToNPM(): void {
const packageJson = JSON.parse(readFileSync('package.json', 'utf-8'));
console.log(`📦 Publishing ${packageJson.name}@${packageJson.version} to NPM...`);
try {
// Dry run first
execSync('npm publish --dry-run', { stdio: 'inherit' });
// Actual publish
execSync('npm publish --access public', { stdio: 'inherit' });
console.log('✅ Successfully published to NPM');
console.log(`📋 Install with: npm install ${packageJson.name}`);
} catch (error) {
console.error('❌ NPM publish failed:', error.message);
process.exit(1);
}
}
GitHub Releases
// scripts/create-github-release.ts
import { Octokit } from '@octokit/rest';
import { readFileSync } from 'fs';
export async function createGitHubRelease(): Promise<void> {
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN
});
const packageJson = JSON.parse(readFileSync('package.json', 'utf-8'));
const changelog = readFileSync('CHANGELOG.md', 'utf-8');
// Extract release notes for current version
const releaseNotes = extractReleaseNotes(changelog, packageJson.version);
try {
const release = await octokit.rest.repos.createRelease({
owner: 'yourusername',
repo: 'weather-plugin',
tag_name: `v${packageJson.version}`,
name: `Release ${packageJson.version}`,
body: releaseNotes,
draft: false,
prerelease: packageJson.version.includes('-')
});
console.log(`✅ GitHub release created: ${release.data.html_url}`);
} catch (error) {
console.error('❌ Failed to create GitHub release:', error.message);
process.exit(1);
}
}
function extractReleaseNotes(changelog: string, version: string): string {
const versionHeader = `## [${version}]`;
const startIndex = changelog.indexOf(versionHeader);
if (startIndex === -1) {
return `Release ${version}`;
}
const nextVersionIndex = changelog.indexOf('\n## [', startIndex + 1);
const endIndex = nextVersionIndex === -1 ? changelog.length : nextVersionIndex;
return changelog.slice(startIndex, endIndex).trim();
}
Update Strategies
Automatic Updates
// src/updateManager.ts
export class PluginUpdateManager {
constructor(
private currentVersion: string,
private updateCheckUrl: string
) {}
async checkForUpdates(): Promise<UpdateInfo | null> {
try {
const response = await fetch(`${this.updateCheckUrl}/latest`);
const updateInfo: UpdateInfo = await response.json();
if (this.isNewerVersion(updateInfo.version)) {
return updateInfo;
}
return null;
} catch (error) {
console.error('Failed to check for updates:', error);
return null;
}
}
private isNewerVersion(remoteVersion: string): boolean {
return semver.gt(remoteVersion, this.currentVersion);
}
async downloadUpdate(updateInfo: UpdateInfo): Promise<void> {
// Implementation depends on update mechanism
// Could be automatic download or redirect to marketplace
}
async scheduleUpdateCheck(): Promise<void> {
// Check for updates daily
setInterval(async () => {
const update = await this.checkForUpdates();
if (update) {
await this.notifyUserOfUpdate(update);
}
}, 24 * 60 * 60 * 1000);
}
private async notifyUserOfUpdate(updateInfo: UpdateInfo): Promise<void> {
// Show update notification to user
await this.showUpdateNotification({
title: 'Update Available',
message: `Version ${updateInfo.version} is now available`,
actions: [
{ label: 'Update Now', action: () => this.downloadUpdate(updateInfo) },
{ label: 'Later', action: () => {} }
]
});
}
}
interface UpdateInfo {
version: string;
releaseNotes: string;
downloadUrl: string;
critical: boolean;
}
Monitoring and Analytics
Usage Analytics
// src/analytics.ts
export class PluginAnalytics {
private events: AnalyticsEvent[] = [];
private flushInterval: NodeJS.Timeout;
constructor(private pluginId: string, private apiEndpoint: string) {
this.flushInterval = setInterval(() => this.flush(), 60000); // Flush every minute
}
track(event: string, properties: Record<string, any> = {}): void {
const analyticsEvent: AnalyticsEvent = {
event,
properties: {
...properties,
pluginId: this.pluginId,
timestamp: new Date().toISOString(),
sessionId: this.getSessionId()
}
};
this.events.push(analyticsEvent);
// Flush immediately for critical events
if (event === 'error' || event === 'crash') {
this.flush();
}
}
private async flush(): Promise<void> {
if (this.events.length === 0) return;
const eventsToSend = [...this.events];
this.events = [];
try {
await fetch(`${this.apiEndpoint}/analytics`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ events: eventsToSend })
});
} catch (error) {
// Re-add events if sending failed
this.events.unshift(...eventsToSend);
console.error('Failed to send analytics:', error);
}
}
private getSessionId(): string {
// Generate or retrieve session ID
return 'session-' + Date.now();
}
destroy(): void {
clearInterval(this.flushInterval);
this.flush(); // Final flush
}
}
interface AnalyticsEvent {
event: string;
properties: Record<string, any>;
}
This comprehensive deployment guide ensures your Qirvo plugins are properly packaged, versioned, and distributed through appropriate channels with monitoring capabilities.
Next: Testing Documentation