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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
3
  "description": "Netlify command line tool",
4
- "version": "17.1.0",
4
+ "version": "17.2.0",
5
5
  "author": "Netlify Inc.",
6
6
  "type": "module",
7
7
  "engines": {
@@ -48,7 +48,7 @@
48
48
  "@netlify/build": "29.25.0",
49
49
  "@netlify/build-info": "7.10.2",
50
50
  "@netlify/config": "20.9.0",
51
- "@netlify/edge-bundler": "9.4.1",
51
+ "@netlify/edge-bundler": "^10.1.0",
52
52
  "@netlify/local-functions-proxy": "1.1.1",
53
53
  "@netlify/zip-it-and-ship-it": "9.25.7",
54
54
  "@octokit/rest": "19.0.13",
@@ -100,6 +100,7 @@
100
100
  "https-proxy-agent": "5.0.1",
101
101
  "inquirer": "6.5.2",
102
102
  "inquirer-autocomplete-prompt": "1.4.0",
103
+ "ipx": "^2.0.1",
103
104
  "is-docker": "3.0.0",
104
105
  "is-stream": "3.0.0",
105
106
  "is-wsl": "2.2.0",
@@ -108,7 +109,7 @@
108
109
  "jsonwebtoken": "9.0.1",
109
110
  "jwt-decode": "3.1.2",
110
111
  "lambda-local": "2.1.2",
111
- "listr": "0.14.3",
112
+ "listr2": "^7.0.2",
112
113
  "locate-path": "7.2.0",
113
114
  "lodash": "4.17.21",
114
115
  "log-symbols": "5.1.0",
@@ -227,6 +227,7 @@ const dev = async (options, command) => {
227
227
  geoCountry: options.country,
228
228
  accountId,
229
229
  functionsRegistry,
230
+ repositoryRoot,
230
231
  })
231
232
 
