import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const extensionDir = path.resolve(__dirname, '..'); const repoRoot = path.resolve(extensionDir, '..'); function parseArgs(argv) { const args = { outDir: path.join(repoRoot, 'extension-package') }; for (let i = 0; i < argv.length; i++) { const arg = argv[i]; if (arg === '--out' && argv[i + 1]) { const outDir = argv[++i]; args.outDir = path.isAbsolute(outDir) ? outDir : path.resolve(process.cwd(), outDir); } } return args; } async function exists(targetPath) { try { await fs.access(targetPath); return true; } catch { return false; } } function isLocalAsset(ref) { return typeof ref === 'string' && ref.length > 0 && !ref.startsWith('http://') && !ref.startsWith('https://') && !ref.startsWith('//') && !ref.startsWith('chrome://') && !ref.startsWith('chrome-extension://') && !ref.startsWith('data:') && !ref.startsWith('#'); } function addLocalAsset(files, ref) { if (isLocalAsset(ref)) files.add(ref); } function collectManifestEntrypoints(manifest) { const files = new Set(['manifest.json']); addLocalAsset(files, manifest.background?.service_worker); addLocalAsset(files, manifest.action?.default_popup); addLocalAsset(files, manifest.options_page); addLocalAsset(files, manifest.devtools_page); addLocalAsset(files, manifest.side_panel?.default_path); for (const ref of Object.values(manifest.icons ?? {})) addLocalAsset(files, ref); for (const ref of Object.values(manifest.action?.default_icon ?? {})) addLocalAsset(files, ref); for (const contentScript of manifest.content_scripts ?? []) { for (const jsFile of contentScript.js ?? []) addLocalAsset(files, jsFile); for (const cssFile of contentScript.css ?? []) addLocalAsset(files, cssFile); } for (const page of manifest.sandbox?.pages ?? []) addLocalAsset(files, page); for (const overridePage of Object.values(manifest.chrome_url_overrides ?? {})) addLocalAsset(files, overridePage); for (const entry of manifest.web_accessible_resources ?? []) { for (const resource of entry.resources ?? []) addLocalAsset(files, resource); } if (manifest.default_locale) files.add('_locales'); return [...files]; } async function collectHtmlDependencies(relativeHtmlPath, files, visited) { if (visited.has(relativeHtmlPath)) return; visited.add(relativeHtmlPath); const htmlPath = path.join(extensionDir, relativeHtmlPath); const html = await fs.readFile(htmlPath, 'utf8'); const attrRe = /\b(?:src|href)=["']([^"'#?]+(?:\?[^"']*)?)["']/gi; for (const match of html.matchAll(attrRe)) { const rawRef = match[1]; const cleanRef = rawRef.split('?')[0]; if (!isLocalAsset(cleanRef)) continue; const resolvedRelativePath = cleanRef.startsWith('/') ? cleanRef.slice(1) : path.posix.normalize(path.posix.join(path.posix.dirname(relativeHtmlPath), cleanRef)); addLocalAsset(files, resolvedRelativePath); if (resolvedRelativePath.endsWith('.html')) { await collectHtmlDependencies(resolvedRelativePath, files, visited); } } } async function collectManifestAssets(manifest) { const files = new Set(collectManifestEntrypoints(manifest)); const htmlPages = []; if (manifest.action?.default_popup) { htmlPages.push(manifest.action.default_popup); } if (manifest.options_page) htmlPages.push(manifest.options_page); if (manifest.devtools_page) htmlPages.push(manifest.devtools_page); if (manifest.side_panel?.default_path) htmlPages.push(manifest.side_panel.default_path); for (const page of manifest.sandbox?.pages ?? []) htmlPages.push(page); for (const overridePage of Object.values(manifest.chrome_url_overrides ?? {})) htmlPages.push(overridePage); const visited = new Set(); for (const htmlPage of htmlPages) { if (isLocalAsset(htmlPage)) { await collectHtmlDependencies(htmlPage, files, visited); } } return [...files]; } async function copyEntry(relativePath, outDir) { const fromPath = path.join(extensionDir, relativePath); const toPath = path.join(outDir, relativePath); const stats = await fs.stat(fromPath); if (stats.isDirectory()) { await fs.cp(fromPath, toPath, { recursive: true }); return; } await fs.mkdir(path.dirname(toPath), { recursive: true }); await fs.copyFile(fromPath, toPath); } async function findMissingEntries(baseDir, entries) { const missingEntries = []; for (const relativePath of entries) { const absolutePath = path.join(baseDir, relativePath); if (!(await exists(absolutePath))) { missingEntries.push(relativePath); } } return missingEntries; } async function main() { const { outDir } = parseArgs(process.argv.slice(2)); const manifestPath = path.join(extensionDir, 'manifest.json'); const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8')); const requiredEntries = await collectManifestAssets(manifest); const missingEntries = await findMissingEntries(extensionDir, requiredEntries); if (missingEntries.length > 0) { console.error('Missing files referenced by the extension package:'); for (const missingEntry of missingEntries) console.error(`- ${missingEntry}`); process.exit(1); } await fs.rm(outDir, { recursive: true, force: true }); await fs.mkdir(outDir, { recursive: true }); for (const relativePath of requiredEntries) { await copyEntry(relativePath, outDir); } // Guard against regressions where manifest entry files (e.g. action.default_popup) // are accidentally omitted from the packaged directory. const packagedEntrypoints = collectManifestEntrypoints(manifest); const missingPackagedEntrypoints = await findMissingEntries(outDir, packagedEntrypoints); if (missingPackagedEntrypoints.length > 0) { console.error('Packaged extension is missing files referenced by manifest.json:'); for (const missingEntry of missingPackagedEntrypoints) console.error(`- ${missingEntry}`); process.exit(1); } console.log(`Extension package prepared at ${path.relative(repoRoot, outDir) || outDir}`); } await main();