alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
143 lines (129 loc) • 4.18 kB
text/typescript
import { $inject, Alepha } from "alepha";
import { FileSystemProvider } from "alepha/system";
import type { UserConfig } from "vite";
import { analyzer as viteAnalyzer } from "vite-bundle-analyzer";
import { ViteUtils } from "../services/ViteUtils.ts";
import { BuildTask, type BuildTaskContext } from "./BuildTask.ts";
/**
* Build client-side bundle with Vite.
*
* Compiles the browser/client code for production,
* including code splitting and minification.
* Analyze step stays in BuildCommand (ViteBuildProvider).
* This task wraps only the actual Vite client build call.
*/
export class BuildClientTask extends BuildTask {
protected readonly alepha = $inject(Alepha);
protected readonly fs = $inject(FileSystemProvider);
protected readonly viteUtils = $inject(ViteUtils);
async run(ctx: BuildTaskContext): Promise<void> {
if (ctx.flags?.prebuilt) {
return;
}
if (!ctx.hasClient) {
return;
}
const distDir = ctx.options.output?.dist ?? "dist";
const publicDir = ctx.options.output?.public ?? "public";
const stats = ctx.options.stats ?? false;
const isCI = this.alepha.isCI();
// Write index.html template for Vite to consume
const template = this.viteUtils.generateIndexHtml(ctx.entry, {
pwa: !!ctx.options.pwa,
});
await this.fs.mkdir(this.fs.join(ctx.root, "node_modules/.alepha"));
const indexHtmlPath = this.fs.join(
ctx.root,
"node_modules/.alepha/index.html",
);
await this.fs.writeFile(indexHtmlPath, template);
try {
await ctx.run({
name: "build client",
handler: async () => {
await this.buildClient({
dist: `${distDir}/${publicDir}`,
stats,
silent: !isCI,
});
},
});
} finally {
await this.fs.rm(indexHtmlPath);
}
}
protected async buildClient(opts: {
dist: string;
stats?: boolean | "json";
silent?: boolean;
}): Promise<void> {
const { build: viteBuild } = await this.viteUtils.importVite();
const plugins: any[] = [];
const viteReact = await this.viteUtils.importViteReact();
if (viteReact) plugins.push(viteReact());
plugins.push(this.viteUtils.createTsconfigPathsPlugin());
plugins.push(this.viteUtils.createSsrPreloadPlugin());
if (opts.stats) {
plugins.push(
viteAnalyzer({
analyzerMode: opts.stats === "json" ? "json" : "static",
}),
);
}
const logger = opts.silent
? this.viteUtils.createBufferedLogger()
: undefined;
const viteBuildClientConfig: UserConfig = {
mode: "production",
logLevel: opts.silent ? "silent" : undefined,
define: {
"process.env.NODE_ENV": '"production"',
},
resolve: {
dedupe: [
"react",
"react-dom",
"react/jsx-runtime",
"react/jsx-dev-runtime",
],
},
publicDir: "public",
build: {
outDir: opts.dist,
manifest: true,
chunkSizeWarningLimit: 1000,
rolldownOptions: {
input: "node_modules/.alepha/index.html",
output: {
entryFileNames: "entry.[hash].js",
chunkFileNames: "chunk.[hash].js",
assetFileNames: "asset.[hash][extname]",
},
},
},
customLogger: logger,
plugins,
};
try {
await viteBuild(viteBuildClientConfig);
await this.postBuildCleanUpForIndexHtml();
} catch (error) {
logger?.flush();
throw error;
}
}
/**
* Weird cleanup required because we changed input from "index.html" to "node_modules/.alepha/index.html".
*/
public async postBuildCleanUpForIndexHtml(dist = "dist/public") {
const manifestPath = `${dist}/.vite/manifest.json`;
let text = await this.fs.readTextFile(manifestPath);
text = text.replaceAll("node_modules/.alepha/index.html", "index.html");
await this.fs.writeFile(manifestPath, text);
await this.fs.cp(
`${dist}/node_modules/.alepha/index.html`,
`${dist}/index.html`,
);
await this.fs.rm(`${dist}/node_modules`, { recursive: true });
}
}