UNPKG

to-userscript

Version:

Converts simple browser extensions to userscripts

217 lines (197 loc) 7.24 kB
// bun.bundler.js import { file, write, build } from "bun"; import { join, dirname, resolve, relative } from "path"; import { rm, mkdir } from "fs/promises"; import * as vite from "vite"; import { viteSingleFile } from "vite-plugin-singlefile"; /** * Finds the path to the options or popup page from the manifest. * @param {object} manifest - The parsed manifest object. * @param {string} pageType - 'options' or 'popup'. * @returns {string|null} The relative path to the page or null. */ function getPagePath(manifest, pageType) { if (pageType === "options") { return manifest.options_ui?.page || manifest.options_page || null; } if (pageType === "popup") { return ( manifest.action?.default_popup || manifest.browser_action?.default_popup || manifest.page_action?.default_popup || null ); } return null; } /** * Main function to drive the bundling process. * @param {string} extPath - The path to the extension directory. */ async function bundleExtension(extPath) { const inputDir = resolve(extPath); const outputDir = resolve(join(process.cwd(), "bun-dist")); const tempDir = resolve(join(outputDir, "temp")); console.log(`Input directory: ${inputDir}`); console.log(`Output directory: ${outputDir}`); // Clean and create directories await rm(outputDir, { recursive: true, force: true }); await mkdir(outputDir, { recursive: true }); await mkdir(tempDir, { recursive: true }); // 1. Parse manifest const manifestPath = join(inputDir, "manifest.json"); const manifestFile = file(manifestPath); if (!(await manifestFile.exists())) { throw new Error(`manifest.json not found in ${inputDir}`); } const manifest = await manifestFile.json(); console.log(`Bundling extension: ${manifest.name} v${manifest.version}`); // --- BUNDLE BACKGROUND SCRIPTS --- const backgroundScripts = ( manifest.background?.scripts || (manifest.background?.service_worker ? [manifest.background.service_worker] : []) ).map((p) => join(inputDir, p)); if (backgroundScripts.length > 0) { console.log("Bundling background scripts..."); const result = await build({ entrypoints: backgroundScripts, outdir: outputDir, naming: "background.js", // minify: true, }); if (!result.success) { console.error("Background script bundling failed:", result.logs); } else { console.log(" -> background.js"); } } else { await write( join(outputDir, "background.js"), "// No background scripts in extension.", ); } // --- BUNDLE CONTENT SCRIPTS --- const contentScriptConfigs = manifest.content_scripts || []; if (contentScriptConfigs.length > 0) { console.log("Bundling content scripts..."); const scriptsByRunAt = { "document-start": [], "document-end": [], "document-idle": [], }; const stylesByRunAt = { "document-start": [], "document-end": [], "document-idle": [], }; for (const config of contentScriptConfigs) { const runAt = (config.run_at || "document-idle").replace("_", "-"); if (config.js) scriptsByRunAt[runAt].push(...config.js); if (config.css) stylesByRunAt[runAt].push(...config.css); } const tempEntryPath = join(tempDir, "_content.entry.js"); const tempEntryFileContent = ` // This is a temporary entry file for Bun bundler const scripts = { 'document-start': [${scriptsByRunAt["document-start"].map((p) => `() => import('${relative(tempDir, join(inputDir, p))}')`).join(",\n")}], 'document-end': [${scriptsByRunAt["document-end"].map((p) => `() => import('${relative(tempDir, join(inputDir, p))}')`).join(",\n")}], 'document-idle': [${scriptsByRunAt["document-idle"].map((p) => `() => import('${relative(tempDir, join(inputDir, p))}')`).join(",\n")}] }; // Bun will bundle imported CSS and inject it automatically. ${Object.values(stylesByRunAt) .flat() .map((p) => `import '${relative(tempDir, join(inputDir, p))}';`) .join("\n")} async function main() { // --- Document Start --- await Promise.all(scripts['document-start'].map(f => f())); // --- Document End --- if (document.readyState === 'loading') { await new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve, { once: true })); } await Promise.all(scripts['document-end'].map(f => f())); // --- Document Idle --- if (typeof requestIdleCallback === 'function') { await new Promise(resolve => requestIdleCallback(resolve)); } else { await new Promise(resolve => setTimeout(resolve, 50)); } await Promise.all(scripts['document-idle'].map(f => f())); } main().catch(console.error); `; await write(tempEntryPath, tempEntryFileContent); const result = await build({ entrypoints: [tempEntryPath], outdir: outputDir, naming: "content.js", // minify: true, }); if (!result.success) { console.error("Content script bundling failed:", result.logs); } else { console.log(" -> content.js"); } } else { await write( join(outputDir, "content.js"), "// No content scripts in extension.", ); } await rm(tempDir, { recursive: true, force: true }); await mkdir(tempDir, { recursive: true }); for (const pageType of ["options", "popup"]) { const pagePath = getPagePath(manifest, pageType); if (pagePath) { console.log(`Bundling ${pageType} page: ${pagePath}`); const fullPagePath = join(inputDir, pagePath); // const buildResult = await build({ // entrypoints: [fullPagePath], // outdir: tempDir, // naming: `${pageType}.html`, // // minify: true, // }); // if (!buildResult.success) { // console.error( // ` ! Bundling assets for ${pageType} page failed:`, // buildResult.logs, // ); // continue; // } vite.build({ root: tempDir, plugins: [viteSingleFile()], build: { rollupOptions: { input: fullPagePath, output: { entryFileNames: `${pageType}.html`, dir: outputDir, }, }, }, }); console.log(` -> ${pageType}.html (relies on ext.)`); } else { await write( join(outputDir, `${pageType}.html`), `<!-- No ${pageType} page in extension. -->`, ); } } // Cleanup and assemble // await rm(tempDir, { recursive: true, force: true }); console.log("\nProcess complete."); } // --- Script Execution --- const args = process.argv.slice(2); if (args.length !== 1) { console.error("Usage: bun run bun.bundler.js <path-to-extension-directory>"); process.exit(1); } bundleExtension(args[0]).catch((err) => { console.error("An error occurred:", err); process.exit(1); });