import type { BuildConfig, BunPlugin, PluginBuilder } from "bun"; import type { FileImporter } from "sass"; import { type Config, type Entrypoint, EntrypointType, LogType } from "./types"; import { basename, join, normalize } from "node:path"; import { fileURLToPath } from "node:url"; import { rm } from "node:fs/promises"; import slug from "slug"; // This is used to prevent creating folders with external packages names slug.extend({ "/": "-" }); const nodeModuleImporter: FileImporter<"async"> = { findFileUrl(url) { if (url.startsWith("@")) { return new URL(import.meta.resolve(url)); } return null; }, }; const sassPlugin: BunPlugin = { name: "Sass Loader", async setup(build: PluginBuilder) { const sass = await import("sass"); build.onLoad({ filter: /\.scss$/ }, async ({ path }) => { const result = await sass.compileAsync(path, { importers: [nodeModuleImporter], }); return { loader: "css", contents: result.css, }; }); }, }; function log(logtype: LogType, msg: string) { const reset = "\x1b[0m"; let color = Bun.color("white", "ansi"); switch (logtype) { case LogType.Success: color = Bun.color("green", "ansi"); break; case LogType.Error: color = Bun.color("#f24444", "ansi"); break; default: break; } if (!color) { color = reset; } console.log(color + msg + reset); } // Packages needs to exist in the package.json file for this to work function resolvePackage(pkg: string): Entrypoint { const path = normalize(fileURLToPath(import.meta.resolve(pkg))); const file = Bun.file(import.meta.resolve(pkg)); const mimetype = file.type.split(";").at(0); let type: EntrypointType; switch (mimetype) { case "text/x-scss": type = EntrypointType.Sass; break; case "text/javascript": type = EntrypointType.Js; break; default: throw new Error(`No loader found for type ${mimetype} at path ${path}`); } return { path, type, }; } export default { log, build: async (config: Config, entrypoints: Entrypoint[]) => { // Resolve external packages const external_cache: string[] = []; const resolved_entrypoints = entrypoints.map((item) => { if (item.type !== EntrypointType.Package) { return item; } external_cache.push(item.path); return resolvePackage(item.path); }); const baseConfig = { minify: config.production, outdir: `${config.outdir}/js`, packages: "external", root: config.root, splitting: config.production, }; // Apply build config per type const loaders = { assetLoader: { entrypoints: resolved_entrypoints .filter((entry) => entry.type === EntrypointType.Asset) .map((entry) => entry.path), outdir: `${config.outdir}/asset`, }, stylesLoader: { ...baseConfig, entrypoints: resolved_entrypoints .filter((entry) => [EntrypointType.Css, EntrypointType.Sass].includes(entry.type), ) .map((entry) => entry.path), outdir: `${config.outdir}/css`, plugins: [sassPlugin], }, jsLoader: { ...baseConfig, entrypoints: resolved_entrypoints .filter((entry) => entry.type === EntrypointType.Js) .map((entry) => entry.path), target: "browser", }, }; // Transform into a list to later use Promise.all const assets = Object.values(loaders).filter( (item) => item.entrypoints.length !== 0, ); log(LogType.Info, "Building assets..."); const out = await Promise.all( assets.map(async (item) => { const result = await Bun.build(item as BuildConfig); if (!result.success) { throw new AggregateError(result.logs, "Build failed"); } // Normalize external packages folder structure for (const out of result.outputs) { if (!out.path.includes("node_modules")) { continue; } let package_name = external_cache.find((pkg) => out.path.includes(pkg), ); if (!package_name) { throw new Error( `Could not normilize path for external package: ${out.path}`, ); } package_name = slug(package_name); if (!package_name) { throw new Error( `Could not normilize path for external package: ${out.path}`, ); } const new_path = join( config.outdir, "pkgs", package_name, basename(out.path), ); await Bun.write(new_path, out); } return result; }), ); if (out.some((item) => !item.success)) { throw new Error(`Some entrypoint failed to build: ${out}`); } await rm(join(config.outdir, "node_modules"), { recursive: true, force: true, }); log(LogType.Success, "Complete!"); }, };