1
- import { getDeployStore } from '@netlify/blobs '
2
- import { NetlifyPluginConstants } from '@netlify/build '
3
- import { globby } from 'globby '
1
+ import { NetlifyPluginOptions } from '@netlify/build '
2
+ import glob from 'fast-glob '
3
+ import type { PrerenderManifest } from 'next/dist/build/index.js '
4
4
import { readFile } from 'node:fs/promises'
5
- import { join } from 'node:path'
5
+ import { basename , dirname , extname , resolve } from 'node:path'
6
+ import { join as joinPosix } from 'node:path/posix'
6
7
import { cpus } from 'os'
7
8
import pLimit from 'p-limit'
8
- import { parse , ParsedPath } from 'path '
9
- import { BUILD_DIR } from '../constants .js'
9
+ import { getBlobStore } from '../blob.js '
10
+ import { getPrerenderManifest } from '../config .js'
10
11
11
12
export type CacheEntry = {
12
13
key : string
@@ -45,107 +46,101 @@ type FetchCacheValue = {
45
46
}
46
47
47
48
// static prerendered pages content with JSON data
48
- const isPage = ( { dir , name , ext } : ParsedPath , paths : string [ ] ) => {
49
- return dir . startsWith ( 'server/pages' ) && ext === '.html' && paths . includes ( ` ${ dir } / ${ name } .json` )
49
+ const isPage = ( key : string , routes : string [ ] ) => {
50
+ return key . startsWith ( 'server/pages' ) && routes . includes ( key . replace ( / ^ s e r v e r \/ p a g e s / , '' ) )
50
51
}
51
52
// static prerendered app content with RSC data
52
- const isApp = ( { dir , ext } : ParsedPath ) => {
53
- return dir . startsWith ( 'server/app' ) && ext === '.html'
53
+ const isApp = ( path : string ) => {
54
+ return path . startsWith ( 'server/app' ) && extname ( path ) === '.html'
54
55
}
55
56
// static prerendered app route handler
56
- const isRoute = ( { dir , ext } : ParsedPath ) => {
57
- return dir . startsWith ( 'server/app' ) && ext === '.body'
57
+ const isRoute = ( path : string ) => {
58
+ return path . startsWith ( 'server/app' ) && extname ( path ) === '.body'
58
59
}
59
- // fetch cache data
60
- const isFetch = ( { dir } : ParsedPath ) => {
61
- return dir . startsWith ( 'cache/fetch-cache' )
60
+ // fetch cache data (excluding tags manifest)
61
+ const isFetch = ( path : string ) => {
62
+ return path . startsWith ( 'cache/fetch-cache' ) && extname ( path ) === ''
62
63
}
63
64
64
65
/**
65
66
* Transform content file paths into cache entries for the blob store
66
67
*/
67
- const buildPrerenderedContentEntries = async ( cwd : string ) : Promise < Promise < CacheEntry > [ ] > => {
68
- const paths = await globby (
69
- [ `cache/fetch-cache/*` , `server/+(app|pages)/**/*.+(html|body|json)` ] ,
70
- {
71
- cwd,
72
- extglob : true ,
73
- } ,
74
- )
68
+ const buildPrerenderedContentEntries = async (
69
+ src : string ,
70
+ routes : string [ ] ,
71
+ ) : Promise < Promise < CacheEntry > [ ] > => {
72
+ const paths = await glob ( [ `cache/fetch-cache/*` , `server/+(app|pages)/**/*.+(html|body)` ] , {
73
+ cwd : resolve ( src ) ,
74
+ extglob : true ,
75
+ } )
76
+
77
+ return paths . map ( async ( path : string ) : Promise < CacheEntry > => {
78
+ const key = joinPosix ( dirname ( path ) , basename ( path , extname ( path ) ) )
79
+ let value
80
+
81
+ if ( isPage ( key , routes ) ) {
82
+ value = {
83
+ kind : 'PAGE' ,
84
+ html : await readFile ( resolve ( src , `${ key } .html` ) , 'utf-8' ) ,
85
+ pageData : JSON . parse ( await readFile ( resolve ( src , `${ key } .json` ) , 'utf-8' ) ) ,
86
+ } satisfies PageCacheValue
87
+ }
88
+
89
+ if ( isApp ( path ) ) {
90
+ value = {
91
+ kind : 'PAGE' ,
92
+ html : await readFile ( resolve ( src , `${ key } .html` ) , 'utf-8' ) ,
93
+ pageData : await readFile ( resolve ( src , `${ key } .rsc` ) , 'utf-8' ) ,
94
+ ...JSON . parse ( await readFile ( resolve ( src , `${ key } .meta` ) , 'utf-8' ) ) ,
95
+ } satisfies PageCacheValue
96
+ }
75
97
76
- return paths
77
- . map ( parse )
78
- . filter ( ( path : ParsedPath ) => {
79
- return isPage ( path , paths ) || isApp ( path ) || isRoute ( path ) || isFetch ( path )
80
- } )
81
- . map ( async ( path : ParsedPath ) : Promise < CacheEntry > => {
82
- const { dir, name, ext } = path
83
- const key = join ( dir , name )
84
- let value
85
-
86
- if ( isPage ( path , paths ) ) {
87
- value = {
88
- kind : 'PAGE' ,
89
- html : await readFile ( `${ cwd } /${ key } .html` , 'utf-8' ) ,
90
- pageData : JSON . parse ( await readFile ( `${ cwd } /${ key } .json` , 'utf-8' ) ) ,
91
- } satisfies PageCacheValue
92
- }
93
-
94
- if ( isApp ( path ) ) {
95
- value = {
96
- kind : 'PAGE' ,
97
- html : await readFile ( `${ cwd } /${ key } .html` , 'utf-8' ) ,
98
- pageData : await readFile ( `${ cwd } /${ key } .rsc` , 'utf-8' ) ,
99
- ...JSON . parse ( await readFile ( `${ cwd } /${ key } .meta` , 'utf-8' ) ) ,
100
- } satisfies PageCacheValue
101
- }
102
-
103
- if ( isRoute ( path ) ) {
104
- value = {
105
- kind : 'ROUTE' ,
106
- body : await readFile ( `${ cwd } /${ key } .body` , 'utf-8' ) ,
107
- ...JSON . parse ( await readFile ( `${ cwd } /${ key } .meta` , 'utf-8' ) ) ,
108
- } satisfies RouteCacheValue
109
- }
110
-
111
- if ( isFetch ( path ) ) {
112
- value = {
113
- kind : 'FETCH' ,
114
- ...JSON . parse ( await readFile ( `${ cwd } /${ key } ` , 'utf-8' ) ) ,
115
- } satisfies FetchCacheValue
116
- }
117
-
118
- return {
119
- key,
120
- value : {
121
- lastModified : Date . now ( ) ,
122
- value,
123
- } ,
124
- }
125
- } )
98
+ if ( isRoute ( path ) ) {
99
+ value = {
100
+ kind : 'ROUTE' ,
101
+ body : await readFile ( resolve ( src , `${ key } .body` ) , 'utf-8' ) ,
102
+ ...JSON . parse ( await readFile ( resolve ( src , `${ key } .meta` ) , 'utf-8' ) ) ,
103
+ } satisfies RouteCacheValue
104
+ }
105
+
106
+ if ( isFetch ( path ) ) {
107
+ value = {
108
+ kind : 'FETCH' ,
109
+ ...JSON . parse ( await readFile ( resolve ( src , key ) , 'utf-8' ) ) ,
110
+ } satisfies FetchCacheValue
111
+ }
112
+
113
+ return {
114
+ key,
115
+ value : {
116
+ lastModified : Date . now ( ) ,
117
+ value,
118
+ } ,
119
+ }
120
+ } )
126
121
}
127
122
128
123
/**
129
124
* Upload prerendered content to the blob store and remove it from the bundle
130
125
*/
131
126
export const uploadPrerenderedContent = async ( {
132
- NETLIFY_API_TOKEN ,
133
- NETLIFY_API_HOST ,
134
- SITE_ID ,
135
- } : NetlifyPluginConstants ) => {
136
- // initialize the blob store
137
- const blob = getDeployStore ( {
138
- deployID : process . env . DEPLOY_ID ,
139
- siteID : SITE_ID ,
140
- token : NETLIFY_API_TOKEN ,
141
- apiURL : `https://${ NETLIFY_API_HOST } ` ,
142
- } )
127
+ constants : { PUBLISH_DIR , NETLIFY_API_TOKEN , NETLIFY_API_HOST , SITE_ID } ,
128
+ } : Pick < NetlifyPluginOptions , 'constants' > ) => {
143
129
// limit concurrent uploads to 2x the number of CPUs
144
130
const limit = pLimit ( Math . max ( 2 , cpus ( ) . length ) )
145
131
146
132
// read prerendered content and build JSON key/values for the blob store
133
+ let manifest : PrerenderManifest
134
+ let blob : ReturnType < typeof getBlobStore >
135
+ try {
136
+ manifest = await getPrerenderManifest ( { PUBLISH_DIR } )
137
+ blob = getBlobStore ( { NETLIFY_API_TOKEN , NETLIFY_API_HOST , SITE_ID } )
138
+ } catch ( error : any ) {
139
+ console . error ( `Unable to upload prerendered content: ${ error . message } ` )
140
+ return
141
+ }
147
142
const entries = await Promise . allSettled (
148
- await buildPrerenderedContentEntries ( join ( process . cwd ( ) , BUILD_DIR , '.next' ) ) ,
143
+ await buildPrerenderedContentEntries ( PUBLISH_DIR , Object . keys ( manifest . routes ) ) ,
149
144
)
150
145
entries . forEach ( ( result ) => {
151
146
if ( result . status === 'rejected' ) {
@@ -156,7 +151,7 @@ export const uploadPrerenderedContent = async ({
156
151
// upload JSON content data to the blob store
157
152
const uploads = await Promise . allSettled (
158
153
entries
159
- . filter ( ( entry ) => entry . status === 'fulfilled' )
154
+ . filter ( ( entry ) => entry . status === 'fulfilled' && entry . value . value . value !== undefined )
160
155
. map ( ( entry : PromiseSettledResult < CacheEntry > ) => {
161
156
const result = entry as PromiseFulfilledResult < CacheEntry >
162
157
const { key, value } = result . value
@@ -166,7 +161,7 @@ export const uploadPrerenderedContent = async ({
166
161
uploads . forEach ( ( upload , index ) => {
167
162
if ( upload . status === 'rejected' ) {
168
163
const result = entries [ index ] as PromiseFulfilledResult < CacheEntry >
169
- console . error ( `Unable to store ${ result . value . key } : ${ upload . reason . message } ` )
164
+ console . error ( `Unable to store ${ result . value ? .key } : ${ upload . reason . message } ` )
170
165
}
171
166
} )
172
167
}
0 commit comments