232
233
  if (devConfig.autoLaunch !== false) {
@@ -71,7 +71,7 @@ export const createFunctionsServeCommand = (program) =>
71
71
  program
72
72
  .command('functions:serve')
73
73
  .alias('function:serve')
74
- .description('(Beta) Serve functions locally')
74
+ .description('Serve functions locally')
75
75
  .option('-f, --functions <dir>', 'Specify a functions directory to serve')
76
76
  .option('-p, --port <port>', 'Specify a port for the functions server', (value) => Number.parseInt(value))
77
77
  .option('-o, --offline', 'disables any features that require network access')
@@ -1,5 +1,5 @@
1
1
  // @ts-check
2
- import Listr from 'listr'
2
+ import { Listr } from 'listr2'
3
3
 
4
4
  import {
5
5
  checkGitLFSVersionStep,
@@ -1,5 +1,5 @@
1
1
  // @ts-check
2
- import Listr from 'listr'
2
+ import { Listr } from 'listr2'
3
3
 
4
4
  import { error } from '../../utils/command-helpers.mjs'
5
5
  import execa from '../../utils/execa.mjs'
@@ -1,13 +1,13 @@
1
1
  // @ts-check
2
2
  import { Buffer } from 'buffer'
3
3
  import { rm } from 'fs/promises'
4
- import { join, relative, resolve } from 'path'
4
+ import { join, resolve } from 'path'
5
5
 
6
6
  // eslint-disable-next-line import/no-namespace
7
7
  import * as bundler from '@netlify/edge-bundler'
8
8
  import getAvailablePort from 'get-port'
9
9
 
10
- import { NETLIFYDEVERR, NETLIFYDEVWARN, chalk, error as printError, log } from '../../utils/command-helpers.mjs'
10
+ import { NETLIFYDEVERR, chalk, error as printError } from '../../utils/command-helpers.mjs'
11
11
  import { getGeoLocation } from '../geo-location.mjs'
12
12
  import { getPathInProject } from '../settings.mjs'
13
13
  import { startSpinner, stopSpinner } from '../spinner.mjs'
@@ -99,6 +99,7 @@ export const initializeProxy = async ({
99
99
  offline,
100
100
  passthroughPort,
101
101
  projectDir,
102
+ repositoryRoot,
102
103
  settings,
103
104
  siteInfo,
104
105
  state,
@@ -132,6 +133,7 @@ export const initializeProxy = async ({
132
133
  internalFunctions,
133
134
  port: isolatePort,
134
135
  projectDir,
136
+ repositoryRoot,
135
137
  })
136
138
  const hasEdgeFunctions = userFunctionsPath !== undefined || internalFunctionsPath
137
139
 
@@ -162,21 +164,7 @@ export const initializeProxy = async ({
162
164
  await registry.initialize()
163
165
 
164
166
  const url = new URL(req.url, `http://${LOCAL_HOST}:${mainPort}`)
165
- const { functionNames, invocationMetadata, orphanedDeclarations } = registry.matchURLPath(url.pathname, req.method)
166
-
167
- // If the request matches a config declaration for an Edge Function without
168
- // a matching function file, we warn the user.
169
- orphanedDeclarations.forEach((functionName) => {
170
- log(
171
- `${NETLIFYDEVWARN} Request to ${chalk.yellow(
172
- url.pathname,
173
- )} matches declaration for edge function ${chalk.yellow(
174
- functionName,
175
- )}, but there's no matching function file in ${chalk.yellow(
176
- relative(projectDir, userFunctionsPath),
177
- )}. Please visit ${chalk.blue('https://ntl.fyi/edge-create')} for more information.`,
178
- )
179
- })
167
+ const { functionNames, invocationMetadata } = registry.matchURLPath(url.pathname, req.method)
180
168
 
181
169
  if (functionNames.length === 0) {
182
170
  return
@@ -217,6 +205,7 @@ const prepareServer = async ({
217
205
  internalFunctions,
218
206
  port,
219
207
  projectDir,
208
+ repositoryRoot,
220
209
  }) => {
221
210
  // Merging internal with user-defined import maps.
222
211
  const importMapPaths = [...importMaps, config.functions['*'].deno_import_map]
@@ -243,6 +232,7 @@ const prepareServer = async ({
243
232
  importMapPaths,
244
233
  inspectSettings,
245
234
  port,
235
+ rootPath: repositoryRoot,
246
236
  servePath,
247
237
  })
248
238
  const registry = new EdgeFunctionsRegistry({
@@ -13,8 +13,14 @@ import {
13
13
 
14
14
  /** @typedef {import('@netlify/edge-bundler').Declaration} Declaration */
15
15
  /** @typedef {import('@netlify/edge-bundler').EdgeFunction} EdgeFunction */
16
+ /**
17
+ * @typedef {"buildError" | "loaded" | "reloaded" | "reloading" | "removed"} EdgeFunctionEvent
18
+ */
16
19
  /** @typedef {import('@netlify/edge-bundler').FunctionConfig} FunctionConfig */
20
+ /** @typedef {import('@netlify/edge-bundler').Manifest} Manifest */
21
+ /** @typedef {import('@netlify/edge-bundler').ModuleGraph} ModuleGraph */
17
22
  /** @typedef {Awaited<ReturnType<typeof import('@netlify/edge-bundler').serve>>} RunIsolate */
23
+ /** @typedef {Omit<Manifest["routes"][0], "pattern"> & { pattern: RegExp }} Route */
18
24
 
19
25
  const featureFlags = { edge_functions_correct_order: true }
20
26
 
@@ -46,12 +52,6 @@ export class EdgeFunctionsRegistry {
46
52
  /** @type {Declaration[]} */
47
53
  #declarationsFromDeployConfig
48
54
 
49
- /** @type {Record<string, FunctionConfig>} */
50
- #userFunctionConfigs = {}
51
-
52
- /** @type {Record<string, FunctionConfig>} */
53
- #internalFunctionConfigs = {}
54
-
55
55
  /** @type {Declaration[]} */
56
56
  #declarationsFromTOML
57
57
 
@@ -67,6 +67,9 @@ export class EdgeFunctionsRegistry {
67
67
  /** @type {Map<string, string>} */
68
68
  #functionPaths = new Map()
69
69
 
70
+ /** @type {Manifest | null} */
71
+ #manifest = null
72
+
70
73
  /** @type {EdgeFunction[]} */
71
74
  #userFunctions = []
72
75
 
@@ -76,6 +79,9 @@ export class EdgeFunctionsRegistry {
76
79
  /** @type {Promise<void>} */
77
80
  #initialScan
78
81
 
82
+ /** @type {Route[]} */
83
+ #routes = []
84
+
79
85
  /** @type {string} */
80
86
  #servePath
81
87
 
@@ -122,8 +128,6 @@ export class EdgeFunctionsRegistry {
122
128
  this.#env = EdgeFunctionsRegistry.#getEnvironmentVariables(env)
123
129
 
124
130
  this.#buildError = null
125
- this.#userFunctionConfigs = {}
126
- this.#internalFunctionConfigs = {}
127
131
  this.#directoryWatchers = new Map()
128
132
  this.#dependencyPaths = new Map()
129
133
  this.#functionPaths = new Map()
@@ -141,12 +145,12 @@ export class EdgeFunctionsRegistry {
141
145
  async #doInitialScan() {
142
146
  await this.#scanForFunctions()
143
147
 
144
- this.#functions.forEach((func) => {
145
- this.#logAddedFunction(func)
146
- })
147
-
148
148
  try {
149
- await this.#build()
149
+ const { warnings } = await this.#build()
150
+
151
+ this.#functions.forEach((func) => {
152
+ this.#logEvent('loaded', { functionName: func.name, warnings: warnings[func.name] })
153
+ })
150
154
  } catch {
151
155
  // no-op
152
156
  }
@@ -160,17 +164,16 @@ export class EdgeFunctionsRegistry {
160
164
  }
161
165
 
162
166
  /**
163
- * @return {Promise<void>}
167
+ * @return {Promise<{warnings: Record<string, string[]>}>}
164
168
  */
165
169
  async #build() {
170
+ /**
171
+ * @type Record<string, string[]>
172
+ */
173
+ const warnings = {}
174
+
166
175
  try {
167
- const { functionsConfig, graph, npmSpecifiersWithExtraneousFiles, success } = await this.#runIsolate(
168
- this.#functions,
169
- this.#env,
170
- {
171
- getFunctionsConfig: true,
172
- },
173
- )
176
+ const { functionsConfig, graph, npmSpecifiersWithExtraneousFiles, success } = await this.#runBuild()
174
177
 
175
178
  if (!success) {
176
179
  throw new Error('Build error')
@@ -182,18 +185,39 @@ export class EdgeFunctionsRegistry {
182
185
  // functionsConfig therefore contains first all internal functionConfigs and then user functionConfigs
183
186
  let index = 0
184
187
 
185
- this.#internalFunctionConfigs = this.#internalFunctions.reduce(
188
+ /** @type {Record<string, FunctionConfig>} */
189
+ const internalFunctionConfigs = this.#internalFunctions.reduce(
186
190
  // eslint-disable-next-line no-plusplus
187
191
  (acc, func) => ({ ...acc, [func.name]: functionsConfig[index++] }),
188
192
  {},
189
193
  )
190
194
 
191
- this.#userFunctionConfigs = this.#userFunctions.reduce(
195
+ /** @type {Record<string, FunctionConfig>} */
196
+ const userFunctionConfigs = this.#userFunctions.reduce(
192
197
  // eslint-disable-next-line no-plusplus
193
198
  (acc, func) => ({ ...acc, [func.name]: functionsConfig[index++] }),
194
199
  {},
195
200
  )
196
201
 
202
+ const { manifest, routes, unroutedFunctions } = this.#buildRoutes(internalFunctionConfigs, userFunctionConfigs)
203
+
204
+ this.#manifest = manifest
205
+ this.#routes = routes
206
+
207
+ unroutedFunctions.forEach((name) => {
208
+ warnings[name] = warnings[name] || []
209
+ warnings[name].push(
210
+ `Edge function is not accessible because it does not have a path configured. Learn more at https://ntl.fyi/edge-create.`,
211
+ )
212
+ })
213
+
214
+ for (const functionName in userFunctionConfigs) {
215
+ if ('paths' in userFunctionConfigs[functionName]) {
216
+ warnings[functionName] = warnings[functionName] || []
217
+ warnings[functionName].push(`Unknown 'paths' configuration property. Did you mean 'path'?`)
218
+ }
219
+ }
220
+
197
221
  this.#processGraph(graph)
198
222
 
199
223
  if (npmSpecifiersWithExtraneousFiles.length !== 0) {
@@ -203,6 +227,8 @@ export class EdgeFunctionsRegistry {
203
227
  `${NETLIFYDEVWARN} The following npm modules, which are directly or indirectly imported by an edge function, may not be supported: ${modules}. For more information, visit https://ntl.fyi/edge-functions-npm.`,
204
228
  )
205
229
  }
230
+
231
+ return { warnings }
206
232
  } catch (error) {
207
233
  this.#buildError = error
208
234
 
@@ -210,6 +236,38 @@ export class EdgeFunctionsRegistry {
210
236
  }
211
237
  }
212
238
 
239
+ /**
240
+ * Builds a manifest and corresponding routes for the functions in the
241
+ * registry, taking into account the declarations from the TOML, from
242
+ * the deploy configuration API, and from the in-source configuration
243
+ * found in both internal and user functions.
244
+ *
245
+ * @param {Record<string, FunctionConfig>} internalFunctionConfigs
246
+ * @param {Record<string, FunctionConfig>} userFunctionConfigs
247
+ */
248
+ #buildRoutes(internalFunctionConfigs, userFunctionConfigs) {
249
+ const declarations = this.#bundler.mergeDeclarations(
250
+ this.#declarationsFromTOML,
251
+ userFunctionConfigs,
252
+ internalFunctionConfigs,
253
+ this.#declarationsFromDeployConfig,
254
+ featureFlags,
255
+ )
256
+ const { declarationsWithoutFunction, manifest, unroutedFunctions } = this.#bundler.generateManifest({
257
+ declarations,
258
+ userFunctionConfig: userFunctionConfigs,
259
+ internalFunctionConfig: internalFunctionConfigs,
260
+ functions: this.#functions,
261
+ featureFlags,
262
+ })
263
+ const routes = [...manifest.routes, ...manifest.post_cache_routes].map((route) => ({
264
+ ...route,
265
+ pattern: new RegExp(route.pattern),
266
+ }))
267
+
268
+ return { declarationsWithoutFunction, manifest, routes, unroutedFunctions }
269
+ }
270
+
213
271
  /**
214
272
  * @returns {Promise<void>}
215
273
  */
@@ -221,14 +279,14 @@ export class EdgeFunctionsRegistry {
221
279
  }
222
280
 
223
281
  try {
224
- await this.#build()
282
+ const { warnings } = await this.#build()
225
283
 
226
284
  deletedFunctions.forEach((func) => {
227
- this.#logDeletedFunction(func)
285
+ this.#logEvent('removed', { functionName: func.name, warnings: warnings[func.name] })
228
286
  })
229
287
 
230
288
  newFunctions.forEach((func) => {
231
- this.#logAddedFunction(func)
289
+ this.#logEvent('loaded', { functionName: func.name, warnings: warnings[func.name] })
232
290
  })
233
291
  } catch {
234
292
  // no-op
@@ -288,28 +346,21 @@ export class EdgeFunctionsRegistry {
288
346
  return
289
347
  }
290
348
 
291
- const reason = this.#debug ? ` because ${chalk.underline(paths.join(','))} has changed` : ''
292
-
293
- log(`${NETLIFYDEVLOG} ${chalk.magenta('Reloading')} edge functions${reason}...`)
349
+ this.#logEvent('reloading', {})
294
350
 
295
351
  try {
296
- await this.#build()
297
-
352
+ const { warnings } = await this.#build()
298
353
  const functionNames = [...matchingFunctions]
299
354
 
300
355
  if (functionNames.length === 0) {
301
- log(`${NETLIFYDEVLOG} ${chalk.green('Reloaded')} edge functions`)
356
+ this.#logEvent('reloaded', {})
302
357
  } else {
303
358
  functionNames.forEach((functionName) => {
304
- log(
305
- `${NETLIFYDEVLOG} ${chalk.green('Reloaded')} edge function ${chalk.yellow(
306
- this.#getDisplayName(functionName),
307
- )}`,
308
- )
359
+ this.#logEvent('reloaded', { functionName, warnings: warnings[functionName] })
309
360
  })
310
361
  }
311
- } catch {
312
- log(`${NETLIFYDEVERR} ${chalk.red('Failed')} reloading edge function`)
362
+ } catch (error) {
363
+ this.#logEvent('buildError', { buildError: error?.message })
313
364
  }
314
365
  }
315
366
 
@@ -321,50 +372,72 @@ export class EdgeFunctionsRegistry {
321
372
  }
322
373
 
323
374
  /**
324
- * @param {EdgeFunction} func
375
+ * Logs an event associated with functions.
376
+ *
377
+ * @param {EdgeFunctionEvent} event
378
+ * @param {object} data
379
+ * @param {Error} [data.buildError]
380
+ * @param {string} [data.functionName]
381
+ * @param {string[]} [data.warnings]
382
+ * @returns
325
383
  */
326
- #logAddedFunction(func) {
327
- log(`${NETLIFYDEVLOG} ${chalk.green('Loaded')} edge function ${chalk.yellow(this.#getDisplayName(func.name))}`)
328
- }
384
+ #logEvent(event, { buildError, functionName, warnings = [] }) {
385
+ const subject = functionName
386
+ ? `edge function ${chalk.yellow(this.#getDisplayName(functionName))}`
387
+ : 'edge functions'
388
+ const warningsText =
389
+ warnings.length === 0 ? '' : ` with warnings:\n${warnings.map((warning) => ` - ${warning}`).join('\n')}`
329
390
 
330
- /**
331
- * @param {EdgeFunction} func
332
- */
333
- #logDeletedFunction(func) {
334
- log(`${NETLIFYDEVLOG} ${chalk.magenta('Removed')} edge function ${chalk.yellow(this.#getDisplayName(func.name))}`)
391
+ if (event === 'buildError') {
392
+ log(`${NETLIFYDEVERR} ${chalk.red('Failed to load')} ${subject}: ${buildError}`)
393
+
394
+ return
395
+ }
396
+
397
+ if (event === 'loaded') {
398
+ const icon = warningsText ? NETLIFYDEVWARN : NETLIFYDEVLOG
399
+ const color = warningsText ? chalk.yellow : chalk.green
400
+
401
+ log(`${icon} ${color('Loaded')} ${subject}${warningsText}`)
402
+
403
+ return
404
+ }
405
+
406
+ if (event === 'reloaded') {
407
+ const icon = warningsText ? NETLIFYDEVWARN : NETLIFYDEVLOG
408
+ const color = warningsText ? chalk.yellow : chalk.green
409
+
410
+ log(`${icon} ${color('Reloaded')} ${subject}${warningsText}`)
411
+
412
+ return
413
+ }
414
+
415
+ if (event === 'reloading') {
416
+ log(`${NETLIFYDEVLOG} ${chalk.magenta('Reloading')} ${subject}...`)
417
+
418
+ return
419
+ }
420
+
421
+ if (event === 'removed') {
422
+ log(`${NETLIFYDEVLOG} ${chalk.magenta('Removed')} ${subject}`)
423
+ }
335
424
  }
336
425
 
337
426
  /**
427
+ * Returns the functions in the registry that should run for a given URL path
428
+ * and HTTP method, based on the routes registered for each function.
429
+ *
338
430
  * @param {string} urlPath
339
431
  * @param {string} method
340
432
  */
341
433
  matchURLPath(urlPath, method) {
342
- const declarations = this.#bundler.mergeDeclarations(
343
- this.#declarationsFromTOML,
344
- this.#userFunctionConfigs,
345
- this.#internalFunctionConfigs,
346
- this.#declarationsFromDeployConfig,
347
- featureFlags,
348
- )
349
- const manifest = this.#bundler.generateManifest({
350
- declarations,
351
- userFunctionConfig: this.#userFunctionConfigs,
352
- internalFunctionConfig: this.#internalFunctionConfigs,
353
- functions: this.#functions,
354
- featureFlags,
355
- })
356
- const routes = [...manifest.routes, ...manifest.post_cache_routes].map((route) => ({
357
- ...route,
358
- pattern: new RegExp(route.pattern),
359
- }))
360
-
361
434
  /** @type string[] */
362
435
  const functionNames = []
363
436
 
364
437
  /** @type number[] */
365
438
  const routeIndexes = []
366
439
 
367
- routes.forEach((route, index) => {
440
+ this.#routes.forEach((route, index) => {
368
441
  if (route.methods && route.methods.length !== 0 && !route.methods.includes(method)) {
369
442
  return
370
443
  }
@@ -373,7 +446,7 @@ export class EdgeFunctionsRegistry {
373
446
  return
374
447
  }
375
448
 
376
- const isExcluded = manifest.function_config[route.function]?.excluded_patterns?.some((pattern) =>
449
+ const isExcluded = this.#manifest?.function_config[route.function]?.excluded_patterns?.some((pattern) =>
377
450
  new RegExp(pattern).test(urlPath),
378
451
  )
379
452
 
@@ -385,55 +458,24 @@ export class EdgeFunctionsRegistry {
385
458
  routeIndexes.push(index)
386
459
  })
387
460
  const invocationMetadata = {
388
- function_config: manifest.function_config,
461
+ function_config: this.#manifest?.function_config,
389
462
  req_routes: routeIndexes,
390
- routes: manifest.routes.map((route) => ({ function: route.function, path: route.path, pattern: route.pattern })),
463
+ routes: this.#manifest?.routes.map((route) => ({
464
+ function: route.function,
465
+ path: route.path,
466
+ pattern: route.pattern,
467
+ })),
391
468
  }
392
- const orphanedDeclarations = this.#matchURLPathAgainstOrphanedDeclarations(urlPath)
393
469
 
394
- return { functionNames, invocationMetadata, orphanedDeclarations }
470
+ return { functionNames, invocationMetadata }
395
471
  }
