#!/usr/bin/env node /** * Build script for Chrome Web Store production package * Usage: node scripts/build-chrome-store.mjs * * This script: * 1. Sets ALLOW_INSTANCE_CONFIG = false in sidepanel.js * 2. Removes localhost permissions from manifest.json * 3. Copies and generates icons from public/icons/ * 4. Creates a production-ready .zip package */ import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' import AdmZip from 'adm-zip' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const extRoot = path.resolve(__dirname, '..') const projectRoot = path.resolve(extRoot, '..') const publicIconsDir = path.join(projectRoot, 'public', 'icons') const distDir = path.join(extRoot, 'dist-chrome-store') // Colors for terminal output const colors = { reset: '\x1b[0m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', red: '\x1b[31m' } function log(message, color = 'reset') { console.log(`${colors[color]}${message}${colors.reset}`) } function ensureDir(dirPath) { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }) } } // Copy all files from source to destination, excluding specified patterns function copyFiles(src, dest, exclude = []) { ensureDir(dest) const entries = fs.readdirSync(src, { withFileTypes: true }) for (const entry of entries) { const srcPath = path.join(src, entry.name) const relPath = path.relative(extRoot, srcPath) // Skip excluded files/directories if (exclude.some(pattern => relPath.match(pattern))) { continue } const destPath = path.join(dest, entry.name) if (entry.isDirectory()) { copyFiles(srcPath, destPath, exclude) } else { fs.copyFileSync(srcPath, destPath) } } } // Read and modify sidepanel.js for production function processSidepanelJs(content) { return content.replace( /const ALLOW_INSTANCE_CONFIG = true/, 'const ALLOW_INSTANCE_CONFIG = false' ) } // Read and modify manifest.json for production function processManifestJson(content) { const manifest = JSON.parse(content) manifest.host_permissions = [ 'https://memento-note.com/*', 'https://www.memento-note.com/*', ] if (manifest.content_scripts?.[0]) { manifest.content_scripts[0].matches = ['https://memento-note.com/*', 'https://www.memento-note.com/*'] } return JSON.stringify(manifest, null, 2) } // Generate PNG icons from SVG using sharp async function generateIcons() { log('๐Ÿ“ฆ Generating PNG icons from SVG...', 'blue') const sharp = (await import('sharp')).default const sizes = [16, 48, 128] // Source SVG files const icon512Svg = path.join(publicIconsDir, 'icon-512.svg') const icon192Svg = path.join(publicIconsDir, 'icon-192.svg') if (!fs.existsSync(icon512Svg) || !fs.existsSync(icon192Svg)) { log('โš ๏ธ Source SVG icons not found. Copying SVG files only.', 'yellow') // Copy SVG files as fallback fs.copyFileSync(icon512Svg, path.join(distDir, 'icon-512.svg')) fs.copyFileSync(icon192Svg, path.join(distDir, 'icon-192.svg')) return } // Generate PNG icons for (const size of sizes) { const sourceSvg = size >= 128 ? icon512Svg : icon192Svg const outputPath = path.join(distDir, `icon-${size}.png`) await sharp(sourceSvg) .resize(size, size) .png() .toFile(outputPath) log(` โœ“ Generated icon-${size}.png`, 'green') } // Also copy SVG files for reference fs.copyFileSync(icon512Svg, path.join(distDir, 'icon-512.svg')) fs.copyFileSync(icon192Svg, path.join(distDir, 'icon-192.svg')) } // Create ZIP package using AdmZip async function createZipPackage() { log('๐Ÿ“ฆ Creating ZIP package...', 'blue') const zipPath = path.join(extRoot, 'memento-web-clipper-chrome-store.zip') try { const zip = new AdmZip() // Add all files from dist directory const addFiles = (dir, base = '') => { const entries = fs.readdirSync(dir, { withFileTypes: true }) for (const entry of entries) { const fullPath = path.join(dir, entry.name) const relativePath = path.join(base, entry.name) if (entry.isDirectory()) { addFiles(fullPath, relativePath) } else { zip.addLocalFile(fullPath, base) } } } addFiles(distDir) // Write the zip file zip.writeZip(zipPath) // Get file size const stats = fs.statSync(zipPath) log(`โœ“ ZIP package created: ${zipPath}`, 'green') log(` Size: ${(stats.size / 1024).toFixed(2)} KB`, 'green') } catch (error) { throw new Error(`Failed to create ZIP: ${error.message}`) } } // Main build process async function build() { log('๐Ÿš€ Starting Chrome Web Store build...', 'blue') log('') try { // Clean dist directory log('๐Ÿงน Cleaning dist directory...', 'blue') if (fs.existsSync(distDir)) { fs.rmSync(distDir, { recursive: true, force: true }) } ensureDir(distDir) // Copy extension files (excluding build scripts and dist) log('๐Ÿ“‹ Copying extension files...', 'blue') copyFiles(extRoot, distDir, [ /^dist-/, /^scripts\//, /\.md$/, /^node_modules$/ ]) // Process sidepanel.js log('โš™๏ธ Processing sidepanel.js...', 'blue') const sidepanelPath = path.join(distDir, 'sidepanel.js') let sidepanelContent = fs.readFileSync(sidepanelPath, 'utf8') sidepanelContent = processSidepanelJs(sidepanelContent) fs.writeFileSync(sidepanelPath, sidepanelContent) log(' โœ“ Set ALLOW_INSTANCE_CONFIG = false', 'green') // Process manifest.json log('โš™๏ธ Processing manifest.json...', 'blue') const manifestPath = path.join(distDir, 'manifest.json') let manifestContent = fs.readFileSync(manifestPath, 'utf8') manifestContent = processManifestJson(manifestContent) // Add icons to manifest const manifest = JSON.parse(manifestContent) manifest.icons = { "16": "icon-16.png", "48": "icon-48.png", "128": "icon-128.png" } manifest.action = { ...manifest.action, "default_icon": { "16": "icon-16.png", "48": "icon-48.png", "128": "icon-128.png" } } fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)) log(' โœ“ Removed localhost permissions', 'green') log(' โœ“ Added icon definitions', 'green') // Generate icons await generateIcons() // Create ZIP package await createZipPackage() log('') log('โœ… Build completed successfully!', 'green') log('') log('๐Ÿ“ฆ Output files:', 'blue') log(` โ€ข Package: ${path.join(extRoot, 'memento-web-clipper-chrome-store.zip')}`, 'reset') log(` โ€ข Dist dir: ${distDir}`, 'reset') log('') log('๐Ÿ“ Next steps:', 'blue') log(' 1. Test the extension by loading the dist-chrome-store folder in Chrome (chrome://extensions)', 'reset') log(' 2. Upload the .zip file to Chrome Web Store Developer Dashboard', 'reset') log('') } catch (error) { log('') log('โŒ Build failed!', 'red') log(` Error: ${error.message}`, 'red') process.exit(1) } } // Run the build build()