Chapter 15: Publishing and Maintenance

Haiyue
52min

Chapter 15: Publishing and Maintenance

Learning Objectives

  1. Master the Chrome Web Store publishing process
  2. Learn extension packaging and optimization techniques
  3. Understand version management and update strategies
  4. Master user feedback collection and issue handling
  5. Learn long-term maintenance and operation of extensions

1. Chrome Web Store Publishing Preparation

1.1 Pre-Publishing Checklist

Before publishing a Chrome extension, a comprehensive review and preparation are required.

// src/utils/pre-publish-checker.js
class PrePublishChecker {
  constructor() {
    this.checks = [
      { name: 'manifest_validation', required: true },
      { name: 'permissions_review', required: true },
      { name: 'icon_validation', required: true },
      { name: 'functionality_test', required: true },
      { name: 'performance_check', required: false },
      { name: 'security_audit', required: true },
      { name: 'compatibility_test', required: true },
      { name: 'documentation_review', required: false }
    ];

    this.results = new Map();
  }

  async runAllChecks() {
    console.log('🔍 Starting pre-publish validation...');

    for (const check of this.checks) {
      try {
        const result = await this.runCheck(check.name);
        this.results.set(check.name, result);

        if (check.required && !result.passed) {
          console.error(`❌ Required check failed: ${check.name}`);
          console.error(result.message);
        } else if (result.passed) {
          console.log(`✅ ${check.name}: PASSED`);
        } else {
          console.warn(`⚠️ ${check.name}: ${result.message}`);
        }
      } catch (error) {
        console.error(`💥 Error running ${check.name}:`, error);
        this.results.set(check.name, {
          passed: false,
          message: `Check failed with error: ${error.message}`
        });
      }
    }

    return this.generateReport();
  }

  async runCheck(checkName) {
    switch (checkName) {
      case 'manifest_validation':
        return this.validateManifest();
      case 'permissions_review':
        return this.reviewPermissions();
      case 'icon_validation':
        return this.validateIcons();
      case 'functionality_test':
        return this.testFunctionality();
      case 'performance_check':
        return this.checkPerformance();
      case 'security_audit':
        return this.auditSecurity();
      case 'compatibility_test':
        return this.testCompatibility();
      case 'documentation_review':
        return this.reviewDocumentation();
      default:
        throw new Error(`Unknown check: ${checkName}`);
    }
  }

  async validateManifest() {
    const manifest = chrome.runtime.getManifest();
    const issues = [];

    // Check required fields
    const requiredFields = ['name', 'version', 'manifest_version', 'description'];
    for (const field of requiredFields) {
      if (!manifest[field]) {
        issues.push(`Missing required field: ${field}`);
      }
    }

    // Check version format
    const versionRegex = /^\d+(\.\d+){0,3}$/;
    if (!versionRegex.test(manifest.version)) {
      issues.push('Invalid version format. Use X.Y.Z.W format');
    }

    // Check icons
    if (!manifest.icons || Object.keys(manifest.icons).length === 0) {
      issues.push('No icons specified');
    }

    // Check permission rationality
    if (manifest.permissions) {
      const sensitivePermissions = ['<all_urls>', 'debugger', 'desktopCapture'];
      const hasSensitive = manifest.permissions.some(p => sensitivePermissions.includes(p));
      if (hasSensitive) {
        issues.push('Extension uses sensitive permissions that require justification');
      }
    }

    return {
      passed: issues.length === 0,
      message: issues.length > 0 ? issues.join('; ') : 'Manifest validation passed',
      details: { manifest, issues }
    };
  }

  async reviewPermissions() {
    const manifest = chrome.runtime.getManifest();
    const permissions = manifest.permissions || [];
    const warnings = [];

    // Permission usage suggestions
    const permissionAdvice = {
      'tabs': 'Consider using activeTab instead if you only need current tab access',
      'storage': 'Ensure you handle storage quota limits',
      'unlimitedStorage': 'Only request if you need to store large amounts of data',
      '<all_urls>': 'This is a sensitive permission. Provide clear justification',
      'webRequest': 'Consider using declarativeNetRequest for better performance'
    };

    for (const permission of permissions) {
      if (permissionAdvice[permission]) {
        warnings.push(`${permission}: ${permissionAdvice[permission]}`);
      }
    }

    return {
      passed: true,
      message: warnings.length > 0 ? 'Permission review completed with suggestions' : 'All permissions look appropriate',
      details: { permissions, warnings }
    };
  }

  async validateIcons() {
    const manifest = chrome.runtime.getManifest();
    const icons = manifest.icons || {};
    const issues = [];

    const requiredSizes = ['16', '48', '128'];
    for (const size of requiredSizes) {
      if (!icons[size]) {
        issues.push(`Missing ${size}px icon`);
      } else {
        // Try to validate if icon file exists
        try {
          const response = await fetch(chrome.runtime.getURL(icons[size]));
          if (!response.ok) {
            issues.push(`${size}px icon file not found: ${icons[size]}`);
          }
        } catch (error) {
          issues.push(`Failed to verify ${size}px icon: ${error.message}`);
        }
      }
    }

    return {
      passed: issues.length === 0,
      message: issues.length > 0 ? issues.join('; ') : 'All required icons are present',
      details: { icons, issues }
    };
  }

  async testFunctionality() {
    const tests = [];

    // Basic functionality tests
    try {
      // Test storage functionality
      await chrome.storage.local.set({ test_key: 'test_value' });
      const result = await chrome.storage.local.get('test_key');
      if (result.test_key === 'test_value') {
        tests.push({ name: 'Storage', passed: true });
      } else {
        tests.push({ name: 'Storage', passed: false, error: 'Storage test failed' });
      }
      await chrome.storage.local.remove('test_key');
    } catch (error) {
      tests.push({ name: 'Storage', passed: false, error: error.message });
    }

    // Test message passing
    try {
      const response = await chrome.runtime.sendMessage({ action: 'ping' });
      tests.push({ name: 'Messaging', passed: true });
    } catch (error) {
      tests.push({ name: 'Messaging', passed: false, error: 'No message handlers found' });
    }

    const failedTests = tests.filter(t => !t.passed);

    return {
      passed: failedTests.length === 0,
      message: failedTests.length > 0 ?
        `${failedTests.length} functionality tests failed` :
        'All functionality tests passed',
      details: { tests }
    };
  }

