1
1
const fs = require ( 'fs-extra' )
2
2
const path = require ( 'path' ) ;
3
3
const glob = require ( 'glob' ) ;
4
- const ncc = require ( '@vercel/ncc' ) ;
5
4
6
- const { configureCloudinary, updateHtmlImagesToCloudinary } = require ( './lib/cloudinary' ) ;
7
- const { PREFIX , PUBLIC_ASSET_PATH } = require ( './data/cloudinary' ) ;
5
+ const { configureCloudinary, updateHtmlImagesToCloudinary, getCloudinaryUrl } = require ( './lib/cloudinary' ) ;
6
+ const { PUBLIC_ASSET_PATH } = require ( './data/cloudinary' ) ;
8
7
9
- const CLOUDINARY_MEDIA_FUNCTIONS = [
8
+ const CLOUDINARY_ASSET_DIRECTORIES = [
10
9
{
11
10
name : 'images' ,
12
11
inputKey : 'imagesPath' ,
@@ -20,109 +19,186 @@ const CLOUDINARY_MEDIA_FUNCTIONS = [
20
19
*/
21
20
22
21
module . exports = {
22
+ async onPreBuild ( { netlifyConfig, constants, inputs } ) {
23
+ const host = process . env . DEPLOY_PRIME_URL || process . env . NETLIFY_HOST ;
24
+
25
+ if ( ! host ) {
26
+ console . warn ( 'Cannot determine Netlify host, not proceeding with on-page image replacement.' ) ;
27
+ console . log ( 'Note: The Netlify CLI does not currently support the ability to determine the host locally, try deploying on Netlify.' ) ;
28
+ return ;
29
+ }
30
+
31
+ const { PUBLISH_DIR } = constants ;
23
32
24
- async onBuild ( { netlifyConfig, constants, inputs } ) {
25
- const { FUNCTIONS_SRC , INTERNAL_FUNCTIONS_SRC } = constants ;
26
33
const {
27
34
deliveryType,
28
- folder = process . env . SITE_NAME ,
29
35
uploadPreset,
36
+ folder = process . env . SITE_NAME
30
37
} = inputs ;
31
38
39
+ // If we're using the fetch API, we don't need to worry about uploading any
40
+ // of the media as it will all be publicly accessible, so we can skip this step
41
+
42
+ if ( deliveryType === 'fetch' ) {
43
+ console . log ( 'Skipping: Delivery type set to fetch.' )
44
+ return ;
45
+ }
46
+
32
47
const cloudName = process . env . CLOUDINARY_CLOUD_NAME || inputs . cloudName ;
33
48
49
+ const apiKey = process . env . CLOUDINARY_API_KEY ;
50
+ const apiSecret = process . env . CLOUDINARY_API_SECRET ;
51
+
34
52
if ( ! cloudName ) {
35
53
throw new Error ( 'A Cloudinary Cloud Name is required. Please set cloudName input or use the environment variable CLOUDINARY_CLOUD_NAME' ) ;
36
54
}
37
55
38
- const functionsPath = INTERNAL_FUNCTIONS_SRC || FUNCTIONS_SRC ;
56
+ configureCloudinary ( {
57
+ cloudName,
58
+ apiKey,
59
+ apiSecret
60
+ } ) ;
39
61
40
- // Copy all of the templates over including the functions to deploy
62
+ const imagesDirectory = glob . sync ( `${ PUBLISH_DIR } /images/**/*` ) ;
63
+ const imagesFiles = imagesDirectory . filter ( file => ! ! path . extname ( file ) ) ;
41
64
42
- const functionTemplatesPath = path . join ( __dirname , 'templates/functions' ) ;
43
- const functionTemplates = await fs . readdir ( functionTemplatesPath ) ;
65
+ const images = await Promise . all ( imagesFiles . map ( async image => {
66
+ const publishPath = image . replace ( PUBLISH_DIR , '' ) ;
67
+ const publishUrl = `${ host } ${ publishPath } ` ;
68
+ const cldAssetPath = `/${ path . join ( PUBLIC_ASSET_PATH , publishPath ) } ` ;
69
+ const cldAssetUrl = `${ host } ${ cldAssetPath } ` ;
44
70
45
- if ( ! Array . isArray ( functionTemplates ) || functionTemplates . length == 0 ) {
46
- throw new Error ( `Failed to generate templates: can not find function templates in ${ functionTemplatesPath } ` ) ;
47
- }
71
+ const cloudinary = await getCloudinaryUrl ( {
72
+ deliveryType,
73
+ folder,
74
+ path : publishPath ,
75
+ localDir : PUBLISH_DIR ,
76
+ uploadPreset,
77
+ remoteHost : host ,
78
+ } ) ;
48
79
49
- try {
50
- await Promise . all ( functionTemplates . map ( async templateFileName => {
51
- const bundle = await ncc ( path . join ( functionTemplatesPath , templateFileName ) ) ;
52
- const { name , base } = path . parse ( templateFileName ) ;
53
- const templateDirectory = path . join ( functionsPath , name ) ;
54
- const filePath = path . join ( templateDirectory , base ) ;
80
+ return {
81
+ publishPath ,
82
+ publishUrl ,
83
+ ... cloudinary
84
+ }
85
+ } ) ) ;
55
86
56
- await fs . ensureDir ( templateDirectory ) ;
57
- await fs . writeFile ( filePath , bundle . code , 'utf8' ) ;
58
- } ) ) ;
59
- } catch ( e ) {
60
- throw new Error ( `Failed to generate templates: ${ e } ` ) ;
87
+ netlifyConfig . build . environment . CLOUDINARY_ASSETS = {
88
+ images
61
89
}
90
+ } ,
62
91
63
- // Configure reference parameters for Cloudinary delivery to attach to redirect
92
+ async onBuild ( { netlifyConfig, constants, inputs } ) {
93
+ const host = process . env . DEPLOY_PRIME_URL || process . env . NETLIFY_HOST ;
64
94
65
- const params = {
66
- uploadPreset,
67
- deliveryType,
68
- cloudName,
69
- folder
95
+ if ( ! host ) {
96
+ console . warn ( 'Cannot determine Netlify host, not proceeding with on-page image replacement.' ) ;
97
+ console . log ( 'Note: The Netlify CLI does not currently support the ability to determine the host locally, try deploying on Netlify.' ) ;
98
+ return ;
70
99
}
71
100
72
- const paramsString = Object . keys ( params )
73
- . filter ( key => typeof params [ key ] !== 'undefined' )
74
- . map ( key => `${ key } =${ encodeURIComponent ( params [ key ] ) } ` )
75
- . join ( '&' ) ;
101
+ const { PUBLISH_DIR } = constants ;
76
102
77
- // Redirect any requests that hits /[media type]/* to a serverless function
103
+ const {
104
+ deliveryType,
105
+ uploadPreset,
106
+ folder = process . env . SITE_NAME
107
+ } = inputs ;
78
108
79
- CLOUDINARY_MEDIA_FUNCTIONS . forEach ( ( { name : mediaName , inputKey, path : defaultPath } ) => {
80
- const mediaPath = inputs [ inputKey ] || defaultPath ;
81
- const mediaPathSplat = path . join ( mediaPath , ':splat' ) ;
82
- const functionName = `${ PREFIX } _${ mediaName } ` ;
109
+ const cloudName = process . env . CLOUDINARY_CLOUD_NAME || inputs . cloudName ;
110
+ const apiKey = process . env . CLOUDINARY_API_KEY ;
111
+ const apiSecret = process . env . CLOUDINARY_API_SECRET ;
83
112
84
- netlifyConfig . redirects . unshift ( {
85
- from : path . join ( PUBLIC_ASSET_PATH , mediaPath , '*' ) ,
86
- to : mediaPathSplat ,
87
- status : 200 ,
88
- force : true
89
- } ) ;
113
+ if ( ! cloudName ) {
114
+ throw new Error ( 'A Cloudinary Cloud Name is required. Please set cloudName input or use the environment variable CLOUDINARY_CLOUD_NAME' ) ;
115
+ }
90
116
91
- netlifyConfig . redirects . unshift ( {
92
- from : path . join ( mediaPath , '*' ) ,
93
- to : `/.netlify/functions/${ functionName } ?path=${ mediaPathSplat } &${ paramsString } ` ,
94
- status : 302 ,
95
- force : true ,
96
- } ) ;
117
+ configureCloudinary ( {
118
+ cloudName,
119
+ apiKey,
120
+ apiSecret
97
121
} ) ;
98
122
123
+ // If the delivery type is set to upload, we need to be able to map individual assets based on their public ID,
124
+ // which would require a dynamic middle solution, but that adds more hops than we want, so add a new redirect
125
+ // for each asset uploaded
126
+
127
+ if ( deliveryType === 'upload' ) {
128
+ await Promise . all ( Object . keys ( netlifyConfig . build . environment . CLOUDINARY_ASSETS ) . flatMap ( mediaType => {
129
+ return netlifyConfig . build . environment . CLOUDINARY_ASSETS [ mediaType ] . map ( async asset => {
130
+ const { publishPath, cloudinaryUrl } = asset ;
131
+
132
+ netlifyConfig . redirects . unshift ( {
133
+ from : `${ publishPath } *` ,
134
+ to : cloudinaryUrl ,
135
+ status : 302 ,
136
+ force : true
137
+ } ) ;
138
+ } )
139
+ } ) ) ;
140
+ }
141
+
142
+ // If the delivery type is fetch, we're able to use the public URL and pass it right along "as is", so
143
+ // we can create generic redirects. The tricky thing is to avoid a redirect loop, we modify the
144
+ // path, so that we can safely allow Cloudinary to fetch the media remotely
145
+
146
+ if ( deliveryType === 'fetch' ) {
147
+ await Promise . all ( CLOUDINARY_ASSET_DIRECTORIES . map ( async ( { name : mediaName , inputKey, path : defaultPath } ) => {
148
+ const mediaPath = inputs [ inputKey ] || defaultPath ;
149
+ const cldAssetPath = `/${ path . join ( PUBLIC_ASSET_PATH , mediaPath ) } ` ;
150
+ const cldAssetUrl = `${ host } /${ cldAssetPath } ` ;
151
+
152
+ const { cloudinaryUrl : assetRedirectUrl } = await getCloudinaryUrl ( {
153
+ deliveryType : 'fetch' ,
154
+ folder,
155
+ path : `${ cldAssetUrl } /:splat` ,
156
+ uploadPreset
157
+ } ) ;
158
+
159
+ netlifyConfig . redirects . unshift ( {
160
+ from : `${ cldAssetPath } /*` ,
161
+ to : `${ mediaPath } /:splat` ,
162
+ status : 200 ,
163
+ force : true
164
+ } ) ;
165
+
166
+ netlifyConfig . redirects . unshift ( {
167
+ from : `${ mediaPath } /*` ,
168
+ to : assetRedirectUrl ,
169
+ status : 302 ,
170
+ force : true
171
+ } ) ;
172
+ } ) ) ;
173
+ }
174
+
99
175
} ,
100
176
101
177
// Post build looks through all of the output HTML and rewrites any src attributes to use a cloudinary URL
102
178
// This only solves on-page references until any JS refreshes the DOM
103
179
104
- async onPostBuild ( { constants, inputs } ) {
105
- const { PUBLISH_DIR } = constants ;
106
- const {
107
- deliveryType,
108
- uploadPreset,
109
- folder = process . env . SITE_NAME
110
- } = inputs ;
111
-
112
- const host = process . env . DEPLOY_PRIME_URL ;
180
+ async onPostBuild ( { netlifyConfig, constants, inputs } ) {
181
+ const host = process . env . DEPLOY_PRIME_URL || process . env . NETLIFY_HOST ;
113
182
114
183
if ( ! host ) {
115
184
console . warn ( 'Cannot determine Netlify host, not proceeding with on-page image replacement.' ) ;
116
185
console . log ( 'Note: The Netlify CLI does not currently support the ability to determine the host locally, try deploying on Netlify.' ) ;
117
186
return ;
118
187
}
119
188
189
+ const { PUBLISH_DIR } = constants ;
190
+ const {
191
+ deliveryType,
192
+ uploadPreset,
193
+ folder = process . env . SITE_NAME
194
+ } = inputs ;
195
+
120
196
const cloudName = process . env . CLOUDINARY_CLOUD_NAME || inputs . cloudName ;
121
197
const apiKey = process . env . CLOUDINARY_API_KEY ;
122
198
const apiSecret = process . env . CLOUDINARY_API_SECRET ;
123
199
124
200
if ( ! cloudName ) {
125
- throw new Error ( 'Cloudinary Cloud Name required. Please use an environment variable CLOUDINARY_CLOUD_NAME' ) ;
201
+ throw new Error ( 'A Cloudinary Cloud Name is required. Please set cloudName input or use the environment variable CLOUDINARY_CLOUD_NAME' ) ;
126
202
}
127
203
128
204
configureCloudinary ( {
@@ -139,6 +215,7 @@ module.exports = {
139
215
const sourceHtml = await fs . readFile ( page , 'utf-8' ) ;
140
216
141
217
const { html, errors } = await updateHtmlImagesToCloudinary ( sourceHtml , {
218
+ assets : netlifyConfig . build . environment . CLOUDINARY_ASSETS ,
142
219
deliveryType,
143
220
uploadPreset,
144
221
folder,
0 commit comments