396
472
 
397
473
  /**
474
+ * Takes the module graph returned from the server and tracks dependencies of
475
+ * each function.
398
476
  *
399
- * @param {string} urlPath
400
- * @returns {string[]}
477
+ * @param {ModuleGraph} graph
401
478
  */
402
- #matchURLPathAgainstOrphanedDeclarations(urlPath) {
403
- // `generateManifest` will only include functions for which there is both a
404
- // function file and a config declaration, but we want to catch cases where
405
- // a config declaration exists without a matching function file. To do that
406
- // we compute a list of functions from the declarations (the `path` doesn't
407
- // really matter).
408
- const functions = this.#declarationsFromTOML.map((declaration) => ({ name: declaration.function, path: '' }))
409
- const manifest = this.#bundler.generateManifest({
410
- declarations: this.#declarationsFromTOML,
411
- userFunctionConfig: this.#userFunctionConfigs,
412
- internalFunctionConfig: this.#internalFunctionConfigs,
413
- functions,
414
- featureFlags,
415
- })
416
-
417
- const routes = [...manifest.routes, ...manifest.post_cache_routes].map((route) => ({
418
- ...route,
419
- pattern: new RegExp(route.pattern),
420
- }))
421
-
422
- const functionNames = routes
423
- .filter((route) => {
424
- const hasFunctionFile = this.#functions.some((func) => func.name === route.function)
425
-
426
- if (hasFunctionFile) {
427
- return false
428
- }
429
-
430
- return route.pattern.test(urlPath)
431
- })
432
- .map((route) => route.function)
433
-
434
- return functionNames
435
- }
436
-
437
479
  #processGraph(graph) {