  async checkPerformance() {
    const metrics = {
      backgroundMemory: 0,
      loadTime: 0,
      responseTime: 0
    };

    // Check memory usage
    if (performance.memory) {
      metrics.backgroundMemory = performance.memory.usedJSHeapSize / 1024 / 1024; // MB
    }

    // Check load time
    const startTime = performance.now();
    // Simulate some operations
    await new Promise(resolve => setTimeout(resolve, 100));
    metrics.loadTime = performance.now() - startTime;

    const warnings = [];
    if (metrics.backgroundMemory > 50) {
      warnings.push('High memory usage in background script');
    }

    if (metrics.loadTime > 1000) {
      warnings.push('Slow initialization detected');
    }

    return {
      passed: warnings.length === 0,
      message: warnings.length > 0 ? warnings.join('; ') : 'Performance checks passed',
      details: { metrics, warnings }
    };
  }

  async auditSecurity() {
    const issues = [];
    const manifest = chrome.runtime.getManifest();

    // Check CSP settings
    if (!manifest.content_security_policy) {
      issues.push('No Content Security Policy defined');
    }

    // Check external connections
    if (manifest.permissions?.includes('<all_urls>')) {
      issues.push('Extension has access to all websites - ensure this is necessary');
    }

    // Check web_accessible_resources
    if (manifest.web_accessible_resources) {
      issues.push('Web accessible resources detected - review for security implications');
    }

    return {
      passed: issues.length === 0,
      message: issues.length > 0 ?
        'Security audit found potential issues' :
        'Security audit passed',
      details: { issues }
    };
  }

  async testCompatibility() {
    const userAgent = navigator.userAgent;
    const chromeVersion = this.extractChromeVersion(userAgent);
    const issues = [];

    // Check minimum Chrome version requirements
    const manifest = chrome.runtime.getManifest();
    const minVersion = manifest.minimum_chrome_version;

    if (minVersion && chromeVersion < parseInt(minVersion)) {
      issues.push(`Current Chrome version ${chromeVersion} is below minimum required ${minVersion}`);
    }

    // Check Manifest V3 compatibility
    if (manifest.manifest_version < 3) {
      issues.push('Consider upgrading to Manifest V3 for better future compatibility');
    }

    return {
      passed: issues.length === 0,
      message: issues.length > 0 ? issues.join('; ') : 'Compatibility checks passed',
      details: { chromeVersion, issues }
    };
  }

  extractChromeVersion(userAgent) {
    const match = userAgent.match(/Chrome\/(\d+)/);
    return match ? parseInt(match[1]) : 0;
  }

  async reviewDocumentation() {
    const files = ['README.md', 'CHANGELOG.md', 'LICENSE'];
    const missingFiles = [];

    for (const file of files) {
      try {
        await fetch(chrome.runtime.getURL(file));
      } catch {
        missingFiles.push(file);
      }
    }

    return {
      passed: missingFiles.length === 0,
      message: missingFiles.length > 0 ?
        `Missing documentation files: ${missingFiles.join(', ')}` :
        'Documentation review passed',
      details: { missingFiles }
    };
  }

  generateReport() {
    const totalChecks = this.checks.length;
    const passedChecks = Array.from(this.results.values()).filter(r => r.passed).length;
    const requiredChecks = this.checks.filter(c => c.required).length;
    const passedRequired = this.checks
      .filter(c => c.required)
      .filter(c => this.results.get(c.name)?.passed).length;

    const isReady = passedRequired === requiredChecks;

    const report = {
      timestamp: new Date().toISOString(),
      ready: isReady,
      summary: {
        total: totalChecks,
        passed: passedChecks,
        failed: totalChecks - passedChecks,
        required_passed: passedRequired,
        required_total: requiredChecks
      },
      results: Object.fromEntries(this.results),
      recommendations: this.generateRecommendations()
    };

    console.log('\n📊 Pre-publish Report:');
    console.log(`Ready for publication: ${isReady ? '✅ YES' : '❌ NO'}`);
    console.log(`Checks passed: ${passedChecks}/${totalChecks}`);
    console.log(`Required checks: ${passedRequired}/${requiredChecks}`);

    if (!isReady) {
      console.log('\n⚠️ Issues to fix before publication:');
      for (const [checkName, result] of this.results) {
        const check = this.checks.find(c => c.name === checkName);
        if (check.required && !result.passed) {
          console.log(`  - ${checkName}: ${result.message}`);
        }
      }
    }

    return report;
  }

  generateRecommendations() {
    const recommendations = [];

    for (const [checkName, result] of this.results) {
      if (!result.passed) {
        switch (checkName) {
          case 'manifest_validation':
            recommendations.push('Fix manifest.json errors before publishing');
            break;
          case 'icon_validation':
            recommendations.push('Ensure all required icons (16px, 48px, 128px) are present');
            break;
          case 'security_audit':
            recommendations.push('Review and address security concerns');
            break;
          case 'performance_check':
            recommendations.push('Optimize extension performance for better user experience');
            break;
        }
      }
    }

    return recommendations;
  }
}

1.2 Node.js Publishing Management Tool

// build-tools/extension-publisher.js
const fs = require('fs').promises;
const path = require('path');
const archiver = require('archiver');
const crypto = require('crypto');

class ChromeExtensionPublisher {
  /**
   * Chrome Extension Publishing Management Tool
   */
  constructor(projectPath) {
    this.projectPath = projectPath;
    this.distPath = path.join(projectPath, 'dist');
    this.buildPath = path.join(projectPath, 'build');
  }

