to-userscript
Version:
Converts simple browser extensions to userscripts
217 lines (197 loc) • 7.24 kB
JavaScript
// 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);
});