feat: add build logic

This commit is contained in:
Alexander Navarro 2025-02-18 16:19:08 -03:00
parent e23a81c78e
commit 8ef4d8408c
11 changed files with 541 additions and 1 deletions

205
src/index.ts Normal file
View file

@ -0,0 +1,205 @@
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!");
},
};