  async prepareForPublishing() {
    /**
     * Prepares the package for publishing
     */
    console.log('🚀 Preparing extension for publishing...');

    const results = {
      success: false,
      packagePath: null,
      size: 0,
      filesCount: 0,
      errors: [],
      warnings: []
    };

    try {
      // 1. Clean build directory
      await this.cleanBuildDirectory();

      // 2. Copy source files
      await this.copySourceFiles();

      // 3. Optimize files
      await this.optimizeFiles();

      // 4. Validate manifest
      const { valid, errors } = await this.validateManifest();
      if (!valid) {
        results.errors.push(...errors);
        return results;
      }

      // 5. Create publishing package
      const packagePath = await this.createPackage();

      // 6. Generate checksum
      const checksum = await this.generateChecksum(packagePath);

      // 7. Get package information
      const stats = await fs.stat(packagePath);
      const packageSize = stats.size;
      const filesCount = await this.countFilesInZip(packagePath);

      Object.assign(results, {
        success: true,
        packagePath: packagePath,
        size: packageSize,
        filesCount: filesCount,
        checksum: checksum,
        buildTime: new Date().toISOString()
      });

      console.log(`✅ Package created successfully: ${packagePath}`);
      console.log(`📦 Size: ${(packageSize / 1024).toFixed(2)} KB`);
      console.log(`📄 Files: ${filesCount}`);
      console.log(`🔐 SHA256: ${checksum}`);

    } catch (error) {
      results.errors.push(`Build failed: ${error.message}`);
      console.error(`❌ Build failed: ${error}`);
    }

    return results;
  }

  async cleanBuildDirectory() {
    /**
     * Cleans the build directory
     */
    const fsExtra = require('fs-extra');

    if (await fsExtra.pathExists(this.buildPath)) {
      await fsExtra.remove(this.buildPath);
    }
    await fsExtra.ensureDir(this.buildPath);

    if (await fsExtra.pathExists(this.distPath)) {
      await fsExtra.remove(this.distPath);
    }
    await fsExtra.ensureDir(this.distPath);
  }

  async copySourceFiles() {
    /**
     * Copies source files to the build directory
     */
    const includePatterns = [
      'manifest.json',
      'src/**/*',
      'assets/**/*',
      'LICENSE',
      'README.md'
    ];

    const excludePatterns = [
      '**/.git/**',
      '**/node_modules/**',
      '**/*.test.js',
      '**/*.spec.js',
      '**/test/**',
      '**/tests/**',
      '**/*.map',
      '**/*.ts',
      '**/webpack.config.js',
      '**/package.json',
      '**/package-lock.json'
    ];

    await this.copyFilesWithPatterns(includePatterns, excludePatterns);
  }

  async copyFilesWithPatterns(includePatterns, excludePatterns) {
    /**
     * Copies files based on patterns
     */
    const fsExtra = require('fs-extra');
    const glob = require('glob');

    for (const pattern of includePatterns) {
      if (pattern === 'manifest.json') {
        const src = path.join(this.projectPath, 'manifest.json');
        if (await fsExtra.pathExists(src)) {
          await fsExtra.copy(src, path.join(this.buildPath, 'manifest.json'));
        }
      } else if (pattern.endsWith('/**/*')) {
        const baseDir = pattern.replace('/**/*', '');
        const srcDir = path.join(this.projectPath, baseDir);
        if (await fsExtra.pathExists(srcDir)) {
          const dstDir = path.join(this.buildPath, baseDir);
          await fsExtra.copy(srcDir, dstDir, {
            filter: (src) => {
              return !excludePatterns.some(exclude =>
                src.includes(exclude.replace('**/', '').replace('/**', ''))
              );
            }
          });
        }
      }
    }
  }

  async optimizeFiles() {
    /**
     * Optimizes files
     */
    await this.minifyJavaScript();
    await this.minifyCSS();
    await this.optimizeImages();
  }

  async minifyJavaScript() {
    /**
     * Minifies JavaScript files
     */
    const { minify } = require('terser');
    const files = await this.getFilesRecursive(this.buildPath, '.js');

    for (const file of files) {
      try {
        const code = await fs.readFile(file, 'utf-8');
        const result = await minify(code, {
          compress: true,
          mangle: true
        });

        if (result.code) {
          await fs.writeFile(file, result.code, 'utf-8');
        }
      } catch (error) {
        console.warn(`⚠️ Failed to minify ${file}: ${error.message}`);
      }
    }
  }

  async minifyCSS() {
    /**
     * Minifies CSS files
     */
    const CleanCSS = require('clean-css');
    const cleanCSS = new CleanCSS();
    const files = await this.getFilesRecursive(this.buildPath, '.css');

    for (const file of files) {
      try {
        const content = await fs.readFile(file, 'utf-8');
        const output = cleanCSS.minify(content);

        if (!output.errors || output.errors.length === 0) {
          await fs.writeFile(file, output.styles, 'utf-8');
        }
      } catch (error) {
        console.warn(`⚠️ Failed to minify ${file}: ${error.message}`);
      }
    }
  }

  async optimizeImages() {
    /**
     * Optimizes image files
     * Can integrate tools like imagemin
     */
    // TODO: Implement image optimization
  }

  async getFilesRecursive(dir, extension) {
    /**
     * Recursively gets files with a specific extension
     */
    const fsExtra = require('fs-extra');
    const files = [];

    const items = await fs.readdir(dir, { withFileTypes: true });

    for (const item of items) {
      const fullPath = path.join(dir, item.name);

      if (item.isDirectory()) {
        files.push(...await this.getFilesRecursive(fullPath, extension));
      } else if (item.name.endsWith(extension)) {
        files.push(fullPath);
      }
    }

    return files;
  }

