netlify-cli 17.1.0 → 17.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,115 @@
1
+ import express from 'express'
2
+ import { createIPX, ipxFSStorage, ipxHttpStorage, createIPXNodeServer } from 'ipx'
3
+
4
+ import { log, NETLIFYDEVERR } from '../../utils/command-helpers.mjs'
5
+
6
+ export const IMAGE_URL_PATTERN = '/.netlify/images'
7
+
8
+ export const parseAllDomains = function (config) {
9
+ const domains = config?.images?.remote_images
10
+ if (!domains) {
11
+ return { errors: [], remoteDomains: [] }
12
+ }
13
+
14
+ const remoteDomains = []
15
+ const errors = []
16
+
17
+ for (const patternString of domains) {
18
+ try {
19
+ const url = new URL(patternString)
20
+ if (url.hostname) {
21
+ remoteDomains.push(url.hostname)
22
+ } else {
23
+ errors.push(`The URL '${patternString}' does not have a valid hostname.`)
24
+ }
25
+ } catch (error) {
26
+ errors.push(`Invalid URL '${patternString}': ${error.message}`)
27
+ }
28
+ }
29
+
30
+ return { errors, remoteDomains }
31
+ }
32
+
33
+ const getErrorMessage = function ({ message }) {
34
+ return message
35
+ }
36
+
37
+ export const handleImageDomainsErrors = async function (errors) {
38
+ if (errors.length === 0) {
39
+ return
40
+ }
41
+
42
+ const errorMessage = await errors.map(getErrorMessage).join('\n\n')
43
+ log(NETLIFYDEVERR, `Image domains syntax errors:\n${errorMessage}`)
44
+ }
45
+
46
+ export const parseRemoteImageDomains = async function ({ config }) {
47
+ if (!config) {
48
+ return []
49
+ }
50
+
51
+ const { errors, remoteDomains } = await parseAllDomains(config)
52
+ await handleImageDomainsErrors(errors)
53
+
54
+ return remoteDomains
55
+ }
56
+ export const isImageRequest = function (req) {
57
+ return req.url.startsWith(IMAGE_URL_PATTERN)
58
+ }
59
+
60
+ export const transformImageParams = function (query) {
61
+ const params = {}
62
+ // eslint-disable-next-line id-length
63
+ params.w = query.w || query.width || null
64
+ // eslint-disable-next-line id-length
65
+ params.h = query.h || query.height || null
66
+ params.quality = query.q || query.quality || null
67
+ params.format = query.fm || null
68
+ params.fit = mapImgixToFitIpx(query.fit, query.crop)
69
+ params.position = query.crop || null
70
+
71
+ return Object.entries(params)
72
+ .filter(([, value]) => value !== null)
73
+ .map(([key, value]) => `${key}_${value}`)
74
+ .join(',')
75
+ }
76
+
77
+ function mapImgixToFitIpx(fit, crop) {
78
+ if (crop) {
79
+ return 'cover'
80
+ }
81
+
82
+ const fitMapping = {
83
+ // IPX doesn't have exact equivalent.
84
+ clamp: null,
85
+ clip: 'contain',
86
+ crop: 'cover',
87
+ max: 'inside',
88
+ min: 'outside',
89
+ scale: 'fill',
90
+ }
91
+
92
+ return fitMapping[fit] ?? 'contain'
93
+ }
94
+
95
+ export const initializeProxy = async function ({ config }) {
96
+ const remoteDomains = await parseRemoteImageDomains({ config })
97
+
98
+ const ipx = createIPX({
99
+ storage: ipxFSStorage({ dir: config?.build?.publish ?? './public' }),
100
+ httpStorage: ipxHttpStorage({ domains: remoteDomains }),
101
+ })
102
+
103
+ const handler = createIPXNodeServer(ipx)
104
+ const app = express()
105
+
106
+ app.use(IMAGE_URL_PATTERN, async (req, res) => {
107
+ const { url, ...query } = req.query
108
+ const modifiers = await transformImageParams(query)
109
+ const path = `/${modifiers}/${encodeURIComponent(url)}`
110
+ req.url = path
111
+ handler(req, res)
112
+ })
113
+
114
+ return app
115
+ }
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'url'
7
7
 
