3
3
import type * as Vite from "vite" ;
4
4
import { type BinaryLike , createHash } from "node:crypto" ;
5
5
import * as path from "node:path" ;
6
- import * as fs from "node:fs/promises " ;
6
+ import * as fse from "fs-extra " ;
7
7
import babel from "@babel/core" ;
8
8
import { type ServerBuild } from "@remix-run/server-runtime" ;
9
9
import {
@@ -182,8 +182,8 @@ function dedupe<T>(array: T[]): T[] {
182
182
}
183
183
184
184
const writeFileSafe = async ( file : string , contents : string ) : Promise < void > => {
185
- await fs . mkdir ( path . dirname ( file ) , { recursive : true } ) ;
186
- await fs . writeFile ( file , contents ) ;
185
+ await fse . ensureDir ( path . dirname ( file ) ) ;
186
+ await fse . writeFile ( file , contents ) ;
187
187
} ;
188
188
189
189
const getRouteModuleExports = async (
@@ -213,7 +213,7 @@ const getRouteModuleExports = async (
213
213
214
214
let [ id , code ] = await Promise . all ( [
215
215
resolveId ( ) ,
216
- fs . readFile ( routePath , "utf-8" ) ,
216
+ fse . readFile ( routePath , "utf-8" ) ,
217
217
// pluginContainer.transform(...) fails if we don't do this first:
218
218
moduleGraph . ensureEntryFromUrl ( url , ssr ) ,
219
219
] ) ;
@@ -244,6 +244,8 @@ export type RemixVitePlugin = (
244
244
export const remixVitePlugin : RemixVitePlugin = ( options = { } ) => {
245
245
let viteCommand : Vite . ResolvedConfig [ "command" ] ;
246
246
let viteUserConfig : Vite . UserConfig ;
247
+ let resolvedViteConfig : Vite . ResolvedConfig | undefined ;
248
+
247
249
let isViteV4 = getViteMajorVersion ( ) === 4 ;
248
250
249
251
let cssModulesManifest : Record < string , string > = { } ;
@@ -338,19 +340,23 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
338
340
};` ;
339
341
} ;
340
342
341
- let createBuildManifest = async ( ) : Promise < Manifest > => {
342
- let pluginConfig = await resolvePluginConfig ( ) ;
343
-
344
- let viteManifestPath = isViteV4
343
+ let loadViteManifest = async ( directory : string ) => {
344
+ let manifestPath = isViteV4
345
345
? "manifest.json"
346
346
: path . join ( ".vite" , "manifest.json" ) ;
347
+ let manifestContents = await fse . readFile (
348
+ path . resolve ( directory , manifestPath ) ,
349
+ "utf-8"
350
+ ) ;
351
+ return JSON . parse ( manifestContents ) as Vite . Manifest ;
352
+ } ;
353
+
354
+ let createBuildManifest = async ( ) : Promise < Manifest > => {
355
+ let pluginConfig = await resolvePluginConfig ( ) ;
347
356
348
- let viteManifest = JSON . parse (
349
- await fs . readFile (
350
- path . resolve ( pluginConfig . assetsBuildDirectory , viteManifestPath ) ,
351
- "utf-8"
352
- )
353
- ) as Vite . Manifest ;
357
+ let viteManifest = await loadViteManifest (
358
+ pluginConfig . assetsBuildDirectory
359
+ ) ;
354
360
355
361
let entry : Manifest [ "entry" ] = resolveBuildAssetPaths (
356
362
pluginConfig ,
@@ -529,6 +535,8 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
529
535
} ,
530
536
}
531
537
: {
538
+ ssrEmitAssets : true , // We move SSR-only assets to client assets and clean the rest
539
+ manifest : true , // We need the manifest to detect SSR-only assets
532
540
outDir : path . dirname ( pluginConfig . serverBuildPath ) ,
533
541
rollupOptions : {
534
542
...viteUserConfig . build ?. rollupOptions ,
@@ -549,6 +557,8 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
549
557
async configResolved ( viteConfig ) {
550
558
await initEsModuleLexer ;
551
559
560
+ resolvedViteConfig = viteConfig ;
561
+
552
562
ssrBuildContext =
553
563
viteConfig . build . ssr && viteCommand === "build"
554
564
? { isSsrBuild : true , getManifest : createBuildManifest }
@@ -737,6 +747,80 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
737
747
}
738
748
} ;
739
749
} ,
750
+ writeBundle : {
751
+ // After the SSR build is finished, we inspect the Vite manifest for
752
+ // the SSR build and move all server assets to client assets directory
753
+ async handler ( ) {
754
+ if ( ! ssrBuildContext . isSsrBuild ) {
755
+ return ;
756
+ }
757
+
758
+ invariant (
759
+ cachedPluginConfig ,
760
+ "Expected plugin config to be cached when writeBundle hook is called"
761
+ ) ;
762
+
763
+ invariant (
764
+ resolvedViteConfig ,
765
+ "Expected resolvedViteConfig to exist when writeBundle hook is called"
766
+ ) ;
767
+
768
+ let { assetsBuildDirectory, serverBuildPath, rootDirectory } =
769
+ cachedPluginConfig ;
770
+ let serverBuildDir = path . dirname ( serverBuildPath ) ;
771
+
772
+ let ssrViteManifest = await loadViteManifest ( serverBuildDir ) ;
773
+ let clientViteManifest = await loadViteManifest ( assetsBuildDirectory ) ;
774
+
775
+ let clientAssetPaths = new Set (
776
+ Object . values ( clientViteManifest ) . flatMap (
777
+ ( chunk ) => chunk . assets ?? [ ]
778
+ )
779
+ ) ;
780
+
781
+ let ssrOnlyAssetPaths = new Set (
782
+ Object . values ( ssrViteManifest )
783
+ . flatMap ( ( chunk ) => chunk . assets ?? [ ] )
784
+ // Only move assets that aren't in the client build
785
+ . filter ( ( asset ) => ! clientAssetPaths . has ( asset ) )
786
+ ) ;
787
+
788
+ let movedAssetPaths = await Promise . all (
789
+ Array . from ( ssrOnlyAssetPaths ) . map ( async ( ssrAssetPath ) => {
790
+ let src = path . join ( serverBuildDir , ssrAssetPath ) ;
791
+ let dest = path . join ( assetsBuildDirectory , ssrAssetPath ) ;
792
+ await fse . move ( src , dest ) ;
793
+ return dest ;
794
+ } )
795
+ ) ;
796
+
797
+ let logger = resolvedViteConfig . logger ;
798
+
799
+ if ( movedAssetPaths . length ) {
800
+ logger . info (
801
+ [
802
+ "" ,
803
+ `${ colors . green ( "✓" ) } ${ movedAssetPaths . length } asset${
804
+ movedAssetPaths . length > 1 ? "s" : ""
805
+ } moved from Remix server build to client assets.`,
806
+ ...movedAssetPaths . map ( ( movedAssetPath ) =>
807
+ colors . dim ( path . relative ( rootDirectory , movedAssetPath ) )
808
+ ) ,
809
+ "" ,
810
+ ] . join ( "\n" )
811
+ ) ;
812
+ }
813
+
814
+ let ssrAssetsDir = path . join (
815
+ resolvedViteConfig . build . outDir ,
816
+ resolvedViteConfig . build . assetsDir
817
+ ) ;
818
+
819
+ if ( fse . existsSync ( ssrAssetsDir ) ) {
820
+ await fse . remove ( ssrAssetsDir ) ;
821
+ }
822
+ } ,
823
+ } ,
740
824
async buildEnd ( ) {
741
825
await viteChildCompiler ?. close ( ) ;
742
826
} ,
@@ -897,8 +981,8 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
897
981
898
982
return [
899
983
"const exports = {}" ,
900
- await fs . readFile ( reactRefreshRuntimePath , "utf8" ) ,
901
- await fs . readFile (
984
+ await fse . readFile ( reactRefreshRuntimePath , "utf8" ) ,
985
+ await fse . readFile (
902
986
require . resolve ( "./static/refresh-utils.cjs" ) ,
903
987
"utf8"
904
988
) ,
0 commit comments