  async validateManifest() {
    /**
     * Validates the manifest file
     */
    const manifestPath = path.join(this.buildPath, 'manifest.json');
    const errors = [];

    try {
      const content = await fs.readFile(manifestPath, 'utf-8');
      const manifest = JSON.parse(content);

      // Check required fields
      const requiredFields = ['name', 'version', 'manifest_version', 'description'];
      for (const field of requiredFields) {
        if (!manifest[field]) {
          errors.push(`Missing required field: ${field}`);
        }
      }

      // Check version format
      const version = manifest.version || '';
      if (!/^\d+(\.\d+){0,3}$/.test(version)) {
        errors.push('Invalid version format');
      }

      // Check icons
      if (!manifest.icons) {
        errors.push('No icons specified');
      } else {
        for (const [size, iconPath] of Object.entries(manifest.icons)) {
          const fullPath = path.join(this.buildPath, iconPath);
          const exists = await fs.access(fullPath).then(() => true).catch(() => false);
          if (!exists) {
            errors.push(`Icon file not found: ${iconPath}`);
          }
        }
      }

    } catch (error) {
      if (error.code === 'ENOENT') {
        errors.push('manifest.json not found');
      } else if (error instanceof SyntaxError) {
        errors.push(`Invalid JSON in manifest.json: ${error.message}`);
      } else {
        errors.push(`Error reading manifest.json: ${error.message}`);
      }
    }

    return { valid: errors.length === 0, errors };
  }

  async createPackage() {
    /**
     * Creates the publishing package
     */
    const manifestPath = path.join(this.buildPath, 'manifest.json');
    const content = await fs.readFile(manifestPath, 'utf-8');
    const manifest = JSON.parse(content);

    const extensionName = manifest.name.replace(/\s+/g, '-').toLowerCase();
    const version = manifest.version;
    const packageName = `${extensionName}-${version}.zip`;
    const packagePath = path.join(this.distPath, packageName);

    return new Promise((resolve, reject) => {
      const output = require('fs').createWriteStream(packagePath);
      const archive = archiver('zip', { zlib: { level: 9 } });

      output.on('close', () => resolve(packagePath));
      archive.on('error', reject);

      archive.pipe(output);
      archive.directory(this.buildPath, false);
      archive.finalize();
    });
  }

  async generateChecksum(filePath) {
    /**
     * Generates a file checksum
     */
    return new Promise((resolve, reject) => {
      const hash = crypto.createHash('sha256');
      const stream = require('fs').createReadStream(filePath);

      stream.on('data', (data) => hash.update(data));
      stream.on('end', () => resolve(hash.digest('hex')));
      stream.on('error', reject);
    });
  }

  async countFilesInZip(zipPath) {
    /**
     * Counts the number of files in a ZIP archive
     */
    const AdmZip = require('adm-zip');
    const zip = new AdmZip(zipPath);
    return zip.getEntries().length;
  }

  async createStoreAssets() {
    /**
     * Creates assets required for the store
     */
    const fsExtra = require('fs-extra');
    const assets = {};

    // Create screenshots directory
    const screenshotsDir = path.join(this.distPath, 'store-assets', 'screenshots');
    await fsExtra.ensureDir(screenshotsDir);

    // Create promotional images directory
    const promoDir = path.join(this.distPath, 'store-assets', 'promotional');
    await fsExtra.ensureDir(promoDir);

    // Generate store description template
    const descriptionFile = path.join(this.distPath, 'store-description.txt');
    await this.generateStoreDescription(descriptionFile);
    assets.description = descriptionFile;

    // Generate privacy policy template
    const privacyFile = path.join(this.distPath, 'privacy-policy.md');
    await this.generatePrivacyPolicy(privacyFile);
    assets.privacyPolicy = privacyFile;

    return assets;
  }

  async generateStoreDescription(filePath) {
    /**
     * Generates a store description template
     */
    const manifestPath = path.join(this.buildPath, 'manifest.json');
    const content = await fs.readFile(manifestPath, 'utf-8');
    const manifest = JSON.parse(content);

    const description = `# ${manifest.name}

${manifest.description}

## Features
- Feature 1: Description
- Feature 2: Description
- Feature 3: Description

## How to Use
1. Install the extension
2. Click on the extension icon
3. Follow the on-screen instructions

## Privacy
This extension respects your privacy. See our privacy policy for details.

## Support
For support and feedback, please visit: [Your Support URL]

## Version ${manifest.version}
- Improvement 1
- Improvement 2
- Bug fixes
`;

    await fs.writeFile(filePath, description, 'utf-8');
  }

  async generatePrivacyPolicy(filePath) {
    /**
     * Generates a privacy policy template
     */
    const manifestPath = path.join(this.buildPath, 'manifest.json');
    const content = await fs.readFile(manifestPath, 'utf-8');
    const manifest = JSON.parse(content);

    const policy = `# Privacy Policy for ${manifest.name}

## Information Collection
This extension may collect the following types of information:
- [List the types of data collected]

## Information Use
We use the collected information for:
- [Describe how data is used]

## Information Sharing
We do not share your personal information with third parties except:
- [List any exceptions]

## Data Security
We implement appropriate security measures to protect your information.

## Changes to This Policy
We may update this privacy policy from time to time.

## Contact
If you have questions about this privacy policy, please contact us at: [Your Contact Email]

Last updated: ${new Date().toISOString().split('T')[0]}
`;

    await fs.writeFile(filePath, policy, 'utf-8');
  }

