// Bundles the content script and the modules it imports into one classic script. // // Content scripts cannot be ES modules, and loading modules with a dynamic import() makes Chrome log // "Unsafe attempt to load URL chrome-extension://..." on every YouTube page, which piles up in the // extension's error list. A single classic file avoids both. // // Supported source style: one-line `import ... from './x.js';`, and `export` only before // const / let / function / class. Top-level names must be unique across the bundled modules. import { readFileSync, writeFileSync } from 'node:fs'; import { basename, dirname, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; const IMPORT = /^import\s+(?:[^'"]*?\s+from\s+)?'([^']+)';?[ \t]*\r?\n/gm; const EXPORT_DECLARATION = /^export\s+(?=(?:async\s+function|function|const|let|class)\b)/gm; const UNSUPPORTED_EXPORT = /^export\s+(?!(?:async\s+function|function|const|let|class)\b)/m; const TOP_LEVEL_NAME = /^(?:async\s+)?(?:function\*?|const|let|class)\s+([A-Za-z_$][\w$]*)/gm; const HEADER = '// GENERATED by tools/build-content.js from src/content/main.js and the modules it imports.\n' + '// Edit those files and run `npm run build`; do not edit this file by hand.\n'; export function bundle(entry) { const order = []; const seen = new Set(); const visit = (file) => { if (seen.has(file)) return; seen.add(file); for (const [, specifier] of readFileSync(file, 'utf8').matchAll(IMPORT)) visit(resolve(dirname(file), specifier)); order.push(file); }; visit(resolve(entry)); const owner = new Map(); const parts = order.map((file) => { const source = readFileSync(file, 'utf8'); if (UNSUPPORTED_EXPORT.test(source)) { throw new Error(`${file}: only "export const/let/function/class" declarations are supported`); } const code = source.replace(IMPORT, '').replace(EXPORT_DECLARATION, '').trim(); for (const [, name] of code.matchAll(TOP_LEVEL_NAME)) { if (owner.has(name)) throw new Error(`Duplicate top-level name "${name}" in ${file} and ${owner.get(name)}`); owner.set(name, file); } return `// ---- ${basename(file)} ----\n${code}\n`; }); return `${HEADER}(() => {\n'use strict';\n\n${parts.join('\n')}})();\n`; } const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href; if (isMain) { const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); writeFileSync(resolve(root, 'src/content.js'), bundle(resolve(root, 'src/content/main.js'))); console.log('Wrote src/content.js'); }