fix: bundle the content script to stop 'Unsafe attempt to load URL' errors

Dynamic import() of extension modules from a content script makes Chrome log
'Unsafe attempt to load URL chrome-extension://...' on YouTube pages, which fills
the extension's error list even though the modules load (HTTP 200). The content
script is now one classic file built by 'npm run build'; web_accessible_resources
is no longer needed and is removed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
IgorVolochay
2026-10-25 12:25:06 +03:00
co-authored by Claude Sonnet 5.5
parent 38dabd9fa2
commit b137776381
9 changed files with 461 additions and 61 deletions
+55
View File
@@ -0,0 +1,55 @@
// 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');
}