  async validatePackage(packagePath) {
    /**
     * Validates the publishing package
     */
    const fsExtra = require('fs-extra');
    const AdmZip = require('adm-zip');

    const results = {
      valid: true,
      issues: [],
      warnings: [],
      sizeMb: 0,
      files: []
    };

    if (!await fsExtra.pathExists(packagePath)) {
      results.valid = false;
      results.issues.push('Package file not found');
      return results;
    }

    // Check file size
    const stats = await fs.stat(packagePath);
    const sizeBytes = stats.size;
    const sizeMb = sizeBytes / (1024 * 1024);
    results.sizeMb = sizeMb;

    if (sizeMb > 128) { // Chrome Web Store limit
      results.issues.push(`Package too large: ${sizeMb.toFixed(2)}MB (limit: 128MB)`);
    } else if (sizeMb > 100) {
      results.warnings.push(`Large package size: ${sizeMb.toFixed(2)}MB`);
    }

    // Validate ZIP content
    try {
      const zip = new AdmZip(packagePath);
      const entries = zip.getEntries();
      const fileList = entries.map(entry => entry.entryName);
      results.files = fileList;

      // Check required files
      if (!fileList.includes('manifest.json')) {
        results.issues.push('manifest.json not found in package');
      }

      // Check for suspicious files
      const suspiciousFiles = fileList.filter(f =>
        f.endsWith('.exe') || f.endsWith('.dll') || f.endsWith('.so')
      );

      if (suspiciousFiles.length > 0) {
        results.warnings.push(...suspiciousFiles.map(f => `Suspicious file: ${f}`));
      }

    } catch (error) {
      results.valid = false;
      results.issues.push(`Error reading package: ${error.message}`);
    }

    results.valid = results.issues.length === 0;
    return results;
  }
}

// Usage example
async function main() {
  const publisher = new ChromeExtensionPublisher('/path/to/extension/project');

  // Prepare publishing package
  const result = await publisher.prepareForPublishing();

  if (result.success) {
    console.log(`\n📦 Package ready: ${result.packagePath}`);

    // Create store assets
    const assets = await publisher.createStoreAssets();
    console.log(`📝 Store assets created: ${Object.keys(assets).length} files`);

    // Validate package
    const validation = await publisher.validatePackage(result.packagePath);
    if (validation.valid) {
      console.log('✅ Package validation passed');
    } else {
      console.log('❌ Package validation failed:');
      validation.issues.forEach(issue => console.log(`  - ${issue}`));
    }
  } else {
    console.log('❌ Package preparation failed:');
    result.errors.forEach(error => console.log(`  - ${error}`));
  }
}

// If this script is run directly
if (require.main === module) {
  main().catch(console.error);
}

module.exports = ChromeExtensionPublisher;

package.json dependencies configuration:

{
  "name": "chrome-extension-builder",
  "version": "1.0.0",
  "description": "Chrome Extension build and publish tools",
  "scripts": {
    "build": "node build-tools/extension-publisher.js"
  },
  "devDependencies": {
    "archiver": "^5.3.0",
    "adm-zip": "^0.5.10",
    "terser": "^5.16.0",
    "clean-css": "^5.3.0",
    "fs-extra": "^11.1.0"
  }
}

2. Chrome Web Store Publishing Process

2.1 Developer Account Setup

// src/utils/store-publisher.js
class StorePublisher {
  constructor(config) {
    this.config = config;
    this.apiEndpoint = 'https://www.googleapis.com/chromewebstore/v1.1';
  }

  async uploadExtension(packagePath, options = {}) {
    console.log('📤 Uploading extension to Chrome Web Store...');

    try {
      const accessToken = await this.getAccessToken();
      const uploadResult = await this.performUpload(packagePath, accessToken);

      if (options.publish) {
        const publishResult = await this.publishExtension(uploadResult.id, accessToken);
        return { upload: uploadResult, publish: publishResult };
      }

      return { upload: uploadResult };
    } catch (error) {
      console.error('❌ Upload failed:', error);
      throw error;
    }
  }

  async getAccessToken() {
    // Use Google OAuth2 to get an access token
    // This requires pre-configured client credentials
    const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      body: new URLSearchParams({
        grant_type: 'refresh_token',
        refresh_token: this.config.refreshToken,
        client_id: this.config.clientId,
        client_secret: this.config.clientSecret
      })
    });

    const tokenData = await tokenResponse.json();

    if (!tokenResponse.ok) {
      throw new Error(`Failed to get access token: ${tokenData.error_description}`);
    }

    return tokenData.access_token;
  }

  async performUpload(packagePath, accessToken) {
    const packageData = await this.readPackageFile(packagePath);

    const response = await fetch(
      `${this.apiEndpoint}/items/${this.config.extensionId}`,
      {
        method: 'PUT',
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/zip'
        },
        body: packageData
      }
    );

    const result = await response.json();

    if (!response.ok) {
      throw new Error(`Upload failed: ${result.error?.message || 'Unknown error'}`);
    }

    console.log('✅ Extension uploaded successfully');
    return result;
  }

  async publishExtension(extensionId, accessToken) {
    const response = await fetch(
      `${this.apiEndpoint}/items/${extensionId}/publish`,
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          target: 'default' // Or 'trustedTesters'
        })
      }
    );

    const result = await response.json();

    if (!response.ok) {
      throw new Error(`Publish failed: ${result.error?.message || 'Unknown error'}`);
    }

    console.log('🎉 Extension published successfully');
    return result;
  }

  async readPackageFile(packagePath) {
    const fs = require('fs').promises;
    return await fs.readFile(packagePath);
  }

  async getExtensionInfo(extensionId, accessToken) {
    const response = await fetch(
      `${this.apiEndpoint}/items/${extensionId}?projection=DRAFT`,
      {
        headers: {
          'Authorization': `Bearer ${accessToken}`
        }
      }
    );

    return await response.json();
  }

  async updateListing(extensionId, listingData, accessToken) {
    const response = await fetch(
      `${this.apiEndpoint}/items/${extensionId}`,
      {
        method: 'PUT',
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(listingData)
      }
    );

    return await response.json();
  }
}

// Publishing configuration
const publishConfig = {
  extensionId: 'your-extension-id',
  clientId: 'your-client-id',
  clientSecret: 'your-client-secret',
  refreshToken: 'your-refresh-token'
};

const publisher = new StorePublisher(publishConfig);

2.2 Store Asset Preparation

