- Nouvelle page /privacy bilingue (FR/EN) avec sélection par Accept-Language
ou ?lang=, sections complètes (données collectées, IA/BYOK, RGPD, sécurité,
contact). Style sombre aligné sur la landing (x.ai premium).
- Liens footer landing.footer.legal.* mis à jour vers /privacy dans les 15 locales.
- Extension Web Clipper rendue cross-browser :
* background.js détecte chrome.sidePanel vs browser.sidebarAction
* sidepanel.js ajoute un shim chrome<->browser pour Firefox
* manifest.json inclut sidebar_action + browser_specific_settings.gecko
+ optional_host_permissions (Chrome les ignore)
- Script scripts/build-firefox.mjs pour produire un .xpi signé pour AMO,
symétrique de build-chrome-store.mjs. Exclusion mutuelle des artefacts
Chrome/Firefox pour éviter la cross-contamination.
- Branding corrigé : 176 occurrences de 'Momento' -> 'Memento' dans le
bundle de traductions embarqué.
- Version bumpée 0.3.1 -> 0.4.0.
259 lines
7.9 KiB
JavaScript
259 lines
7.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Build script for Firefox Add-ons (AMO) production package
|
|
* Usage: node scripts/build-firefox.mjs
|
|
*
|
|
* This script:
|
|
* 1. Sets ALLOW_INSTANCE_CONFIG = false in sidepanel.js
|
|
* 2. Restricts host_permissions to memento-note.com only
|
|
* 3. Removes side_panel (Chrome-only) from manifest.json
|
|
* 4. Keeps sidebar_action + browser_specific_settings (Firefox-specific)
|
|
* 5. Copies and generates icons from public/icons/
|
|
* 6. Creates a Firefox-ready .xpi 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-firefox')
|
|
|
|
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.
|
|
// `exclude` accepts RegExp or a function (relPath, isDirectory) -> boolean.
|
|
function copyFiles(src, dest, exclude = []) {
|
|
ensureDir(dest)
|
|
const entries = fs.readdirSync(src, { withFileTypes: true })
|
|
|
|
const isExcluded = (relPath, isDir) =>
|
|
exclude.some(rule => {
|
|
if (typeof rule === 'function') return rule(relPath, isDir)
|
|
return relPath.match(rule) !== null
|
|
})
|
|
|
|
for (const entry of entries) {
|
|
const srcPath = path.join(src, entry.name)
|
|
const relPath = path.relative(extRoot, srcPath)
|
|
|
|
if (isExcluded(relPath, entry.isDirectory())) {
|
|
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 (lock URL to memento-note.com)
|
|
function processSidepanelJs(content) {
|
|
return content.replace(
|
|
/const ALLOW_INSTANCE_CONFIG = true/,
|
|
'const ALLOW_INSTANCE_CONFIG = false'
|
|
)
|
|
}
|
|
|
|
// Read and modify manifest.json for Firefox production
|
|
function processManifestJson(content) {
|
|
const manifest = JSON.parse(content)
|
|
|
|
// Firefox doesn't support chrome.sidePanel — use sidebarAction (already declared).
|
|
delete manifest.side_panel
|
|
|
|
// Firefox MV3 service workers are supported, but background events are also
|
|
// reliable with background.scripts. Keep service_worker for simplicity.
|
|
|
|
// Restrict host_permissions to production. Keep optional_host_permissions as-is.
|
|
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]
|
|
|
|
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')
|
|
fs.copyFileSync(icon512Svg, path.join(distDir, 'icon-512.svg'))
|
|
fs.copyFileSync(icon192Svg, path.join(distDir, 'icon-192.svg'))
|
|
return
|
|
}
|
|
|
|
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')
|
|
}
|
|
|
|
fs.copyFileSync(icon512Svg, path.join(distDir, 'icon-512.svg'))
|
|
fs.copyFileSync(icon192Svg, path.join(distDir, 'icon-192.svg'))
|
|
}
|
|
|
|
// Create XPI (ZIP) package using AdmZip
|
|
async function createXpiPackage() {
|
|
log('📦 Creating XPI package...', 'blue')
|
|
|
|
const xpiPath = path.join(extRoot, 'memento-web-clipper-firefox.xpi')
|
|
|
|
try {
|
|
const zip = new AdmZip()
|
|
|
|
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)
|
|
zip.writeZip(xpiPath)
|
|
|
|
const stats = fs.statSync(xpiPath)
|
|
log(`✓ XPI package created: ${xpiPath}`, 'green')
|
|
log(` Size: ${(stats.size / 1024).toFixed(2)} KB`, 'green')
|
|
} catch (error) {
|
|
throw new Error(`Failed to create XPI: ${error.message}`)
|
|
}
|
|
}
|
|
|
|
async function build() {
|
|
log('🦊 Starting Firefox (AMO) build...', 'blue')
|
|
log('')
|
|
|
|
try {
|
|
log('🧹 Cleaning dist directory...', 'blue')
|
|
if (fs.existsSync(distDir)) {
|
|
fs.rmSync(distDir, { recursive: true, force: true })
|
|
}
|
|
ensureDir(distDir)
|
|
|
|
log('📋 Copying extension files...', 'blue')
|
|
copyFiles(extRoot, distDir, [
|
|
(rel, isDir) => isDir && /^dist-/i.test(rel),
|
|
(rel, isDir) => isDir && (rel === 'scripts' || rel === 'i18n'),
|
|
/\.md$/,
|
|
/^node_modules$/,
|
|
/^diagnose\.js$/,
|
|
/^test-sidepanel\.html$/,
|
|
/^memento-web-clipper-chrome-store\.zip$/,
|
|
/^memento-web-clipper-firefox\.xpi$/,
|
|
])
|
|
|
|
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')
|
|
|
|
log('⚙️ Processing manifest.json...', 'blue')
|
|
const manifestPath = path.join(distDir, 'manifest.json')
|
|
let manifestContent = fs.readFileSync(manifestPath, 'utf8')
|
|
manifestContent = processManifestJson(manifestContent)
|
|
|
|
const manifest = JSON.parse(manifestContent)
|
|
manifest.icons = {
|
|
"16": "icon-16.png",
|
|
"48": "icon-48.png",
|
|
"128": "icon-128.png"
|
|
}
|
|
// Firefox uses browser_action icon OR sidebar_action icon
|
|
manifest.action = {
|
|
...manifest.action,
|
|
"default_icon": {
|
|
"16": "icon-16.png",
|
|
"48": "icon-48.png",
|
|
"128": "icon-128.png"
|
|
}
|
|
}
|
|
if (manifest.sidebar_action) {
|
|
manifest.sidebar_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 side_panel (Chrome-only)', 'green')
|
|
log(' ✓ Restricted host_permissions to memento-note.com', 'green')
|
|
log(' ✓ Kept sidebar_action + browser_specific_settings for Firefox', 'green')
|
|
log(' ✓ Added icon definitions', 'green')
|
|
|
|
await generateIcons()
|
|
await createXpiPackage()
|
|
|
|
log('')
|
|
log('✅ Build completed successfully!', 'green')
|
|
log('')
|
|
log('📦 Output files:', 'blue')
|
|
log(` • Package: ${path.join(extRoot, 'memento-web-clipper-firefox.xpi')}`, 'reset')
|
|
log(` • Dist dir: ${distDir}`, 'reset')
|
|
log('')
|
|
log('📝 Next steps:', 'blue')
|
|
log(' 1. Test: web-ext run --source-dir ' + distDir, 'reset')
|
|
log(' 2. Sign: web-ext sign --api-key=<KEY> --api-secret=<SECRET> --source-dir ' + distDir, 'reset')
|
|
log(' 3. Submit signed .xpi to https://addons.mozilla.org/developers/', 'reset')
|
|
log('')
|
|
} catch (error) {
|
|
log('')
|
|
log('❌ Build failed!', 'red')
|
|
log(` Error: ${error.message}`, 'red')
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
build()
|