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>
56 lines
2.5 KiB
JavaScript
56 lines
2.5 KiB
JavaScript
// 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');
|
|
}
|