// src/utils/store-assets-generator.js
class StoreAssetsGenerator {
  constructor() {
    this.requiredAssets = {
      screenshots: {
        count: { min: 1, max: 5 },
        size: '1280x800 or 640x400',
        format: 'JPEG or PNG'
      },
      icon: {
        sizes: ['128x128'],
        format: 'PNG'
      },
      promotionalImages: {
        small: '440x280',
        large: '1400x560',
        marquee: '1400x560' // optional
      }
    };
  }

  generateStoreDescription(manifest, features, changelog) {
    const description = {
      name: manifest.name,
      summary: manifest.description,
      detailedDescription: this.buildDetailedDescription(features),
      changelog: this.formatChangelog(changelog),
      category: this.suggestCategory(manifest),
      language: 'en',
      regions: ['US', 'GB', 'CA', 'AU'] // Can be adjusted as needed
    };

    return description;
  }

  buildDetailedDescription(features) {
    let description = "## Key Features\n\n";

    features.forEach((feature, index) => {
      description += `${index + 1}. **${feature.title}**: ${feature.description}\n`;
    });

    description += "\n## How to Use\n\n";
    description += "1. Install the extension from Chrome Web Store\n";
    description += "2. Click on the extension icon in your browser toolbar\n";
    description += "3. Follow the on-screen instructions to get started\n\n";

    description += "## Privacy & Security\n\n";
    description += "This extension respects your privacy and follows Chrome's security best practices. ";
    description += "All data is stored locally on your device unless otherwise specified.\n\n";

    description += "## Support\n\n";
    description += "If you encounter any issues or have suggestions, please contact us through the support section.\n";

    return description;
  }

  formatChangelog(changelog) {
    return changelog.map(version => {
      let entry = `## Version ${version.version}\n`;
      if (version.date) {
        entry += `*Released: ${version.date}*\n\n`;
      }

      if (version.features && version.features.length > 0) {
        entry += "### New Features\n";
        version.features.forEach(feature => {
          entry += `- ${feature}\n`;
        });
        entry += "\n";
      }

      if (version.improvements && version.improvements.length > 0) {
        entry += "### Improvements\n";
        version.improvements.forEach(improvement => {
          entry += `- ${improvement}\n`;
        });
        entry += "\n";
      }

      if (version.bugfixes && version.bugfixes.length > 0) {
        entry += "### Bug Fixes\n";
        version.bugfixes.forEach(fix => {
          entry += `- ${fix}\n`;
        });
        entry += "\n";
      }

      return entry;
    }).join('\n---\n\n');
  }

  suggestCategory(manifest) {
    const categoryKeywords = {
      'Productivity': ['productivity', 'task', 'todo', 'note', 'organize', 'manage'],
      'Developer Tools': ['developer', 'code', 'debug', 'api', 'git'],
      'Communication': ['chat', 'email', 'social', 'message', 'contact'],
      'Shopping': ['shop', 'price', 'deal', 'coupon', 'buy'],
      'News & Weather': ['news', 'weather', 'feed', 'article'],
      'Education': ['learn', 'study', 'course', 'tutorial', 'education'],
      'Entertainment': ['game', 'music', 'video', 'fun', 'entertainment'],
      'Accessibility': ['accessibility', 'screen reader', 'magnifier', 'contrast']
    };

    const description = (manifest.description || '').toLowerCase();
    const name = (manifest.name || '').toLowerCase();
    const searchText = `${name} ${description}`;

    for (const [category, keywords] of Object.entries(categoryKeywords)) {
      if (keywords.some(keyword => searchText.includes(keyword))) {
        return category;
      }
    }

    return 'Productivity'; // Default category
  }

  generatePrivacyPolicy(extensionData) {
    const template = `# Privacy Policy

## Information Collection
This extension may collect and store the following information:
${extensionData.dataCollection.map(item => `- ${item}`).join('\n')}

## Information Usage
The collected information is used for:
${extensionData.dataUsage.map(item => `- ${item}`).join('\n')}

## Data Storage
${extensionData.storageLocation}

## Third-Party Services
${extensionData.thirdPartyServices || 'This extension does not use third-party services.'}

## Data Sharing
We do not share your personal information with third parties except:
- [List any exceptions]

## Data Security
We implement appropriate security measures to protect your information against unauthorized access, alteration, disclosure, or destruction.

## Changes to This Policy
We may update this privacy policy from time to time. We will notify you of any changes by posting the new policy on this page.

## Contact
If you have any questions about this privacy policy, please contact us at: ${extensionData.contactEmail}

Last updated: ${new Date().toLocaleDateString()}*
`;

    return template;
  }

  validateAssets(assetsPath) {
    const validation = {
      valid: true,
      errors: [],
      warnings: []
    };

    // Validate screenshots
    const screenshotPath = `${assetsPath}/screenshots`;
    const screenshots = this.getImageFiles(screenshotPath);

    if (screenshots.length < 1) {
      validation.errors.push('At least 1 screenshot is required');
    } else if (screenshots.length > 5) {
      validation.warnings.push('Maximum 5 screenshots recommended');
    }

    // Validate icons
    const iconPath = `${assetsPath}/icon128.png`;
    if (!this.fileExists(iconPath)) {
      validation.errors.push('128x128 icon is required');
    }

    // Validate promotional images (optional)
    const promoPath = `${assetsPath}/promo-small.png`;
    if (!this.fileExists(promoPath)) {
      validation.warnings.push('Promotional image recommended for better visibility');
    }

    validation.valid = validation.errors.length === 0;
    return validation;
  }

  getImageFiles(directory) {
    // Simulate getting a list of image files
    const fs = require('fs');
    if (!fs.existsSync(directory)) return [];

    return fs.readdirSync(directory)
      .filter(file => /\.(png|jpg|jpeg)$/i.test(file));
  }

  fileExists(filePath) {
    const fs = require('fs');
    return fs.existsSync(filePath);
  }
}

// Usage example
const generator = new StoreAssetsGenerator();

