1
1
import type { NetlifyPluginOptions } from '@netlify/build'
2
2
import glob from 'fast-glob'
3
+ import { existsSync } from 'node:fs'
3
4
import { 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'
6
6
import { getPrerenderManifest } from '../config.js'
7
7
import { BLOB_DIR } from '../constants.js'
8
8
9
9
export type CacheEntry = {
10
- key : string
11
- value : CacheEntryValue
12
- }
13
-
14
- export type CacheEntryValue = {
15
10
lastModified : number
16
- value : PageCacheValue | RouteCacheValue | FetchCacheValue
11
+ value : CacheValue
17
12
}
18
13
14
+ type CacheValue = PageCacheValue | RouteCacheValue | FetchCacheValue
15
+
19
16
export type PageCacheValue = {
20
17
kind : 'PAGE'
21
18
html : string
@@ -42,110 +39,121 @@ type FetchCacheValue = {
42
39
}
43
40
}
44
41
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
+ )
60
49
}
61
50
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 )
85
52
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
+ } )
94
58
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
+ } )
102
65
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
+ } )
109
71
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
+ } )
119
76
120
77
/**
121
- * Upload prerendered content to the blob store and remove it from the bundle
78
+ * Upload prerendered content to the blob store
122
79
*/
123
- export const uploadPrerenderedContent = async ( {
80
+ export const copyPrerenderedContent = async ( {
124
81
constants : { PUBLISH_DIR } ,
125
- utils,
82
+ utils : {
83
+ build : { failBuild } ,
84
+ } ,
126
85
} : Pick < NetlifyPluginOptions , 'constants' | 'utils' > ) => {
127
86
try {
128
87
// read prerendered content and build JSON key/values for the blob store
129
88
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'
133
91
134
- // movce JSON content to the blob store directory for upload
135
92
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
+ } ) ,
143
118
)
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
+ }
144
128
} catch ( error ) {
145
- utils . build . failBuild (
129
+ failBuild (
146
130
'Failed assembling prerendered content for upload' ,
147
131
error instanceof Error ? { error } : { } ,
148
132
)
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 } : { } )
150
158
}
151
159
}
0 commit comments