33import type * as Vite from "vite" ;
44import { type BinaryLike , createHash } from "node:crypto" ;
55import * as path from "node:path" ;
6- import * as fs from "node:fs/promises " ;
6+ import * as fse from "fs-extra " ;
77import babel from "@babel/core" ;
88import { type ServerBuild } from "@remix-run/server-runtime" ;
99import {
@@ -182,8 +182,8 @@ function dedupe<T>(array: T[]): T[] {
182182}
183183
184184const 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 ) ;
187187} ;
188188
189189const getRouteModuleExports = async (
@@ -213,7 +213,7 @@ const getRouteModuleExports = async (
213213
214214 let [ id , code ] = await Promise . all ( [
215215 resolveId ( ) ,
216- fs . readFile ( routePath , "utf-8" ) ,
216+ fse . readFile ( routePath , "utf-8" ) ,
217217 // pluginContainer.transform(...) fails if we don't do this first:
218218 moduleGraph . ensureEntryFromUrl ( url , ssr ) ,
219219 ] ) ;
@@ -244,6 +244,8 @@ export type RemixVitePlugin = (
244244export const remixVitePlugin : RemixVitePlugin = ( options = { } ) => {
245245 let viteCommand : Vite . ResolvedConfig [ "command" ] ;
246246 let viteUserConfig : Vite . UserConfig ;
247+ let resolvedViteConfig : Vite . ResolvedConfig | undefined ;
248+
247249 let isViteV4 = getViteMajorVersion ( ) === 4 ;
248250
249251 let cssModulesManifest : Record < string , string > = { } ;
@@ -338,19 +340,23 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
338340 };` ;
339341 } ;
340342
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
345345 ? "manifest.json"
346346 : 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 ( ) ;
347356
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+ ) ;
354360
355361 let entry : Manifest [ "entry" ] = resolveBuildAssetPaths (
356362 pluginConfig ,
@@ -529,6 +535,8 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
529535 } ,
530536 }
531537 : {
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
532540 outDir : path . dirname ( pluginConfig . serverBuildPath ) ,
533541 rollupOptions : {
534542 ...viteUserConfig . build ?. rollupOptions ,
@@ -549,6 +557,8 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
549557 async configResolved ( viteConfig ) {
550558 await initEsModuleLexer ;
551559
560+ resolvedViteConfig = viteConfig ;
561+
552562 ssrBuildContext =
553563 viteConfig . build . ssr && viteCommand === "build"
554564 ? { isSsrBuild : true , getManifest : createBuildManifest }
@@ -737,6 +747,80 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
737747 }
738748 } ;
739749 } ,
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+ } ,
740824 async buildEnd ( ) {
741825 await viteChildCompiler ?. close ( ) ;
742826 } ,
@@ -897,8 +981,8 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
897981
898982 return [
899983 "const exports = {}" ,
900- await fs . readFile ( reactRefreshRuntimePath , "utf8" ) ,
901- await fs . readFile (
984+ await fse . readFile ( reactRefreshRuntimePath , "utf8" ) ,
985+ await fse . readFile (
902986 require . resolve ( "./static/refresh-utils.cjs" ) ,
903987 "utf8"
904988 ) ,
0 commit comments