Chapter 15: Publishing and Maintenance
Haiyue
52min
Chapter 15: Publishing and Maintenance
Learning Objectives
- Master the Chrome Web Store publishing process
- Learn extension packaging and optimization techniques
- Understand version management and update strategies
- Master user feedback collection and issue handling
- 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
- Conduct comprehensive testing before publishing, including different Chrome versions
- Prepare detailed store descriptions and screenshots
- Set reasonable permissions, avoid over-requesting
- Provide a clear privacy policy
- 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:
- Publishing Preparation: Code review, package optimization, asset preparation
- Store Publishing: Developer account, upload process, store optimization
- Version Management: Automated updates, migration strategies, history
- 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 图表...