11import { mkdir , rename , rm , writeFile } from "node:fs/promises" ;
22import path from "node:path" ;
3- import { build as viteBuild } from "vite" ;
3+ import { createBuilder } from "vite" ;
44import { ZuploEnv } from "../app/env.js" ;
55import {
66 findOutputPathOfServerConfig ,
@@ -17,26 +17,26 @@ import { prerender } from "./prerender/prerender.js";
1717const DIST_DIR = "dist" ;
1818
1919export async function runBuild ( options : { dir : string } ) {
20- // Shouldn't run in parallel because it's potentially racy
21- const viteClientConfig = await getViteConfig ( options . dir , {
20+ const viteConfig = await getViteConfig ( options . dir , {
2221 mode : "production" ,
2322 command : "build" ,
2423 } ) ;
25- const viteServerConfig = await getViteConfig ( options . dir , {
26- mode : "production" ,
27- command : "build" ,
28- isSsrBuild : true ,
29- } ) ;
3024
31- // Don't run in parallel because it might overwrite itself
32- const clientResult = await viteBuild ( viteClientConfig ) ;
33- const serverResult = await viteBuild ( {
34- ...viteServerConfig ,
35- logLevel : "silent" ,
36- } ) ;
37- if ( Array . isArray ( clientResult ) ) {
38- throw new Error ( "Build failed" ) ;
39- }
25+ const builder = await createBuilder ( viteConfig ) ;
26+
27+ invariant ( builder . environments . client , "Client environment is missing" ) ;
28+ invariant ( builder . environments . ssr , "SSR environment is missing" ) ;
29+
30+ const [ clientResult , serverResult ] = await Promise . all ( [
31+ builder . build ( builder . environments . client ) ,
32+ builder . build ( builder . environments . ssr ) ,
33+ ] ) ;
34+
35+ invariant (
36+ clientResult && ! Array . isArray ( clientResult ) ,
37+ "Client build failed to produce valid output" ,
38+ ) ;
39+ invariant ( serverResult , "SSR build failed to produce valid output" ) ;
4040
4141 const { config } = await loadZudokuConfig (
4242 { mode : "production" , command : "build" } ,
@@ -45,92 +45,94 @@ export async function runBuild(options: { dir: string }) {
4545
4646 const issuer = await getIssuer ( config ) ;
4747
48- if ( "output" in clientResult ) {
49- const [ jsEntry , cssEntries ] = [
50- clientResult . output . find ( ( o ) => "isEntry" in o && o . isEntry ) ?. fileName ,
51- clientResult . output
52- . filter ( ( o ) => o . fileName . endsWith ( ".css" ) )
53- . map ( ( o ) => o . fileName ) ,
54- ] ;
48+ const base = viteConfig . base ?? "/" ;
49+ const clientOutDir = viteConfig . environments ?. client ?. build ?. outDir ;
50+ const serverOutDir = viteConfig . environments ?. ssr ?. build ?. outDir ;
5551
56- if ( ! jsEntry || cssEntries . length === 0 ) {
57- throw new Error ( "Build failed. No js or css assets found" ) ;
58- }
52+ invariant ( clientOutDir , "Client build outDir is missing" ) ;
53+ invariant ( serverOutDir , "Server build outDir is missing" ) ;
5954
60- const html = getBuildHtml ( {
61- jsEntry : joinUrl ( viteClientConfig . base , jsEntry ) ,
62- cssEntries : cssEntries . map ( ( css ) => joinUrl ( viteClientConfig . base , css ) ) ,
63- dir : config . site ?. dir ,
64- } ) ;
55+ if ( ! ( "output" in clientResult ) ) {
56+ throw new Error ( "Client build output is missing" ) ;
57+ }
6558
66- const serverConfigFilename = findOutputPathOfServerConfig ( serverResult ) ;
59+ const [ jsEntry , cssEntries ] = [
60+ clientResult . output . find ( ( o ) => "isEntry" in o && o . isEntry ) ?. fileName ,
61+ clientResult . output
62+ . filter ( ( o ) => o . fileName . endsWith ( ".css" ) )
63+ . map ( ( o ) => o . fileName ) ,
64+ ] ;
6765
68- invariant ( viteClientConfig . build ?. outDir , "Client build outDir is missing" ) ;
69- invariant ( viteServerConfig . build ?. outDir , "Server build outDir is missing" ) ;
66+ if ( ! jsEntry || cssEntries . length === 0 ) {
67+ throw new Error ( "Build failed. No js or css assets found" ) ;
68+ }
7069
71- try {
72- const results = await prerender ( {
73- html,
74- dir : options . dir ,
75- basePath : config . basePath ,
76- serverConfigFilename,
77- writeRedirects : process . env . VERCEL === undefined ,
78- } ) ;
70+ const html = getBuildHtml ( {
71+ jsEntry : joinUrl ( base , jsEntry ) ,
72+ cssEntries : cssEntries . map ( ( css ) => joinUrl ( base , css ) ) ,
73+ dir : config . site ?. dir ,
74+ } ) ;
7975
80- const indexHtml = path . join ( viteClientConfig . build . outDir , "index.html" ) ;
76+ const serverConfigFilename = findOutputPathOfServerConfig ( serverResult ) ;
8177
82- if ( ! results . find ( ( r ) => r . outputPath === indexHtml ) ) {
83- await writeFile ( indexHtml , html , "utf-8" ) ;
84- }
78+ try {
79+ const results = await prerender ( {
80+ html,
81+ dir : options . dir ,
82+ basePath : config . basePath ,
83+ serverConfigFilename,
84+ writeRedirects : process . env . VERCEL === undefined ,
85+ } ) ;
86+
87+ const indexHtml = path . join ( clientOutDir , "index.html" ) ;
88+
89+ if ( ! results . find ( ( r ) => r . outputPath === indexHtml ) ) {
90+ await writeFile ( indexHtml , html , "utf-8" ) ;
91+ }
8592
86- // find 400.html, 404.html, 500.html
87- const statusPages = results . flatMap ( ( r ) =>
88- / 4 0 0 | 4 0 4 | 5 0 0 \. h t m l $ / . test ( r . outputPath ) ? r . outputPath : [ ] ,
93+ // find 400.html, 404.html, 500.html
94+ const statusPages = results . flatMap ( ( r ) =>
95+ / 4 0 0 | 4 0 4 | 5 0 0 \. h t m l $ / . test ( r . outputPath ) ? r . outputPath : [ ] ,
96+ ) ;
97+
98+ // move status pages to root path (i.e. without base path)
99+ for ( const statusPage of statusPages ) {
100+ await rename (
101+ statusPage ,
102+ path . join ( options . dir , DIST_DIR , path . basename ( statusPage ) ) ,
89103 ) ;
104+ }
90105
91- // move status pages to root path (i.e. without base path)
92- for ( const statusPage of statusPages ) {
93- await rename (
94- statusPage ,
95- path . join ( options . dir , DIST_DIR , path . basename ( statusPage ) ) ,
96- ) ;
97- }
98-
99- // Delete the server build output directory because we don't need it anymore
100- await rm ( viteServerConfig . build . outDir , { recursive : true , force : true } ) ;
101-
102- if ( process . env . VERCEL ) {
103- await mkdir ( path . join ( options . dir , ".vercel/output/static" ) , {
104- recursive : true ,
105- } ) ;
106- await rename (
107- path . join ( options . dir , DIST_DIR ) ,
108- path . join ( options . dir , ".vercel/output/static" ) ,
109- ) ;
110- }
111-
112- // Write the build output file
113- await writeOutput ( options . dir , {
114- config,
115- redirects : results . flatMap ( ( r ) => r . redirect ?? [ ] ) ,
116- } ) ;
106+ // Delete the server build output directory because we don't need it anymore
107+ await rm ( serverOutDir , { recursive : true , force : true } ) ;
117108
118- if ( ZuploEnv . isZuplo && issuer ) {
119- await writeFile (
120- path . join ( options . dir , DIST_DIR , ".output/zuplo.json" ) ,
121- JSON . stringify ( { issuer } , null , 2 ) ,
122- "utf-8" ,
123- ) ;
124- }
125- } catch ( e ) {
126- // dynamic imports in prerender swallow the stack trace, so we log it here
127- // biome-ignore lint/suspicious/noConsole: Logging allowed here
128- console . error ( e ) ;
129- throw e ;
109+ if ( process . env . VERCEL ) {
110+ await mkdir ( path . join ( options . dir , ".vercel/output/static" ) , {
111+ recursive : true ,
112+ } ) ;
113+ await rename (
114+ path . join ( options . dir , DIST_DIR ) ,
115+ path . join ( options . dir , ".vercel/output/static" ) ,
116+ ) ;
130117 }
131118
132- return ;
133- }
119+ // Write the build output file
120+ await writeOutput ( options . dir , {
121+ config,
122+ redirects : results . flatMap ( ( r ) => r . redirect ?? [ ] ) ,
123+ } ) ;
134124
135- throw new Error ( "Build failed" ) ;
125+ if ( ZuploEnv . isZuplo && issuer ) {
126+ await writeFile (
127+ path . join ( options . dir , DIST_DIR , ".output/zuplo.json" ) ,
128+ JSON . stringify ( { issuer } , null , 2 ) ,
129+ "utf-8" ,
130+ ) ;
131+ }
132+ } catch ( e ) {
133+ // dynamic imports in prerender swallow the stack trace, so we log it here
134+ // biome-ignore lint/suspicious/noConsole: Logging allowed here
135+ console . error ( e ) ;
136+ throw e ;
137+ }
136138}
0 commit comments