const features = [
  {
    title: "Smart Organization",
    description: "Automatically organize your bookmarks with AI-powered categorization"
  },
  {
    title: "Quick Search",
    description: "Find any bookmark instantly with powerful search functionality"
  },
  {
    title: "Cloud Sync",
    description: "Keep your bookmarks synchronized across all your devices"
  }
];

const changelog = [
  {
    version: "1.2.0",
    date: "2024-01-15",
    features: ["Added AI categorization", "Improved search algorithm"],
    improvements: ["Better performance", "Enhanced UI"],
    bugfixes: ["Fixed sync issues", "Resolved popup layout problems"]
  },
  {
    version: "1.1.0",
    date: "2024-01-01",
    features: ["Cloud synchronization"],
    improvements: ["Faster loading times"],
    bugfixes: ["Fixed bookmark deletion bug"]
  }
];

const manifest = chrome.runtime.getManifest();
const storeDescription = generator.generateStoreDescription(manifest, features, changelog);

3. Version Management and Updates

3.1 Automated Version Management

// src/utils/version-manager.js
class VersionManager {
  constructor() {
    this.manifest = chrome.runtime.getManifest();
    this.currentVersion = this.manifest.version;
  }

  parseVersion(version) {
    const parts = version.split('.').map(Number);
    return {
      major: parts[0] || 0,
      minor: parts[1] || 0,
      patch: parts[2] || 0,
      build: parts[3] || 0
    };
  }

  compareVersions(version1, version2) {
    const v1 = this.parseVersion(version1);
    const v2 = this.parseVersion(version2);

    const compareOrder = ['major', 'minor', 'patch', 'build'];

    for (const part of compareOrder) {
      if (v1[part] > v2[part]) return 1;
      if (v1[part] < v2[part]) return -1;
    }

    return 0;
  }

  incrementVersion(version, type = 'patch') {
    const parts = this.parseVersion(version);

    switch (type) {
      case 'major':
        parts.major++;
        parts.minor = 0;
        parts.patch = 0;
        parts.build = 0;
        break;
      case 'minor':
        parts.minor++;
        parts.patch = 0;
        parts.build = 0;
        break;
      case 'patch':
        parts.patch++;
        parts.build = 0;
        break;
      case 'build':
        parts.build++;
        break;
      default:
        throw new Error(`Invalid version type: ${type}`);
    }

    return `${parts.major}.${parts.minor}.${parts.patch}.${parts.build}`;
  }

  async checkForUpdates() {
    try {
      // Check Chrome Web Store version
      const storeVersion = await this.getStoreVersion();
      const comparison = this.compareVersions(storeVersion, this.currentVersion);

      return {
        hasUpdate: comparison > 0,
        currentVersion: this.currentVersion,
        latestVersion: storeVersion,
        updateAvailable: comparison > 0
      };
    } catch (error) {
      console.error('Failed to check for updates:', error);
      return {
        hasUpdate: false,
        currentVersion: this.currentVersion,
        error: error.message
      };
    }
  }

  async getStoreVersion() {
    // Simulate getting version information from Chrome Web Store API
    // Actual implementation requires using Chrome Web Store API
    const response = await fetch(
      `https://chrome.google.com/webstore/detail/${chrome.runtime.id}`
    );

    if (!response.ok) {
      throw new Error('Failed to fetch store information');
    }

    const html = await response.text();
    const versionMatch = html.match(/Version:\s*(\d+\.\d+\.\d+(?:\.\d+)?)/);

    if (!versionMatch) {
      throw new Error('Could not extract version from store page');
    }

    return versionMatch[1];
  }

  generateUpdateNotes(fromVersion, toVersion, changes) {
    const notes = {
      version: toVersion,
      previousVersion: fromVersion,
      releaseDate: new Date().toISOString().split('T')[0],
      changes: changes || [],
      migrationSteps: this.getMigrationSteps(fromVersion, toVersion)
    };

    return notes;
  }

  getMigrationSteps(fromVersion, toVersion) {
    const steps = [];
    const from = this.parseVersion(fromVersion);
    const to = this.parseVersion(toVersion);

    // Major version upgrade
    if (to.major > from.major) {
      steps.push({
        type: 'major_upgrade',
        description: 'Major version upgrade detected',
        actions: [
          'Clear old cache data',
          'Update storage schema',
          'Reset user preferences if needed'
        ]
      });
    }

    // Minor version upgrade
    if (to.minor > from.minor) {
      steps.push({
        type: 'minor_upgrade',
        description: 'New features available',
        actions: [
          'Update feature flags',
          'Migrate user settings'
        ]
      });
    }

    return steps;
  }

  async performUpdate(updateNotes) {
    console.log('🔄 Performing extension update...');

    try {
      // Execute migration steps
      for (const step of updateNotes.migrationSteps) {
        await this.executeMigrationStep(step);
      }

      // Update version record
      await this.updateVersionHistory(updateNotes);

      // Display update notification
      await this.showUpdateNotification(updateNotes);

      console.log('✅ Update completed successfully');
    } catch (error) {
      console.error('❌ Update failed:', error);
      throw error;
    }
  }

  async executeMigrationStep(step) {
    console.log(`Executing migration step: ${step.type}`);

    switch (step.type) {
      case 'major_upgrade':
        await this.performMajorUpgrade();
        break;
      case 'minor_upgrade':
        await this.performMinorUpgrade();
        break;
      default:
        console.warn(`Unknown migration step type: ${step.type}`);
    }
  }

  async performMajorUpgrade() {
    // Clean up old data
    const oldKeys = await chrome.storage.local.get();
    const keysToRemove = Object.keys(oldKeys).filter(key =>
      key.startsWith('legacy_') || key.startsWith('deprecated_')
    );

    if (keysToRemove.length > 0) {
      await chrome.storage.local.remove(keysToRemove);
    }

    // Update storage schema
    await this.updateStorageSchema();
  }