438
480
  if (!graph) {
439
481
  warn('Could not process edge functions dependency graph. Live reload will not be available.')
@@ -485,6 +527,33 @@ export class EdgeFunctionsRegistry {
485
527
  this.#functionPaths = functionPaths
486
528
  }
487
529
 
530
+ /**
531
+ * Thin wrapper for `#runIsolate` that skips running a build and returns an
532
+ * empty response if there are no functions in the registry.
533
+ */
534
+ async #runBuild() {
535
+ if (this.#functions.length === 0) {
536
+ return {
537
+ functionsConfig: [],
538
+ graph: {
539
+ modules: [],
540
+ },
541
+ npmSpecifiersWithExtraneousFiles: [],
542
+ success: true,
543
+ }
544
+ }
545
+
546
+ const { functionsConfig, graph, npmSpecifiersWithExtraneousFiles, success } = await this.#runIsolate(
547
+ this.#functions,
548
+ this.#env,
549
+ {
550
+ getFunctionsConfig: true,
551
+ },
552
+ )
553
+
554
+ return { functionsConfig, graph, npmSpecifiersWithExtraneousFiles, success }
555
+ }
556
+
488
557
  /**
489
558
  * @returns {Promise<{all: EdgeFunction[], new: EdgeFunction[], deleted: EdgeFunction[]}>}
490
559
  */