11import { cpus } from 'os'
2- import path from 'path'
2+ import { parse , ParsedPath } from 'path'
33
44import { getDeployStore } from '@netlify/blobs'
55import { NetlifyPluginConstants } from '@netlify/build'
6- import { copy , move , remove } from 'fs-extra/esm'
6+ import { copy , move , mkdirp } from 'fs-extra/esm'
77import { globby } from 'globby'
88import pLimit from 'p-limit'
99
1010import { buildCacheValue } from './cache.js'
1111import { BUILD_DIR } from './constants.js'
12+ import { EnhancedNetlifyPluginConstants } from './types.js'
13+
14+ type ContentPath = ParsedPath & {
15+ relative : string
16+ absolute : string
17+ publish : string
18+ }
1219
1320/**
1421 * Move the Next.js build output from the publish dir to a temp dir
1522 */
16- export const stashBuildOutput = async ( { PUBLISH_DIR } : NetlifyPluginConstants ) => {
17- await move ( PUBLISH_DIR , `${ BUILD_DIR } /.next` , { overwrite : true } )
23+ export const stashBuildOutput = async ( { PUBLISH_DIR } : NetlifyPluginConstants ) : Promise < void > => {
24+ return move ( PUBLISH_DIR , `${ BUILD_DIR } /.next` , { overwrite : true } )
25+ }
1826
19- // remove prerendered content from the standalone build (it's also in the main build dir)
20- const prerenderedContent = await getPrerenderedContent ( `${ BUILD_DIR } /.next/standalone` , false )
21- await Promise . all (
22- prerenderedContent . map ( ( file : string ) => remove ( `${ BUILD_DIR } /.next/standalone/${ file } ` ) ) ,
23- ) . catch ( ( error ) => console . error ( error ) )
27+ /**
28+ * Glob the build output for static page content we can upload to the CDN
29+ */
30+ const getStaticContent = async ( cwd : string ) : Promise < ContentPath [ ] > => {
31+ const content = await globby ( [ `server/pages/**/*.+(html|json)` ] , {
32+ cwd,
33+ extglob : true ,
34+ } )
35+ return content
36+ . map ( ( path ) => parsePath ( path , cwd ) )
37+ . filter ( ( path ) => filterStatic ( path , content , 'keep' ) )
2438}
2539
2640/**
27- * Glob for prerendered content in the build output
41+ * Glob the build output for prerendered content we can upload to the blob store
2842 */
29- const getPrerenderedContent = async ( cwd : string , get = true ) : Promise < string [ ] > => {
30- // TODO: test this
31- return await globby (
32- get
33- ? [ `cache/fetch-cache/*` , `server/+(app|pages)/**/*.+(html|body)` ]
34- : [
35- `cache/fetch-cache/*` ,
36- `server/+(app|pages)/**/*.+(html|json|rsc|body|meta)` ,
37- `!server/**/*.js.nft.{html,json}` ,
38- ] ,
39- { cwd, extglob : true } ,
43+ const getPrerenderedContent = async ( cwd : string ) : Promise < ContentPath [ ] > => {
44+ const content = await globby (
45+ [ `cache/fetch-cache/*` , `server/+(app|pages)/**/*.+(html|body|json)` ] ,
46+ {
47+ cwd,
48+ extglob : true ,
49+ } ,
4050 )
51+ return content
52+ . map ( ( path ) => parsePath ( path , cwd ) )
53+ . filter ( ( path ) => filterStatic ( path , content , 'omit' ) )
4154}
4255
4356/**
44- * Upload prerendered content from the main build dir to the blob store
57+ * Glob the build output for JS content we can bundle with the server handler
58+ */
59+ export const getServerContent = async ( cwd : string ) : Promise < ContentPath [ ] > => {
60+ const content = await globby ( [ `**` , `!server/+(app|pages)/**/*.+(html|body|json|rsc|meta)` ] , {
61+ cwd,
62+ extglob : true ,
63+ } )
64+ return content . map ( ( path ) => parsePath ( path , cwd ) )
65+ }
66+
67+ /**
68+ * Upload prerendered content to the blob store and remove it from the bundle
4569 */
4670export const storePrerenderedContent = async ( {
4771 NETLIFY_API_TOKEN ,
4872 NETLIFY_API_HOST ,
4973 SITE_ID ,
50- } : NetlifyPluginConstants & { NETLIFY_API_TOKEN : string ; NETLIFY_API_HOST : string } ) => {
74+ } : EnhancedNetlifyPluginConstants ) : Promise < void [ ] > => {
5175 if ( ! process . env . DEPLOY_ID ) {
5276 // TODO: maybe change to logging
5377 throw new Error (
@@ -61,28 +85,52 @@ export const storePrerenderedContent = async ({
6185 token : NETLIFY_API_TOKEN ,
6286 apiURL : `https://${ NETLIFY_API_HOST } ` ,
6387 } )
64-
65- // todo: Check out setFiles within Blobs.js to see how to upload files to blob storage
6688 const limit = pLimit ( Math . max ( 2 , cpus ( ) . length ) )
6789
68- const prerenderedContent = await getPrerenderedContent ( `${ BUILD_DIR } /.next` )
90+ const content = await getPrerenderedContent ( `${ BUILD_DIR } /.next/standalone /.next` )
6991 return await Promise . all (
70- prerenderedContent . map ( async ( rawPath : string ) => {
71- // TODO: test this with files that have a double extension
72- const ext = path . extname ( rawPath )
73- const key = rawPath . replace ( ext , '' )
74- const value = await buildCacheValue ( key , ext )
92+ content . map ( ( path : ContentPath ) => {
93+ const { dir, name, ext } = path
94+ const key = `${ dir } /${ name } `
95+ const value = buildCacheValue ( key , ext )
7596 return limit ( ( ) => blob . setJSON ( key , value ) )
7697 } ) ,
7798 )
7899}
79100
80101/**
81- * Move static assets to the publish dir so they are uploaded to the CDN
102+ * Move static content to the publish dir so it is uploaded to the CDN
82103 */
83- export const publishStaticAssets = ( { PUBLISH_DIR } : NetlifyPluginConstants ) => {
84- return Promise . all ( [
104+ export const publishStaticContent = async ( {
105+ PUBLISH_DIR ,
106+ } : NetlifyPluginConstants ) : Promise < void [ ] > => {
107+ const content = await getStaticContent ( `${ BUILD_DIR } /.next/standalone/.next` )
108+ return await Promise . all ( [
109+ mkdirp ( PUBLISH_DIR ) ,
85110 copy ( 'public' , PUBLISH_DIR ) ,
86111 copy ( `${ BUILD_DIR } /.next/static/` , `${ PUBLISH_DIR } /_next/static` ) ,
112+ ...content . map ( ( path : ContentPath ) => copy ( path . absolute , `${ PUBLISH_DIR } /${ path . publish } ` ) ) ,
87113 ] )
88114}
115+
116+ /**
117+ * Keep or remove static content based on whether it has a corresponding JSON file
118+ */
119+ const filterStatic = (
120+ { dir, name, ext } : ContentPath ,
121+ content : string [ ] ,
122+ type : 'keep' | 'omit' ,
123+ ) : boolean =>
124+ type === 'keep'
125+ ? dir . startsWith ( 'server/pages' ) && ! content . includes ( `${ dir } /${ name } .json` )
126+ : ext !== '.json' &&
127+ ( ! dir . startsWith ( 'server/pages' ) || content . includes ( `${ dir } /${ name } .json` ) )
128+ /**
129+ * Parse a file path into an object with file path variants
130+ */
131+ const parsePath = ( path : string , cwd : string ) : ContentPath => ( {
132+ ...parse ( path ) ,
133+ relative : path ,
134+ absolute : `${ cwd } /${ path } ` ,
135+ publish : path . replace ( / ^ s e r v e r \/ ( a p p | p a g e s ) \/ / , '' ) ,
136+ } )
0 commit comments