  async performMinorUpgrade() {
    // Update feature flags
    const currentFlags = await chrome.storage.local.get('feature_flags') || {};
    const updatedFlags = {
      ...currentFlags,
      new_feature_enabled: true,
      beta_features: false
    };

    await chrome.storage.local.set({ feature_flags: updatedFlags });
  }

  async updateStorageSchema() {
    // Check current storage schema version
    const { storage_schema_version = 1 } = await chrome.storage.local.get('storage_schema_version');
    const currentSchemaVersion = 2; // New schema version

    if (storage_schema_version < currentSchemaVersion) {
      // Execute schema migration
      await this.migrateStorageSchema(storage_schema_version, currentSchemaVersion);
      await chrome.storage.local.set({ storage_schema_version: currentSchemaVersion });
    }
  }

  async migrateStorageSchema(fromVersion, toVersion) {
    if (fromVersion === 1 && toVersion >= 2) {
      // Example: Migrate old format bookmark data to new format
      const { bookmarks = [] } = await chrome.storage.local.get('bookmarks');

      const migratedBookmarks = bookmarks.map(bookmark => ({
        ...bookmark,
        createdAt: bookmark.dateAdded || Date.now(),
        updatedAt: Date.now(),
        version: 2
      }));

      await chrome.storage.local.set({ bookmarks: migratedBookmarks });
    }
  }

  async updateVersionHistory(updateNotes) {
    const { version_history = [] } = await chrome.storage.local.get('version_history');

    version_history.unshift({
      version: updateNotes.version,
      previousVersion: updateNotes.previousVersion,
      updateDate: new Date().toISOString(),
      changes: updateNotes.changes
    });

    // Only keep the last 10 update records
    const recentHistory = version_history.slice(0, 10);

    await chrome.storage.local.set({ version_history: recentHistory });
  }

  async showUpdateNotification(updateNotes) {
    if (chrome.notifications) {
      await chrome.notifications.create({
        type: 'basic',
        iconUrl: 'assets/icon48.png',
        title: 'Extension Updated',
        message: `Updated to version ${updateNotes.version}. Click to see what's new.`, 
        contextMessage: updateNotes.changes.slice(0, 2).join(', ')
      });
    }
  }

  async getVersionHistory() {
    const { version_history = [] } = await chrome.storage.local.get('version_history');
    return version_history;
  }
}

// Initialize update checker
class UpdateChecker {
  constructor() {
    this.versionManager = new VersionManager();
    this.checkInterval = 24 * 60 * 60 * 1000; // Check once every 24 hours
  }

  startPeriodicCheck() {
    // Check immediately
    this.checkForUpdates();

    // Set up periodic checks
    setInterval(() => {
      this.checkForUpdates();
    }, this.checkInterval);
  }

  async checkForUpdates() {
    try {
      const updateInfo = await this.versionManager.checkForUpdates();

      if (updateInfo.hasUpdate) {
        await this.handleUpdateAvailable(updateInfo);
      }
    } catch (error) {
      console.error('Update check failed:', error);
    }
  }

  async handleUpdateAvailable(updateInfo) {
    console.log('📦 Update available:', updateInfo.latestVersion);

    // Can choose to auto-update or notify the user
    const shouldAutoUpdate = await this.shouldAutoUpdate();

    if (shouldAutoUpdate) {
      await this.performAutoUpdate(updateInfo);
    } else {
      await this.notifyUserOfUpdate(updateInfo);
    }
  }

  async shouldAutoUpdate() {
    const { auto_update = false } = await chrome.storage.local.get('auto_update');
    return auto_update;
  }

  async performAutoUpdate(updateInfo) {
    // Chrome will automatically update extensions, this mainly handles logic after update
    console.log('Auto-update enabled, preparing for update...');

    // Save update information for post-update processing
    await chrome.storage.local.set({
      pending_update: {
        fromVersion: updateInfo.currentVersion,
        toVersion: updateInfo.latestVersion,
        timestamp: Date.now()
      }
    });
  }

  async notifyUserOfUpdate(updateInfo) {
    if (chrome.notifications) {
      await chrome.notifications.create({
        type: 'basic',
        iconUrl: 'assets/icon48.png',
        title: 'Update Available',
        message: `Version ${updateInfo.latestVersion} is available. Your version: ${updateInfo.currentVersion}`,
        buttons: [
          { title: 'Update Now' },
          { title: 'Remind Later' }
        ]
      });
    }
  }

  async handlePostUpdateTasks() {
    const { pending_update } = await chrome.storage.local.get('pending_update');

    if (pending_update) {
      const updateNotes = this.versionManager.generateUpdateNotes(
        pending_update.fromVersion,
        pending_update.toVersion,
        ['Bug fixes and improvements', 'Performance enhancements']
      );

      await this.versionManager.performUpdate(updateNotes);
      await chrome.storage.local.remove('pending_update');
    }
  }
}

// Initialize update checker
const updateChecker = new UpdateChecker();
Publishing Best Practices
  1. Conduct comprehensive testing before publishing, including different Chrome versions
  2. Prepare detailed store descriptions and screenshots
  3. Set reasonable permissions, avoid over-requesting
  4. Provide a clear privacy policy
  5. Respond to user feedback and issues promptly
Publishing Notes
  • Chrome Web Store review may take several business days
  • Ensure compliance with all store policies
  • Sensitive permissions require detailed justification for use
  • Avoid misleading language in descriptions

Learning Summary

This chapter covered Chrome extension publishing and maintenance:

  1. Publishing Preparation: Code review, package optimization, asset preparation
  2. Store Publishing: Developer account, upload process, store optimization
  3. Version Management: Automated updates, migration strategies, history
  4. Long-term Maintenance: User feedback, issue fixing, feature updates

Mastering these skills will help developers successfully publish and maintain Chrome extensions.

Mermaid Publishing Flowchart

🔄 正在渲染 Mermaid 图表...

Categories