sitegen/framework/bundle.ts
2025-06-09 21:13:51 -07:00

166 lines
4.7 KiB
TypeScript

// This file implements client-side bundling, mostly wrapping esbuild.
const clientPlugins: esbuild.Plugin[] = [
// There are currently no plugins needed by 'paperclover.net'
];
export async function bundleClientJavaScript(
referencedScripts: string[],
extraPublicScripts: string[],
incr: Incremental,
dev: boolean = false,
) {
const entryPoints = [
...new Set([
...referencedScripts,
...extraPublicScripts,
]),
];
if (entryPoints.length === 0) return;
const invalidFiles = entryPoints
.filter((file) => !file.match(/\.client\.[tj]sx?/));
if (invalidFiles.length > 0) {
const cwd = process.cwd();
throw new Error(
"All client-side scripts should be named like '.client.ts'. Exceptions: " +
invalidFiles.map((x) => path.join(cwd, x)).join(","),
);
}
const bundle = await esbuild.build({
bundle: true,
chunkNames: "/js/c.[hash]",
entryNames: "/js/[name]",
assetNames: "/asset/[hash]",
entryPoints,
format: "esm",
minify: !dev,
outdir: "/out!",
plugins: clientPlugins,
splitting: true,
write: false,
metafile: true,
});
if (bundle.errors.length || bundle.warnings.length) {
throw new AggregateError(
bundle.errors.concat(bundle.warnings),
"JS bundle failed",
);
}
const publicScriptRoutes = extraPublicScripts.map((file) =>
path.basename(file).replace(/\.client\.[tj]sx?/, "")
);
const { metafile } = bundle;
console.log(metafile);
const promises: Promise<void>[] = [];
// TODO: add a shared build hash to entrypoints, derived from all the chunk hashes.
for (const file of bundle.outputFiles) {
const { text } = file;
let route = file.path.replace(/^.*!/, "").replaceAll("\\", "/");
const { inputs } = UNWRAP(metafile.outputs["out!" + route]);
const sources = Object.keys(inputs);
// Register non-chunks as script entries.
const chunk = route.startsWith("/js/c.");
if (!chunk) {
route = route.replace(".client.js", ".js");
incr.put({
sources,
type: "script",
key: route.slice("/js/".length, -".js".length),
value: text,
});
}
// Register chunks and public scripts as assets.
if (chunk || publicScriptRoutes.includes(route)) {
promises.push(incr.putAsset({
sources,
key: route,
body: text,
}));
}
}
await Promise.all(promises);
}
type ServerPlatform = "node" | "passthru";
export async function bundleServerJavaScript(
/** Has 'export default app;' */
backendEntryPoint: string,
/** Views for dynamic loading */
viewEntryPoints: FileItem[],
platform: ServerPlatform = "node",
) {
const scriptMagic = "CLOVER_CLIENT_SCRIPTS_DEFINITION";
const viewSource = [
...viewEntryPoints.map((view, i) =>
`import * as view${i} from ${JSON.stringify(view.file)}`
),
`const scripts = ${scriptMagic}[-1]`,
"export const views = {",
...viewEntryPoints.flatMap((view, i) => [
` ${JSON.stringify(view.id)}: {`,
` component: view${i}.default,`,
` meta: view${i}.meta,`,
` layout: view${i}.layout?.default,`,
` theme: view${i}.layout?.theme ?? view${i}.theme,`,
` scripts: ${scriptMagic}[${i}]`,
` },`,
]),
"}",
].join("\n");
const serverPlugins: esbuild.Plugin[] = [
virtualFiles({
"$views": viewSource,
}),
banFiles([
"hot.ts",
"incremental.ts",
"bundle.ts",
"generate.ts",
"css.ts",
].map((subPath) => path.join(hot.projectRoot, "framework/" + subPath))),
{
name: "marko",
setup(b) {
b.onLoad({ filter: /\.marko$/ }, async ({ path }) => {
const src = await fs.readFile(path);
const result = await marko.compile(src, path, {
output: "html",
});
return {
loader: "ts",
contents: result.code,
};
});
},
},
];
const bundle = await esbuild.build({
bundle: true,
chunkNames: "/js/c.[hash]",
entryNames: "/js/[name]",
assetNames: "/asset/[hash]",
entryPoints: [backendEntryPoint],
platform: "node",
format: "esm",
minify: false,
// outdir: "/out!",
outdir: ".clover/wah",
plugins: serverPlugins,
splitting: true,
write: true,
external: ["@babel/preset-typescript"],
});
console.log(bundle);
throw new Error("wahhh");
}
import * as esbuild from "esbuild";
import * as path from "node:path";
import process from "node:process";
import * as hot from "./hot.ts";
import { banFiles, virtualFiles } from "./esbuild-support.ts";
import { Incremental } from "./incremental.ts";
import type { FileItem } from "#sitegen";
import * as marko from "@marko/compiler";
import * as fs from "./lib/fs.ts";