netlify-cli 10.0.0 → 10.1.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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
- "version": "10.0.0",
3
+ "version": "10.1.0",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "netlify-cli",
9
- "version": "10.0.0",
9
+ "version": "10.1.0",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
@@ -138,6 +138,7 @@
138
138
  "husky": "^7.0.4",
139
139
  "ini": "^2.0.0",
140
140
  "mock-fs": "^5.1.2",
141
+ "nock": "^13.2.4",
141
142
  "p-timeout": "^4.0.0",
142
143
  "rewiremock": "^3.14.3",
143
144
  "seedrandom": "^3.0.5",
@@ -14405,6 +14406,12 @@
14405
14406
  "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
14406
14407
  "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
14407
14408
  },
14409
+ "node_modules/lodash.set": {
14410
+ "version": "4.3.2",
14411
+ "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
14412
+ "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=",
14413
+ "dev": true
14414
+ },
14408
14415
  "node_modules/lodash.some": {
14409
14416
  "version": "4.6.0",
14410
14417
  "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz",
@@ -15831,6 +15838,21 @@
15831
15838
  "isarray": "0.0.1"
15832
15839
  }
15833
15840
  },
15841
+ "node_modules/nock": {
15842
+ "version": "13.2.4",
15843
+ "resolved": "https://registry.npmjs.org/nock/-/nock-13.2.4.tgz",
15844
+ "integrity": "sha512-8GPznwxcPNCH/h8B+XZcKjYPXnUV5clOKCjAqyjsiqA++MpNx9E9+t8YPp0MbThO+KauRo7aZJ1WuIZmOrT2Ug==",
15845
+ "dev": true,
15846
+ "dependencies": {
15847
+ "debug": "^4.1.0",
15848
+ "json-stringify-safe": "^5.0.1",
15849
+ "lodash.set": "^4.3.2",
15850
+ "propagate": "^2.0.0"
15851
+ },
15852
+ "engines": {
15853
+ "node": ">= 10.13"
15854
+ }
15855
+ },
15834
15856
  "node_modules/node-domexception": {
15835
15857
  "version": "1.0.0",
15836
15858
  "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@@ -18235,6 +18257,15 @@
18235
18257
  "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
18236
18258
  "dev": true
18237
18259
  },
18260
+ "node_modules/propagate": {
18261
+ "version": "2.0.1",
18262
+ "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
18263
+ "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
18264
+ "dev": true,
18265
+ "engines": {
18266
+ "node": ">= 8"
18267
+ }
18268
+ },
18238
18269
  "node_modules/proxy-addr": {
18239
18270
  "version": "2.0.7",
18240
18271
  "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -33416,6 +33447,12 @@
33416
33447
  "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
33417
33448
  "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
33418
33449
  },
33450
+ "lodash.set": {
33451
+ "version": "4.3.2",
33452
+ "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
33453
+ "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=",
33454
+ "dev": true
33455
+ },
33419
33456
  "lodash.some": {
33420
33457
  "version": "4.6.0",
33421
33458
  "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz",
@@ -34486,6 +34523,18 @@
34486
34523
  }
34487
34524
  }
34488
34525
  },
34526
+ "nock": {
34527
+ "version": "13.2.4",
34528
+ "resolved": "https://registry.npmjs.org/nock/-/nock-13.2.4.tgz",
34529
+ "integrity": "sha512-8GPznwxcPNCH/h8B+XZcKjYPXnUV5clOKCjAqyjsiqA++MpNx9E9+t8YPp0MbThO+KauRo7aZJ1WuIZmOrT2Ug==",
34530
+ "dev": true,
34531
+ "requires": {
34532
+ "debug": "^4.1.0",
34533
+ "json-stringify-safe": "^5.0.1",
34534
+ "lodash.set": "^4.3.2",
34535
+ "propagate": "^2.0.0"
34536
+ }
34537
+ },
34489
34538
  "node-domexception": {
34490
34539
  "version": "1.0.0",
34491
34540
  "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@@ -36300,6 +36349,12 @@
36300
36349
  }
