11import type { NetlifyPluginOptions } from '@netlify/build'
22import glob from 'fast-glob'
3+ import { existsSync } from 'node:fs'
34import { mkdir , readFile , writeFile } from 'node:fs/promises'
4- import { basename , dirname , extname , resolve } from 'node:path'
5- import { join as joinPosix } from 'node:path/posix'
5+ import { dirname , resolve } from 'node:path'
66import { getPrerenderManifest } from '../config.js'
77import { BLOB_DIR } from '../constants.js'
88
99export type CacheEntry = {
10- key : string
11- value : CacheEntryValue
12- }
13-
14- export type CacheEntryValue = {
1510 lastModified : number
16- value : PageCacheValue | RouteCacheValue | FetchCacheValue
11+ value : CacheValue
1712}
1813
14+ type CacheValue = PageCacheValue | RouteCacheValue | FetchCacheValue
15+
1916export type PageCacheValue = {
2017 kind : 'PAGE'
2118 html : string
@@ -42,110 +39,121 @@ type FetchCacheValue = {
4239 }
4340}
4441
45- // static prerendered pages content with JSON data
46- const isPage = ( key : string , routes : string [ ] ) => {
47- return key . startsWith ( 'server/pages' ) && routes . includes ( key . replace ( / ^ s e r v e r \/ p a g e s / , '' ) )
48- }
49- // static prerendered app content with RSC data
50- const isApp = ( path : string ) => {
51- return path . startsWith ( 'server/app' ) && extname ( path ) === '.html'
52- }
53- // static prerendered app route handler
54- const isRoute = ( path : string ) => {
55- return path . startsWith ( 'server/app' ) && extname ( path ) === '.body'
56- }
57- // fetch cache data (excluding tags manifest)
58- const isFetch = ( path : string ) => {
59- return path . startsWith ( 'cache/fetch-cache' ) && extname ( path ) === ''
42+ const writeCacheEntry = async ( key : string , value : CacheValue ) => {
43+ await mkdir ( dirname ( resolve ( BLOB_DIR , key ) ) , { recursive : true } )
44+ await writeFile (
45+ resolve ( BLOB_DIR , key ) ,
46+ JSON . stringify ( { lastModified : Date . now ( ) , value } satisfies CacheEntry ) ,
47+ 'utf-8' ,
48+ )
6049}
6150
62- /**
63- * Transform content file paths into cache entries for the blob store
64- */
65- const buildPrerenderedContentEntries = async (
66- src : string ,
67- routes : string [ ] ,
68- ) : Promise < Promise < CacheEntry > [ ] > => {
69- const paths = await glob ( [ `cache/fetch-cache/*` , `server/+(app|pages)/**/*.+(html|body)` ] , {
70- cwd : resolve ( src ) ,
71- extglob : true ,
72- } )
73-
74- return paths . map ( async ( path : string ) : Promise < CacheEntry > => {
75- const key = joinPosix ( dirname ( path ) , basename ( path , extname ( path ) ) )
76- let value
77-
78- if ( isPage ( key , routes ) ) {
79- value = {
80- kind : 'PAGE' ,
81- html : await readFile ( resolve ( src , `${ key } .html` ) , 'utf-8' ) ,
82- pageData : JSON . parse ( await readFile ( resolve ( src , `${ key } .json` ) , 'utf-8' ) ) ,
83- } satisfies PageCacheValue
84- }
51+ const urlPathToFilePath = ( path : string ) => ( path === '/' ? '/index' : path )
8552
86- if ( isApp ( path ) ) {
87- value = {
88- kind : 'PAGE' ,
89- html : await readFile ( resolve ( src , `${ key } .html` ) , 'utf-8' ) ,
90- pageData : await readFile ( resolve ( src , `${ key } .rsc` ) , 'utf-8' ) ,
91- ...JSON . parse ( await readFile ( resolve ( src , `${ key } .meta` ) , 'utf-8' ) ) ,
92- } satisfies PageCacheValue
93- }
53+ const buildPagesCacheValue = async ( path : string ) : Promise < PageCacheValue > => ( {
54+ kind : 'PAGE' ,
55+ html : await readFile ( resolve ( `${ path } .html` ) , 'utf-8' ) ,
56+ pageData : JSON . parse ( await readFile ( resolve ( `${ path } .json` ) , 'utf-8' ) ) ,
57+ } )
9458
95- if ( isRoute ( path ) ) {
96- value = {
97- kind : 'ROUTE' ,
98- body : await readFile ( resolve ( src , `${ key } .body` ) , 'utf-8' ) ,
99- ...JSON . parse ( await readFile ( resolve ( src , `${ key } .meta` ) , 'utf-8' ) ) ,
100- } satisfies RouteCacheValue
101- }
59+ const buildAppCacheValue = async ( path : string ) : Promise < PageCacheValue > => ( {
60+ kind : 'PAGE' ,
61+ html : await readFile ( resolve ( `${ path } .html` ) , 'utf-8' ) ,
62+ pageData : await readFile ( resolve ( `${ path } .rsc` ) , 'utf-8' ) ,
63+ ...JSON . parse ( await readFile ( resolve ( `${ path } .meta` ) , 'utf-8' ) ) ,
64+ } )
10265
103- if ( isFetch ( path ) ) {
104- value = {
105- kind : 'FETCH' ,
106- ...JSON . parse ( await readFile ( resolve ( src , key ) , 'utf-8' ) ) ,
107- } satisfies FetchCacheValue
108- }
66+ const buildRouteCacheValue = async ( path : string ) : Promise < RouteCacheValue > => ( {
67+ kind : 'ROUTE' ,
68+ body : await readFile ( resolve ( `${ path } .body` ) , 'utf-8' ) ,
69+ ...JSON . parse ( await readFile ( resolve ( `${ path } .meta` ) , 'utf-8' ) ) ,
70+ } )
10971
110- return {
111- key,
112- value : {
113- lastModified : Date . now ( ) ,
114- value,
115- } ,
116- }
117- } )
118- }
72+ const buildFetchCacheValue = async ( path : string ) : Promise < FetchCacheValue > => ( {
73+ kind : 'FETCH' ,
74+ ...JSON . parse ( await readFile ( resolve ( path ) , 'utf-8' ) ) ,
75+ } )
11976
12077/**
121- * Upload prerendered content to the blob store and remove it from the bundle
78+ * Upload prerendered content to the blob store
12279 */
123- export const uploadPrerenderedContent = async ( {
80+ export const copyPrerenderedContent = async ( {
12481 constants : { PUBLISH_DIR } ,
125- utils,
82+ utils : {
83+ build : { failBuild } ,
84+ } ,
12685} : Pick < NetlifyPluginOptions , 'constants' | 'utils' > ) => {
12786 try {
12887 // read prerendered content and build JSON key/values for the blob store
12988 const manifest = await getPrerenderManifest ( { PUBLISH_DIR } )
130- const entries = await Promise . all (
131- await buildPrerenderedContentEntries ( PUBLISH_DIR , Object . keys ( manifest . routes ) ) ,
132- )
89+ const routes = Object . entries ( manifest . routes )
90+ const notFoundRoute = 'server/app/_not-found'
13391
134- // movce JSON content to the blob store directory for upload
13592 await Promise . all (
136- entries
137- . filter ( ( entry ) => entry . value . value !== undefined )
138- . map ( async ( entry ) => {
139- const dest = resolve ( BLOB_DIR , entry . key )
140- await mkdir ( dirname ( dest ) , { recursive : true } )
141- await writeFile ( resolve ( BLOB_DIR , entry . key ) , JSON . stringify ( entry . value ) , 'utf-8' )
142- } ) ,
93+ routes . map ( async ( [ path , route ] ) => {
94+ let key , value
95+
96+ switch ( true ) {
97+ case route . dataRoute ?. endsWith ( '.json' ) :
98+ key = `server/pages/${ urlPathToFilePath ( path ) } `
99+ value = await buildPagesCacheValue ( resolve ( PUBLISH_DIR , key ) )
100+ break
101+
102+ case route . dataRoute ?. endsWith ( '.rsc' ) :
103+ key = `server/app/${ urlPathToFilePath ( path ) } `
104+ value = await buildAppCacheValue ( resolve ( PUBLISH_DIR , key ) )
105+ break
106+
107+ case route . dataRoute === null :
108+ key = `server/app/${ urlPathToFilePath ( path ) } `
109+ value = await buildRouteCacheValue ( resolve ( PUBLISH_DIR , key ) )
110+ break
111+
112+ default :
113+ throw new Error ( `Unrecognized prerendered content: ${ path } ` )
114+ }
115+
116+ await writeCacheEntry ( key , value )
117+ } ) ,
143118 )
119+
120+ // app router 404 pages are not in the prerender manifest
121+ // so we need to check for them manually
122+ if ( existsSync ( resolve ( PUBLISH_DIR , `${ notFoundRoute } .html` ) ) ) {
123+ await writeCacheEntry (
124+ notFoundRoute ,
125+ await buildAppCacheValue ( resolve ( PUBLISH_DIR , notFoundRoute ) ) ,
126+ )
127+ }
144128 } catch ( error ) {
145- utils . build . failBuild (
129+ failBuild (
146130 'Failed assembling prerendered content for upload' ,
147131 error instanceof Error ? { error } : { } ,
148132 )
149- throw error
133+ }
134+ }
135+
136+ /**
137+ * Upload fetch content to the blob store
138+ */
139+ export const copyFetchContent = async ( {
140+ constants : { PUBLISH_DIR } ,
141+ utils : {
142+ build : { failBuild } ,
143+ } ,
144+ } : Pick < NetlifyPluginOptions , 'constants' | 'utils' > ) => {
145+ try {
146+ const paths = await glob ( [ `cache/fetch-cache/!(*.*)` ] , {
147+ cwd : resolve ( PUBLISH_DIR ) ,
148+ extglob : true ,
149+ } )
150+
151+ await Promise . all (
152+ paths . map ( async ( key ) => {
153+ await writeCacheEntry ( key , await buildFetchCacheValue ( resolve ( PUBLISH_DIR , key ) ) )
154+ } ) ,
155+ )
156+ } catch ( error ) {
157+ failBuild ( 'Failed assembling fetch content for upload' , error instanceof Error ? { error } : { } )
150158 }
151159}
0 commit comments