8
8
  import execa from 'execa'
9
9
  import hasbin from 'hasbin'
10
- import Listr from 'listr'
10
+ import { Listr } from 'listr2'
11
11
  import pathKey from 'path-key'
12
12
 
13
13
  import { fetchLatestVersion, shouldFetchLatestVersion } from '../../lib/exec-fetcher.mjs'
@@ -52,6 +52,7 @@ export const generateInspectSettings = (edgeInspect, edgeInspectBrk) => {
52
52
  * @param {object} params.site
53
53
  * @param {*} params.siteInfo
54
54
  * @param {string} params.projectDir
55
+ * @param {string} params.repositoryRoot
55
56
  * @param {import('./state-config.mjs').default} params.state
56
57
  * @param {import('../lib/functions/registry.mjs').FunctionsRegistry=} params.functionsRegistry
57
58
  * @returns
@@ -71,6 +72,7 @@ export const startProxyServer = async ({
71
72
  inspectSettings,
72
73
  offline,
73
74
  projectDir,
75
+ repositoryRoot,
74
76
  settings,
75
77
  site,
76
78
  siteInfo,
@@ -94,6 +96,7 @@ export const startProxyServer = async ({
94
96
  state,
95
97
  siteInfo,
96
98
  accountId,
99
+ repositoryRoot,
97
100
  })
98
101
  if (!url) {
99
102
  log(NETLIFYDEVERR, `Unable to start proxy server on port '${settings.port}'`)
@@ -28,6 +28,7 @@ import {
28
28
  } from '../lib/edge-functions/proxy.mjs'
29
29
  import { fileExistsAsync, isFileAsync } from '../lib/fs.mjs'
30
30
  import { DEFAULT_FUNCTION_URL_EXPRESSION } from '../lib/functions/registry.mjs'
31
+ import { initializeProxy as initializeImageProxy, isImageRequest } from '../lib/images/proxy.mjs'
31
32
  import renderErrorTemplate from '../lib/render-error-template.mjs'
32
33
 
33
34
  import { NETLIFYDEVLOG, NETLIFYDEVWARN, log, chalk } from './command-helpers.mjs'
@@ -182,7 +183,17 @@ const alternativePathsFor = function (url) {
182
183
  return paths
183
184
  }
184
185
 
185
- const serveRedirect = async function ({ env, functionsRegistry, match, options, proxy, req, res, siteInfo }) {
186
+ const serveRedirect = async function ({
187
+ env,
188
+ functionsRegistry,
189
+ imageProxy,
190
+ match,
191
+ options,
192
+ proxy,
193
+ req,
194
+ res,
195
+ siteInfo,
196
+ }) {
186
197
  if (!match) return proxy.web(req, res, options)
187
198
 
188
199
  options = options || req.proxyOptions || {}
@@ -355,7 +366,9 @@ const serveRedirect = async function ({ env, functionsRegistry, match, options,
355
366
 
356
367
  return proxy.web(req, res, { headers: functionHeaders, target: options.functionsServer })
357
368
  }
358
-
369
+ if (isImageRequest(req)) {
370
+ return imageProxy(req, res)
371
+ }
359
372
  const addonUrl = getAddonUrl(options.addonsUrls, req)
360
373
  if (addonUrl) {
361
374
  return handleAddonUrl({ req, res, addonUrl })
@@ -378,7 +391,7 @@ const reqToURL = function (req, pathname) {
378
391
 
379
392
  const MILLISEC_TO_SEC = 1e3
380
393
 
381
- const initializeProxy = async function ({ configPath, distDir, env, host, port, projectDir, siteInfo }) {
394
+ const initializeProxy = async function ({ configPath, distDir, env, host, imageProxy, port, projectDir, siteInfo }) {
382
395
  const proxy = httpProxy.createProxyServer({
383
396
  selfHandleResponse: true,
384
397
  target: {
@@ -386,7 +399,6 @@ const initializeProxy = async function ({ configPath, distDir, env, host, port,
386
399
  port,
387
400
  },
388
401
  })
389
-
390
402
  const headersFiles = [...new Set([path.resolve(projectDir, '_headers'), path.resolve(distDir, '_headers')])]
391
403
 
392
404
  let headers = await parseHeaders({ headersFiles, configPath })
@@ -466,6 +478,7 @@ const initializeProxy = async function ({ configPath, distDir, env, host, port,
466
478
  req,
467
479
  res,
468
480
  proxy: handlers,
481
+ imageProxy,
469
482
  match: req.proxyOptions.match,
470
483
  options: req.proxyOptions,
471
484
  siteInfo,
@@ -484,6 +497,7 @@ const initializeProxy = async function ({ configPath, distDir, env, host, port,
484
497
  req,
485
498
  res,
486
499
  proxy: handlers,
500
+ imageProxy,
487
501
  match: null,
488
502
  options: req.proxyOptions,
489
503
  siteInfo,
@@ -586,7 +600,18 @@ const initializeProxy = async function ({ configPath, distDir, env, host, port,
586
600
  }
587
601
 
588
602
  const onRequest = async (
589
- { addonsUrls, edgeFunctionsProxy, env, functionsRegistry, functionsServer, proxy, rewriter, settings, siteInfo },
603
+ {
604
+ addonsUrls,
605
+ edgeFunctionsProxy,
606
+ env,
607
+ functionsRegistry,
608
+ functionsServer,
609
+ imageProxy,
610
+ proxy,
611
+ rewriter,
612
+ settings,
613
+ siteInfo,
614
+ },
590
615
  req,
591
616
  res,
592
617
  ) => {
@@ -642,7 +667,7 @@ const onRequest = async (
642
667
  // We don't want to generate an ETag for 3xx redirects.
643
668
  req[shouldGenerateETag] = ({ statusCode }) => statusCode < 300 || statusCode >= 400
644
669
 
645
- return serveRedirect({ req, res, proxy, match, options, siteInfo, env, functionsRegistry })
670
+ return serveRedirect({ req, res, proxy, imageProxy, match, options, siteInfo, env, functionsRegistry })
646
671
  }
647
672
 
648
673
  // The request will be served by the framework server, which means we want to
@@ -660,6 +685,10 @@ const onRequest = async (
660
685
  return proxy.web(req, res, { target: functionsServer })
661
686
  }
662
687
 
688
+ if (isImageRequest(req)) {
689
+ return imageProxy(req, res)
690
+ }
691
+
663
692
  proxy.web(req, res, options)
664
693
  }
665
694
 
@@ -687,6 +716,7 @@ export const startProxy = async function ({
687
716
  inspectSettings,
688
717
  offline,
689
718
  projectDir,
719
+ repositoryRoot,
690
720
  settings,
691
721
  siteInfo,
692
722
  state,
@@ -708,10 +738,15 @@ export const startProxy = async function ({
708
738
  passthroughPort: secondaryServerPort || settings.port,
709
739
  settings,
710
740
  projectDir,
741
+ repositoryRoot,
711
742
  siteInfo,
712
743
  accountId,
713
744
  state,
714
745
  })
746
+
747
+ const imageProxy = await initializeImageProxy({
748
+ config,
749
+ })
715
750
  const proxy = await initializeProxy({
716
751
  env,
717
752
  host: settings.frameworkHost,
@@ -720,6 +755,7 @@ export const startProxy = async function ({
720
755
  projectDir,
721
756
  configPath,
722
757
  siteInfo,
758
+ imageProxy,
723
759
  })
724
760
 
725
761
  const rewriter = await createRewriter({
@@ -739,6 +775,7 @@ export const startProxy = async function ({
739
775
  functionsRegistry,
740
776
  functionsServer,
741
777
  edgeFunctionsProxy,
778
+ imageProxy,
742
779
  siteInfo,
743
780
  env,
744
781
  })