36301
36350
  }
36302
36351
  },
36352
+ "propagate": {
36353
+ "version": "2.0.1",
36354
+ "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
36355
+ "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
36356
+ "dev": true
36357
+ },
36303
36358
  "proxy-addr": {
36304
36359
  "version": "2.0.7",
36305
36360
  "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
3
  "description": "Netlify command line tool",
4
- "version": "10.0.0",
4
+ "version": "10.1.0",
5
5
  "author": "Netlify Inc.",
6
6
  "contributors": [
7
7
  "Abraham Schilling <AbrahamSchilling@gmail.com> (https://gitlab.com/n4bb12)",
@@ -103,6 +103,7 @@
103
103
  "Sam Holmes <samholmes1337@gmail.com> (https://samholmes.net)",
104
104
  "Sander de Groot (https://degroot.dev)",
105
105
  "Sarah Drasner <sarah.drasner@gmail.com> (https://twitter.com/sarah_edo)",
106
+ "Sarah Etter <sarah@sarahetter.com> (http://www.sarahetter.com)",
106
107
  "Scott Spence <spences10apps@gmail.com> (https://twitter.com/spences10)",
107
108
  "Sean Grove <sean@bushi.do> (https://twitter.com/sgrove)",
108
109
  "Sebastian Smolorz",
@@ -330,6 +331,7 @@
330
331
  "husky": "^7.0.4",
331
332
  "ini": "^2.0.0",
332
333
  "mock-fs": "^5.1.2",
334
+ "nock": "^13.2.4",
333
335
  "p-timeout": "^4.0.0",
334
336
  "rewiremock": "^3.14.3",
335
337
  "seedrandom": "^3.0.5",
@@ -201,18 +201,33 @@ const FRAMEWORK_PORT_TIMEOUT = 6e5
201
201
  * @param {*} params.addonsUrls
202
202
  * @param {import('../base-command').NetlifyOptions["config"]} params.config
203
203
  * @param {() => Promise<object>} params.getUpdatedConfig
204
+ * @param {string} params.geolocationMode
204
205
  * @param {*} params.settings
206
+ * @param {boolean} params.offline
205
207
  * @param {*} params.site
208
+ * @param {import('../../utils/state-config').StateConfig} params.state
206
209
  * @returns
207
210
  */
208
- const startProxyServer = async ({ addonsUrls, config, getUpdatedConfig, settings, site }) => {
211
+ const startProxyServer = async ({
212
+ addonsUrls,
213
+ config,
214
+ geolocationMode,
215
+ getUpdatedConfig,
216
+ offline,
217
+ settings,
218
+ site,
219
+ state,
220
+ }) => {
209
221
  const url = await startProxy({
210
222
  addonsUrls,
211
223
  config,
212
224
  configPath: site.configPath,
225
+ geolocationMode,
213
226
  getUpdatedConfig,
227
+ offline,
214
228
  projectDir: site.root,
215
229
  settings,
230
+ state,
216
231
  })
217
232
 
218
233
  if (!url) {
@@ -365,7 +380,16 @@ const dev = async (options, command) => {
365
380
  return normalizedNewConfig
366
381
  }
367
382
 
368
- let url = await startProxyServer({ settings, site, addonsUrls, config, getUpdatedConfig })
383
+ let url = await startProxyServer({
384
+ addonsUrls,
385
+ config,
386
+ geolocationMode: options.geo,
387
+ getUpdatedConfig,
388
+ offline: options.offline,
389
+ settings,
390
+ site,
391
+ state,
392
+ })
369
393
 
370
394
  const liveTunnelUrl = await handleLiveTunnel({ options, site, api, settings })
371
395
  url = liveTunnelUrl || url
@@ -484,17 +508,19 @@ const createDevCommand = (program) => {
484
508
  .option('-o ,--offline', 'disables any features that require network access')
485
509
  .option('-l, --live', 'start a public live session', false)
486
510
  .option('--functionsPort <port>', 'port of functions server', (value) => Number.parseInt(value))
511
+ .addOption(
512
+ new Option(
513
+ '--geo <mode>',
514
+ 'force geolocation data to be updated, use cached data from the last 24h if found, or use a mock location',
515
+ )
516
+ .choices(['cache', 'mock', 'update'])
517
+ .default('cache'),
518
+ )
487
519
  .addOption(
488
520
  new Option('--staticServerPort <port>', 'port of the static app server used when no framework is detected')
489
521
  .argParser((value) => Number.parseInt(value))
490
522
  .hideHelp(),
491
523
  )
492
- .addOption(
493
- new Option(
494
- '-g ,--locationDb <path>',
495
- 'specify the path to a local GeoIP location database in MMDB format',
496
- ).hideHelp(),
497
- )
498
524
  .addOption(new Option('--graph', 'enable Netlify Graph support').hideHelp())
499
525
  .addExamples([
500
526
  'netlify dev',
@@ -1,5 +1,6 @@
1
1
  module.exports = {
2
2
  Functions: 'x-deno-functions',
3
+ Geo: 'x-nf-geo',
3
4
  PassHost: 'X-NF-Pass-Host',
4
5
  Passthrough: 'x-deno-pass',
5
6
  RequestID: 'X-NF-Request-ID',
@@ -1,10 +1,12 @@
1
1
  // @ts-check
2
- const { env } = require('process')
2
+ const { relative } = require('path')
3
+ const { cwd, env } = require('process')
3
4
 
4
5
  const getAvailablePort = require('get-port')
5
6
  const { v4: generateUUID } = require('uuid')
6
7
 
7
- const { NETLIFYDEVERR, chalk } = require('../../utils/command-helpers')
8
+ const { NETLIFYDEVERR, NETLIFYDEVWARN, chalk, log } = require('../../utils/command-helpers')
9
+ const { getGeoLocation } = require('../geo-location')
8
10
  const { getPathInProject } = require('../settings')
9
11
  const { startSpinner, stopSpinner } = require('../spinner')
10
12
 
@@ -40,7 +42,7 @@ const handleProxyRequest = (req, proxyReq) => {
40
42
  })
41
43
  }
42
44
 
43
- const initializeProxy = async ({ config, configPath, getUpdatedConfig, settings }) => {
45
+ const initializeProxy = async ({ config, configPath, geolocationMode, getUpdatedConfig, offline, settings, state }) => {
44
46
  const { functions: internalFunctions, importMap, path: internalFunctionsPath } = await getInternalFunctions()
45
47
  const { port: mainPort } = settings
46
48
  const userFunctionsPath = config.build.edge_functions
@@ -65,24 +67,39 @@ const initializeProxy = async ({ config, configPath, getUpdatedConfig, settings
65
67
  return
66
68
  }
67
69
 
68
- const { registry } = await server
70
+ const [geoLocation, { registry }] = await Promise.all([
71
+ getGeoLocation({ mode: geolocationMode, offline, state }),
72
+ server,
73
+ ])
74
+
75
+ // Setting header with geolocation.
76
+ req.headers[headers.Geo] = JSON.stringify(geoLocation)
69
77
 
70
78
  await registry.initialize()
71
79
 
72
- const manifest = await registry.getManifest()
73
80
  const url = new URL(req.url, `http://${LOCAL_HOST}:${mainPort}`)
74
- const routes = manifest.routes.map((route) => ({
75
- ...route,
76
- pattern: new RegExp(route.pattern),
77
- }))
78
- const matchingFunctions = routes.filter(({ pattern }) => pattern.test(url.pathname)).map((route) => route.function)
79
-
80
- if (matchingFunctions.length === 0) {
81
+ const { functionNames, orphanedDeclarations } = await registry.matchURLPath(url.pathname)
82
+
83
+ // If the request matches a config declaration for an Edge Function without
84
+ // a matching function file, we warn the user.
85
+ orphanedDeclarations.forEach((functionName) => {
86
+ log(
87
+ `${NETLIFYDEVWARN} Request to ${chalk.yellow(
88
+ url.pathname,
89
+ )} matches declaration for edge function ${chalk.yellow(
90
+ functionName,
91
+ )}, but there's no matching function file in ${chalk.yellow(
92
+ relative(cwd(), userFunctionsPath),
93
+ )}. Please visit ${chalk.blue('https://ntl.fyi/edge-create')} for more information.`,
94
+ )
95
+ })
96
+
97
+ if (functionNames.length === 0) {
81
98
  return
82
99
  }
83
100
 
84
101
  req[headersSymbol] = {
85
- [headers.Functions]: matchingFunctions.join(','),
102
+ [headers.Functions]: functionNames.join(','),
86
103
  [headers.PassHost]: `${LOCAL_HOST}:${mainPort}`,
87
104
  [headers.Passthrough]: 'passthrough',
88
105
  [headers.RequestID]: generateUUID(),
@@ -158,7 +158,10 @@ class EdgeFunctionsRegistry {
158
158
 
159
159
  getDeclarations(config) {
160
160
  const { edge_functions: userFunctions = [] } = config
161
- const declarations = [...this.internalFunctions, ...userFunctions]
161
+
162
+ // The order is important, since we want to run user-defined functions
163
+ // before internal functions.
164
+ const declarations = [...userFunctions, ...this.internalFunctions]
162
165
 
163
166
  return declarations
164
167
  }
@@ -224,6 +227,45 @@ class EdgeFunctionsRegistry {
224
227
  log(`${NETLIFYDEVLOG} ${chalk.magenta('Removed')} edge function ${chalk.yellow(func.name)}`)
225
228
  }
226
229
 
230
+ /**
231
+ * @param {string} urlPath
232
+ */
233
+ async matchURLPath(urlPath) {
234
+ // `generateManifest` will only include functions for which there is both a
235
+ // function file and a config declaration, but we want to catch cases where
236
+ // a config declaration exists without a matching function file. To do that
237
+ // we compute a list of functions from the declarations (the `path` doesn't
238
+ // really matter) and later on match the resulting routes against the list
239
+ // of functions we have in the registry. Any functions found in the former
240
+ // but not the latter are treated as orphaned declarations.
241
+ const functions = this.declarations.map((declaration) => ({ name: declaration.function, path: '' }))
242
+ const manifest = await this.bundler.generateManifest({
243
+ declarations: this.declarations,
244
+ functions,
245
+ })
246
+ const routes = manifest.routes.map((route) => ({
247
+ ...route,
248
+ pattern: new RegExp(route.pattern),
249
+ }))
250
+ const orphanedDeclarations = new Set()
251
+ const functionNames = routes
252
+ .filter(({ pattern }) => pattern.test(urlPath))
253
+ .map((route) => {
254
+ const matchingFunction = this.functions.find(({ name }) => name === route.function)
255
+
256
+ if (matchingFunction === undefined) {
257
+ orphanedDeclarations.add(route.function)
258
+
259
+ return null
260
+ }
261
+
262
+ return matchingFunction.name
263
+ })
264
+ .filter(Boolean)
265
+
266
+ return { functionNames, orphanedDeclarations }
267
+ }
268
+
227
269
  processGraph(graph) {
228
270
  if (!graph) {
229
271
  warn('Could not process edge functions dependency graph. Live reload will not be available.')
@@ -0,0 +1,99 @@
1
+ // @ts-check
2
+ const fetch = require('node-fetch')
3
+
4
+ const API_URL = 'https://netlifind.netlify.app'
5
+ const STATE_GEO_PROPERTY = 'geolocation'
6
+
7
+ // 24 hours
8
+ const CACHE_TTL = 8.64e7
9
+
10
+ // 10 seconds
11
+ const REQUEST_TIMEOUT = 1e4
12
+
13
+ /**
14
+ * @typedef GeoLocation
15
+ * @type {object}
16
+ * @property {string} city
17
+ * @property {object} country
18
+ * @property {string} country.code
19
+ * @property {string} country.name
20
+ * @property {object} country
21
+ * @property {string} country.code
22
+ * @property {string} country.name
23
+ */
24
+
25
+ // The default location to be used if we're unable to talk to the API.
26
+ const mockLocation = {
27
+ city: 'San Francisco',
28
+ country: { code: 'US', name: 'United States' },
29
+ subdivision: { code: 'CA', name: 'California' },
30
+ }
31
+
32
+ /**
33
+ * Returns geolocation data from a remote API, the local cache, or a mock
34
+ * location, depending on the mode selected.
35
+ *
36
+ * @param {object} params
37
+ * @param {string} params.geolocationMode
38
+ * @param {"cache"|"update"|"mock"} params.mode
39
+ * @param {boolean} params.offline
40
+ * @param {import('../utils/state-config').StateConfig} params.state
41
+ * @returns {Promise<GeoLocation>}
42
+ */
43
+ const getGeoLocation = async ({ mode, offline, state }) => {
44
+ const cacheObject = state.get(STATE_GEO_PROPERTY)
45
+
46
+ // If we have cached geolocation data and the `--geo` option is set to
47
+ // `cache`, let's try to use it.
48
+ if (cacheObject !== undefined && mode === 'cache') {
49
+ const age = Date.now() - cacheObject.timestamp
50
+
51
+ // Let's use the cached data if it's not older than the TTL. Also, if the
52
+ // `--offline` option was used, it's best to use the cached location than
53
+ // the mock one.
54
+ if (age < CACHE_TTL || offline) {
55
+ return cacheObject.data
56
+ }
57
+ }
58
+
59
+ // If the `--geo` option is set to `mock`, we use the mock location. Also,
60
+ // if the `--offline` option was used, we can't talk to the API, so let's
61
+ // also use the mock location.
62
+ if (mode === 'mock' || offline) {
63
+ return mockLocation
64
+ }
65
+
66
+ // Trying to retrieve geolocation data from the API and caching it locally.
67
+ try {
68
+ const data = await getGeoLocationFromAPI()
69
+ const newCacheObject = {
70
+ data,
71
+ timestamp: Date.now(),
72
+ }
73
+
74
+ state.set(STATE_GEO_PROPERTY, newCacheObject)
75
+
76
+ return data
77
+ } catch {
78
+ // We couldn't get geolocation data from the API, so let's return the
79
+ // mock location.
80
+ return mockLocation
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Returns geolocation data from a remote API
86
+ *
87
+ * @returns {Promise<GeoLocation>}
88
+ */
89
+ const getGeoLocationFromAPI = async () => {
90
+ const res = await fetch(API_URL, {
91
+ method: 'GET',
92
+ timeout: REQUEST_TIMEOUT,
93
+ })
94
+ const { geo } = await res.json()
95
+
96
+ return geo
97
+ }
98
+
99
+ module.exports = { getGeoLocation, mockLocation }
@@ -461,13 +461,26 @@ const onRequest = async ({ addonsUrls, edgeFunctionsProxy, functionsServer, prox
461
461
  proxy.web(req, res, options)
462
462
  }
463
463
 
464
- const startProxy = async function ({ addonsUrls, config, configPath, getUpdatedConfig, projectDir, settings }) {
464
+ const startProxy = async function ({
465
+ addonsUrls,
466
+ config,
467
+ configPath,
468
+ geolocationMode,
469
+ getUpdatedConfig,
470
+ offline,
471
+ projectDir,
472
+ settings,
473
+ state,
474
+ }) {
465
475
  const functionsServer = settings.functionsPort ? `http://localhost:${settings.functionsPort}` : null
466
476
  const edgeFunctionsProxy = await edgeFunctions.initializeProxy({
467
477
  config,
468
478
  configPath,
479
+ geolocationMode,
469
480
  getUpdatedConfig,
481
+ offline,
470
482
  settings,
483
+ state,
471
484
  })
472
485
  const proxy = await initializeProxy({
473
486
  port: settings.frameworkPort,