netlify-cli 16.5.1 → 16.6.1

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": "16.5.1",
4
+ "version": "16.6.1",
5
5
  "author": "Netlify Inc.",
6
6
  "type": "module",
7
7
  "engines": {
@@ -44,13 +44,13 @@
44
44
  "dependencies": {
45
45
  "@bugsnag/js": "7.20.2",
46
46
  "@fastify/static": "6.10.2",
47
- "@netlify/build": "29.21.2",
48
- "@netlify/build-info": "7.9.0",
47
+ "@netlify/build": "29.22.2",
48
+ "@netlify/build-info": "7.10.1",
49
49
  "@netlify/config": "20.9.0",
50
50
  "@netlify/edge-bundler": "9.1.0",
51
51
  "@netlify/local-functions-proxy": "1.1.1",
52
- "@netlify/serverless-functions-api": "1.7.3",
53
- "@netlify/zip-it-and-ship-it": "9.19.0",
52
+ "@netlify/serverless-functions-api": "1.8.0",
53
+ "@netlify/zip-it-and-ship-it": "9.23.0",
54
54
  "@octokit/rest": "19.0.13",
55
55
  "ansi-escapes": "6.2.0",
56
56
  "ansi-styles": "6.2.1",
@@ -29,7 +29,12 @@ const getFormHandler = function ({ functionsRegistry }) {
29
29
 
30
30
  export const createFormSubmissionHandler = function ({ functionsRegistry, siteUrl }) {
31
31
  return async function formSubmissionHandler(req, res, next) {
32
- if (req.url.startsWith('/.netlify/') || req.method !== 'POST') return next()
32
+ if (
33
+ req.url.startsWith('/.netlify/') ||
34
+ req.method !== 'POST' ||
35
+ (await functionsRegistry.getFunctionForURLPath(req.url, req.method))
36
+ )
37
+ return next()
33
38
 
34
39
  const fakeRequest = new Readable({
35
40
  read() {
@@ -1,4 +1,5 @@
1
1
  // @ts-check
2
+ import { basename, extname } from 'path'
2
3
  import { version as nodeVersion } from 'process'
3
4
 
4
5
  import CronParser from 'cron-parser'
@@ -7,6 +8,7 @@ import semver from 'semver'
7
8
  import { error as errorExit } from '../../utils/command-helpers.mjs'
8
9
  import { BACKGROUND } from '../../utils/functions/get-functions.mjs'
9
10
 
11
+ const TYPESCRIPT_EXTENSIONS = new Set(['.cts', '.mts', '.ts'])
10
12
  const V2_MIN_NODE_VERSION = '18.0.0'
11
13
 
12
14
  // Returns a new set with all elements of `setA` that don't exist in `setB`.
@@ -57,6 +59,35 @@ export default class NetlifyFunction {
57
59
  this.srcFiles = new Set()
58
60
  }
59
61
 
62
+ get filename() {
63
+ if (!this.buildData?.mainFile) {
64
+ return null
65
+ }
66
+
67
+ return basename(this.buildData.mainFile)
68
+ }
69
+
70
+ getRecommendedExtension() {
71
+ if (this.buildData?.runtimeAPIVersion !== 2) {
72
+ return
73
+ }
74
+
75
+ const extension = this.buildData?.mainFile ? extname(this.buildData.mainFile) : undefined
76
+ const moduleFormat = this.buildData?.outputModuleFormat
77
+
78
+ if (moduleFormat === 'esm') {
79
+ return
80
+ }
81
+
82
+ if (extension === '.ts') {
83
+ return '.mts'
84
+ }
85
+
86
+ if (extension === '.js') {
87
+ return '.mjs'
88
+ }
89
+ }
90
+
60
91
  hasValidName() {
61
92
  // same as https://github.com/netlify/bitballoon/blob/fbd7881e6c8e8c48e7a0145da4ee26090c794108/app/models/deploy.rb#L482
62
93
  // eslint-disable-next-line unicorn/better-regex
@@ -73,6 +104,14 @@ export default class NetlifyFunction {
73
104
  return !(this.buildData?.runtimeAPIVersion === 2 && semver.lt(nodeVersion, V2_MIN_NODE_VERSION))
74
105
  }
75
106
 
107
+ isTypeScript() {
108
+ if (this.filename === null) {
109
+ return false
110
+ }
111
+
112
+ return TYPESCRIPT_EXTENSIONS.has(extname(this.filename))
113
+ }
114
+
76
115
  async getNextRun() {
77
116
  if (!(await this.isScheduled())) {
78
117
  return null
@@ -111,7 +150,7 @@ export default class NetlifyFunction {
111
150
  throw new Error(
112
151
  `Function requires Node.js version ${V2_MIN_NODE_VERSION} or above, but ${nodeVersion.slice(
113
152
  1,
114
- )} is installed. Refer to https://ntl.fyi/functions-node18 for information on how to update.`,
153
+ )} is installed. Refer to https://ntl.fyi/functions-runtime for information on how to update.`,
115
154
  )
116
155
  }
117
156
 
@@ -123,6 +162,12 @@ export default class NetlifyFunction {
123
162
  }
124
163
  }
125
164
 
165
+ async getBuildData() {
166
+ await this.buildQueue
167
+
168
+ return this.buildData
169
+ }
170
+
126
171
  // Compares a new set of source files against a previous one, returning an
127
172
  // object with two Sets, one with added and the other with deleted files.
128
173
  getSrcFilesDiff(newSrcFiles) {
@@ -167,7 +212,8 @@ export default class NetlifyFunction {
167
212
  async matchURLPath(rawPath, method) {
168
213
  await this.buildQueue
169
214
 
170
- const path = (rawPath.endsWith('/') ? rawPath.slice(0, -1) : rawPath).toLowerCase()
215
+ let path = rawPath !== '/' && rawPath.endsWith('/') ? rawPath.slice(0, -1) : rawPath
216
+ path = path.toLowerCase()
171
217
  const { routes = [] } = this.buildData
172
218
  return routes.find(({ expression, literal, methods }) => {
173
219
  if (methods.length !== 0 && !methods.includes(method)) {
@@ -188,6 +234,10 @@ export default class NetlifyFunction {
188
234
  })
189
235
  }
190
236
 
237
+ get runtimeAPIVersion() {
238
+ return this.buildData?.runtimeAPIVersion ?? 1
239
+ }
240
+
191
241
  get url() {
192
242
  // This line fixes the issue here https://github.com/netlify/cli/issues/4116
193
243
  // Not sure why `settings.port` was used here nor does a valid reference exist.
@@ -1,12 +1,22 @@
1
1
  // @ts-check
2
2
  import { mkdir } from 'fs/promises'
3
- import { extname, isAbsolute, join, resolve } from 'path'
3
+ import { createRequire } from 'module'
4
+ import { basename, extname, isAbsolute, join, resolve } from 'path'
4
5
  import { env } from 'process'
5
6
 
6
7
  import { listFunctions } from '@netlify/zip-it-and-ship-it'
7
8
  import extractZip from 'extract-zip'
8
9
 
9
- import { chalk, log, NETLIFYDEVERR, NETLIFYDEVLOG, warn, watchDebounced } from '../../utils/command-helpers.mjs'
10
+ import {
11
+ chalk,
12
+ log,
13
+ getTerminalLink,
14
+ NETLIFYDEVERR,
15
+ NETLIFYDEVLOG,
16
+ NETLIFYDEVWARN,
17
+ warn,
18
+ watchDebounced,
19
+ } from '../../utils/command-helpers.mjs'
10
20
  import { INTERNAL_FUNCTIONS_FOLDER, SERVE_FUNCTIONS_FOLDER } from '../../utils/functions/functions.mjs'
11
21
  import { BACKGROUND_FUNCTIONS_WARNING } from '../log.mjs'
12
22
  import { getPathInProject } from '../settings.mjs'
@@ -14,10 +24,25 @@ import { getPathInProject } from '../settings.mjs'
14
24
  import NetlifyFunction from './netlify-function.mjs'
15
25
  import runtimes from './runtimes/index.mjs'
16
26
 
27
+ export const DEFAULT_FUNCTION_URL_EXPRESSION = /^\/.netlify\/(functions|builders)\/([^/]+).*/
28
+ const TYPES_PACKAGE = '@netlify/functions'
17
29
  const ZIP_EXTENSION = '.zip'
18
30
 
31
+ /**
32
+ * @typedef {"buildError" | "extracted" | "loaded" | "missing-types-package" | "reloaded" | "reloading" | "removed"} FunctionEvent
33
+ */
34
+
19
35
  export class FunctionsRegistry {
20
- constructor({ capabilities, config, debug = false, isConnected = false, projectRoot, settings, timeouts }) {
36
+ constructor({
37
+ capabilities,
38
+ config,
39
+ debug = false,
40
+ isConnected = false,
41
+ logLambdaCompat,
42
+ projectRoot,
43
+ settings,
44
+ timeouts,
45
+ }) {
21
46
  this.capabilities = capabilities
22
47
  this.config = config
23
48
  this.debug = debug
@@ -26,25 +51,78 @@ export class FunctionsRegistry {
26
51
  this.timeouts = timeouts
27
52
  this.settings = settings
28
53
 
29
- // An object to be shared among all functions in the registry. It can be
30
- // used to cache the results of the build function — e.g. it's used in
31
- // the `memoizedBuild` method in the JavaScript runtime.
54
+ /**
55
+ * An object to be shared among all functions in the registry. It can be
56
+ * used to cache the results of the build function — e.g. it's used in
57
+ * the `memoizedBuild` method in the JavaScript runtime.
58
+ *
59
+ * @type {Record<string, unknown>}
60
+ */
32
61
  this.buildCache = {}
33
62
 
34
- // File watchers for parent directories where functions live — i.e. the
35
- // ones supplied to `scan()`. This is a Map because in the future we
36
- // might have several function directories.
63
+ /**
64
+ * File watchers for parent directories where functions live i.e. the
65
+ * ones supplied to `scan()`. This is a Map because in the future we
66
+ * might have several function directories.
67
+ *
68
+ * @type {Map<string, Awaited<ReturnType<watchDebounced>>>}
69
+ */
37
70
  this.directoryWatchers = new Map()
38
71
 
39
- // The functions held by the registry. Maps function names to instances of
40
- // `NetlifyFunction`.
72
+ /**
73
+ * The functions held by the registry
74
+ *
75
+ * @type {Map<string, NetlifyFunction>}
76
+ */
41
77
  this.functions = new Map()
42
78
 
43
- // File watchers for function files. Maps function names to objects built
44
- // by the `watchDebounced` utility.
79
+ /**
80
+ * File watchers for function files. Maps function names to objects built
81
+ * by the `watchDebounced` utility.
82
+ *
83
+ * @type {Map<string, Awaited<ReturnType<watchDebounced>>>}
84
+ */
45
85
  this.functionWatchers = new Map()
86
+
87
+ /**
88
+ * Keeps track of whether we've checked whether `TYPES_PACKAGE` is
89
+ * installed.
90
+ */
91
+ this.hasCheckedTypesPackage = false
92
+
93
+ /**
94
+ * Whether to log V1 functions as using the "Lambda compatibility mode"
95
+ *
96
+ * @type {boolean}
97
+ */
98
+ this.logLambdaCompat = Boolean(logLambdaCompat)
99
+ }
100
+
101
+ checkTypesPackage() {
102
+ if (this.hasCheckedTypesPackage) {
103
+ return
104
+ }
105
+
106
+ this.hasCheckedTypesPackage = true
107
+
108
+ const require = createRequire(this.projectRoot)
109
+
110
+ try {
111
+ require.resolve(TYPES_PACKAGE, { paths: [this.projectRoot] })
112
+ } catch (error) {
113
+ if (error?.code === 'MODULE_NOT_FOUND') {
114
+ FunctionsRegistry.logEvent('missing-types-package', {})
115
+ }
116
+ }
46
117
  }
47
118
 
119
+ /**
120
+ * Runs before `scan` and calls any `onDirectoryScan` hooks defined by the
121
+ * runtime before the directory is read. This gives runtime the opportunity
122
+ * to run additional logic when a directory is scanned.
123
+ *
124
+ * @param {string} directory
125
+ */
48
126
  static async prepareDirectoryScan(directory) {
49
127
  await mkdir(directory, { recursive: true })
50
128
 
@@ -62,23 +140,45 @@ export class FunctionsRegistry {
62
140
  )
63
141
  }
64
142
 
143
+ /**
144
+ * Builds a function and sets up the appropriate file watchers so that any
145
+ * changes will trigger another build.
146
+ *
147
+ * @param {NetlifyFunction} func
148
+ * @param {boolean} [firstLoad ]
149
+ * @returns
150
+ */
65
151
  async buildFunctionAndWatchFiles(func, firstLoad = false) {
66
152
  if (!firstLoad) {
67
- log(`${NETLIFYDEVLOG} ${chalk.magenta('Reloading')} function ${chalk.yellow(func.displayName)}...`)
153
+ FunctionsRegistry.logEvent('reloading', { func })
68
154
  }
69
155
 
70
156
  const { error: buildError, includedFiles, srcFilesDiff } = await func.build({ cache: this.buildCache })
71
157
 
72
158
  if (buildError) {
73
- log(
74
- `${NETLIFYDEVERR} ${chalk.red('Failed to load')} function ${chalk.yellow(func.displayName)}: ${
75
- buildError.message
76
- }`,
77
- )
159
+ FunctionsRegistry.logEvent('buildError', { func })
78
160
  } else {
79
- const verb = firstLoad ? 'Loaded' : 'Reloaded'
161
+ const event = firstLoad ? 'loaded' : 'reloaded'
162
+ const recommendedExtension = func.getRecommendedExtension()
163
+
164
+ if (recommendedExtension) {
165
+ const { filename } = func
166
+ const newFilename = filename ? `${basename(filename, extname(filename))}${recommendedExtension}` : null
167
+ const action = newFilename
168
+ ? `rename the function file to ${chalk.underline(
169
+ newFilename,
170
+ )}. Refer to https://ntl.fyi/functions-runtime for more information`
171
+ : `refer to https://ntl.fyi/functions-runtime`
172
+ const warning = `The function is using the legacy CommonJS format. To start using ES modules, ${action}.`
173
+
174
+ FunctionsRegistry.logEvent(event, { func, warnings: [warning] })
175
+ } else {
176
+ FunctionsRegistry.logEvent(event, { func })
177
+ }
178
+ }
80
179
 
81
- log(`${NETLIFYDEVLOG} ${chalk.green(verb)} function ${chalk.yellow(func.displayName)}`)
180
+ if (func.isTypeScript()) {
181
+ this.checkTypesPackage()
82
182
  }
83
183
 
84
184
  // If the build hasn't resulted in any files being added or removed, there
@@ -107,7 +207,6 @@ export class FunctionsRegistry {
107
207
  // we create a new watcher and watch them.
108
208
  if (srcFilesDiff.added.size !== 0) {
109
209
  const filesToWatch = [...srcFilesDiff.added, ...includedFiles]
110
-
111
210
  const newWatcher = await watchDebounced(filesToWatch, {
112
211
  onChange: () => {
113
212
  this.buildFunctionAndWatchFiles(func, false)
@@ -118,11 +217,56 @@ export class FunctionsRegistry {
118
217
  }
119
218
  }
120
219
 
220
+ /**
221
+ * Returns a function by name.
222
+ *
223
+ * @param {string} name
224
+ */
121
225
  get(name) {
122
226
  return this.functions.get(name)
123
227
  }
124
228
 
125
- async getFunctionForURLPath(urlPath, method) {
229
+ /**
230
+ * Looks for the first function that matches a given URL path. If a match is
231
+ * found, returns an object with the function and the route. If the URL path
232
+ * matches the default functions URL (i.e. can only be for a function) but no
233
+ * function with the given name exists, returns an object with the function
234
+ * and the route set to `null`. Otherwise, `undefined` is returned,
235
+ *
236
+ * @param {string} url
237
+ * @param {string} method
238
+ */
239
+ async getFunctionForURLPath(url, method) {
240
+ // We're constructing a URL object just so that we can extract the path from
241
+ // the incoming URL. It doesn't really matter that we don't have the actual
242
+ // local URL with the correct port.
243
+ const urlPath = new URL(url, 'http://localhost').pathname
244
+ const defaultURLMatch = urlPath.match(DEFAULT_FUNCTION_URL_EXPRESSION)
245
+
246
+ if (defaultURLMatch) {
247
+ const func = this.get(defaultURLMatch[2])
248
+
249
+ if (!func) {
250
+ return { func: null, route: null }
251
+ }
252
+
253
+ const { routes = [] } = await func.getBuildData()
254
+
255
+ if (routes.length !== 0) {
256
+ const paths = routes.map((route) => chalk.underline(route.pattern)).join(', ')
257
+
258
+ warn(
259
+ `Function ${chalk.yellow(func.name)} cannot be invoked on ${chalk.underline(
260
+ urlPath,
261
+ )}, because the function has the following URL paths defined: ${paths}`,
262
+ )
263
+
264
+ return
265
+ }
266
+
267
+ return { func, route: null }
268
+ }
269
+
126
270
  for (const func of this.functions.values()) {
127
271
  const route = await func.matchURLPath(urlPath, method)
128
272
 
@@ -132,7 +276,90 @@ export class FunctionsRegistry {
132
276
  }
133
277
  }
134
278
 
135
- async registerFunction(name, funcBeforeHook) {
279
+ /**
280
+ * Logs an event associated with functions.
281
+ *
282
+ * @param {FunctionEvent} event
283
+ * @param {object} data
284
+ * @param {NetlifyFunction} [data.func]
285
+ * @param {string[]} [data.warnings]
286
+ * @returns
287
+ */
288
+ static logEvent(event, { func, warnings = [] }) {
289
+ let warningsText = ''
290
+
291
+ if (warnings.length !== 0) {
292
+ warningsText = ` with warnings:\n${warnings.map((warning) => ` - ${warning}`).join('\n')}`
293
+ }
294
+
295
+ if (event === 'buildError') {
296
+ log(
297
+ `${NETLIFYDEVERR} ${chalk.red('Failed to load')} function ${chalk.yellow(func?.displayName)}: ${
298
+ func?.buildError?.message
299
+ }`,
300
+ )
301
+ }
302
+
303
+ if (event === 'extracted') {
304
+ log(
305
+ `${NETLIFYDEVLOG} ${chalk.green('Extracted')} function ${chalk.yellow(func?.displayName)} from ${
306
+ func?.mainFile
307
+ }.`,
308
+ )
309
+
310
+ return
311
+ }
312
+
313
+ if (event === 'loaded') {
314
+ const icon = warningsText ? NETLIFYDEVWARN : NETLIFYDEVLOG
315
+ const color = warningsText ? chalk.yellow : chalk.green
316
+ const mode =
317
+ func?.runtimeAPIVersion === 1 && this.logLambdaCompat
318
+ ? ` in ${getTerminalLink('Lambda compatibility mode', 'https://ntl.fyi/lambda-compat')}`
319
+ : ''
320
+
321
+ log(`${icon} ${color('Loaded')} function ${chalk.yellow(func?.displayName)}${mode}${warningsText}`)
322
+
323
+ return
324
+ }
325
+
326
+ if (event === 'missing-types-package') {
327
+ log(
328
+ `${NETLIFYDEVWARN} For a better experience with TypeScript functions, consider installing the ${chalk.underline(
329
+ TYPES_PACKAGE,
330
+ )} package. Refer to https://ntl-fyi/function-types for more information.`,
331
+ )
332
+ }
333
+
334
+ if (event === 'reloaded') {
335
+ const icon = warningsText ? NETLIFYDEVWARN : NETLIFYDEVLOG
336
+ const color = warningsText ? chalk.yellow : chalk.green
337
+
338
+ log(`${icon} ${color('Reloaded')} function ${chalk.yellow(func?.displayName)}${warningsText}`)
339
+
340
+ return
341
+ }
342
+
343
+ if (event === 'reloading') {
344
+ log(`${NETLIFYDEVLOG} ${chalk.magenta('Reloading')} function ${chalk.yellow(func?.displayName)}...`)
345
+
346
+ return
347
+ }
348
+
349
+ if (event === 'removed') {
350
+ log(`${NETLIFYDEVLOG} ${chalk.magenta('Removed')} function ${chalk.yellow(func?.displayName)}`)
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Adds a function to the registry
356
+ *
357
+ * @param {string} name
358
+ * @param {NetlifyFunction} funcBeforeHook
359
+ * @param {boolean} [isReload]
360
+ * @returns
361
+ */
362
+ async registerFunction(name, funcBeforeHook, isReload = false) {
136
363
  const { runtime } = funcBeforeHook
137
364
 
138
365
  // The `onRegister` hook allows runtimes to modify the function before it's
@@ -160,7 +387,7 @@ export class FunctionsRegistry {
160
387
  const unzippedDirectory = await this.unzipFunction(func)
161
388
 
162
389
  if (this.debug) {
163
- log(`${NETLIFYDEVLOG} ${chalk.green('Extracted')} function ${chalk.yellow(name)} from ${func.mainFile}.`)
390
+ FunctionsRegistry.logEvent('extracted', { func })
164
391
  }
165
392
 
166
393
  func.mainFile = join(unzippedDirectory, `${func.name}.js`)
@@ -168,15 +395,27 @@ export class FunctionsRegistry {
168
395
 
169
396
  this.functions.set(name, func)
170
397
 
171
- this.buildFunctionAndWatchFiles(func, true)
398
+ this.buildFunctionAndWatchFiles(func, !isReload)
172
399
  }
173
400
 
174
- // This function is here so we can mock it in tests
401
+ /**
402
+ * A proxy to zip-it-and-ship-it's `listFunctions` method. It exists just so
403
+ * that we can mock it in tests.
404
+ * @param {Parameters<listFunctions>} args
405
+ * @returns
406
+ */
175
407
  // eslint-disable-next-line class-methods-use-this
176
408
  async listFunctions(...args) {
177
409
  return await listFunctions(...args)
178
410
  }
179
411
 
412
+ /**
413
+ * Takes a list of directories and scans for functions. It keeps tracks of
414
+ * any functions in those directories that we've previously seen, and takes
415
+ * care of registering and unregistering functions as they come and go.
416
+ *
417
+ * @param {string[]} relativeDirs
418
+ */
180
419
  async scan(relativeDirs) {
181
420
  const directories = relativeDirs.filter(Boolean).map((dir) => (isAbsolute(dir) ? dir : join(this.projectRoot, dir)))
182
421
 
@@ -199,15 +438,16 @@ export class FunctionsRegistry {
199
438
  // the previous list but are missing from the new one. We unregister them.
200
439
  const deletedFunctions = [...this.functions.values()].filter((oldFunc) => {
201
440
  const isFound = functions.some(
202
- (newFunc) => newFunc.name === oldFunc.name && newFunc.runtime === oldFunc.runtime.name,
441
+ (newFunc) => newFunc.name === oldFunc.name && newFunc.mainFile === oldFunc.mainFile,
203
442
  )
204
443
 
205
444
  return !isFound
206
445
  })
207
446
 
208
- await Promise.all(deletedFunctions.map((func) => this.unregisterFunction(func.name)))
447
+ await Promise.all(deletedFunctions.map((func) => this.unregisterFunction(func)))
209
448
 
210
- await Promise.all(
449
+ const deletedFunctionNames = new Set(deletedFunctions.map((func) => func.name))
450
+ const addedFunctions = await Promise.all(
211
451
  // zip-it-and-ship-it returns an array sorted based on which extension should have precedence,
212
452
  // where the last ones precede the previous ones. This is why
213
453
  // we reverse the array so we get the right functions precedence in the CLI.
@@ -238,16 +478,38 @@ export class FunctionsRegistry {
238
478
  settings: this.settings,
239
479
  })
240
480
 
241
- await this.registerFunction(name, func)
481
+ // If a function we're registering was also unregistered in this run,
482
+ // then it was a rename. Let's flag it as such so that the messaging
483
+ // is adjusted accordingly.
484
+ const isReload = deletedFunctionNames.has(name)
485
+
486
+ await this.registerFunction(name, func, isReload)
487
+
488
+ return func
242
489
  }),
243
490
  )
491
+ const addedFunctionNames = new Set(addedFunctions.filter(Boolean).map((func) => func?.name))
492
+
493
+ deletedFunctions.forEach((func) => {
494
+ // If a function we've unregistered was also registered in this run, then
495
+ // it was a rename that we've already logged. Nothing to do in this case.
496
+ if (addedFunctionNames.has(func.name)) {
497
+ return
498
+ }
499
+
500
+ FunctionsRegistry.logEvent('removed', { func })
501
+ })
244
502
 
245
503
  await Promise.all(directories.map((path) => this.setupDirectoryWatcher(path)))
246
504
  }
247
505
 
248
- // This watcher looks at files being added or removed from a functions
249
- // directory. It doesn't care about files being changed, because those
250
- // will be handled by each functions' watcher.
506
+ /**
507
+ * Creates a watcher that looks at files being added or removed from a
508
+ * functions directory. It doesn't care about files being changed, because
509
+ * those will be handled by each functions' watcher.
510
+ *
511
+ * @param {string} directory
512
+ */
251
513
  async setupDirectoryWatcher(directory) {
252
514
  if (this.directoryWatchers.has(directory)) {
253
515
  return
@@ -266,18 +528,30 @@ export class FunctionsRegistry {
266
528
  this.directoryWatchers.set(directory, watcher)
267
529
  }
268
530
 
269
- async unregisterFunction(name) {
270
- this.functions.delete(name)
531
+ /**
532
+ * Removes a function from the registry and closes its file watchers.
533
+ *
534
+ * @param {NetlifyFunction} func
535
+ */
536
+ async unregisterFunction(func) {
537
+ const { name } = func
271
538
 
272
- log(`${NETLIFYDEVLOG} ${chalk.magenta('Removed')} function ${chalk.yellow(name)}.`)
539
+ this.functions.delete(name)
273
540
 
274
541
  const watcher = this.functionWatchers.get(name)
275
542
 
276
543
  if (watcher) {
277
544
  await watcher.close()
278
545
  }
546
+
547
+ this.functionWatchers.delete(name)
279
548
  }
280
549
 
550
+ /**
551
+ * Takes a zipped function and extracts its contents to an internal directory.
552
+ *
553
+ * @param {NetlifyFunction} func
554
+ */
281
555
  async unzipFunction(func) {
282
556
  const targetDirectory = resolve(
283
557
  this.projectRoot,
@@ -57,6 +57,8 @@ const buildFunction = async ({
57
57
  entryFilename,
58
58
  includedFiles,
59
59
  inputs,
60
+ mainFile,
61
+ outputModuleFormat,
60
62
  path: functionPath,
61
63
  routes,
62
64
  runtimeAPIVersion,
@@ -82,7 +84,7 @@ const buildFunction = async ({
82
84
 
83
85
  clearFunctionsCache(targetDirectory)
84
86
 
85
- return { buildPath, includedFiles, routes, runtimeAPIVersion, srcFiles, schedule }
87
+ return { buildPath, includedFiles, outputModuleFormat, mainFile, routes, runtimeAPIVersion, srcFiles, schedule }
86
88
  }
87
89
 
88
90
  /**
@@ -4,7 +4,6 @@ import { pathToFileURL } from 'url'
4
4
  import { Worker } from 'worker_threads'
5
5
 
6
6
  import lambdaLocal from 'lambda-local'
7
- import winston from 'winston'
8
7
 
9
8
  import detectNetlifyLambdaBuilder from './builders/netlify-lambda.mjs'
10
9
  import detectZisiBuilder, { parseFunctionForMetadata } from './builders/zisi.mjs'
@@ -14,12 +13,7 @@ export const name = 'js'
14
13
 
15
14
  let netlifyLambdaDetectorCache
16
15
 
17
- const logger = winston.createLogger({
18
- levels: winston.config.npm.levels,
19
- transports: [new winston.transports.Console({ level: 'warn' })],
20
- })
21
-
22
- lambdaLocal.setLogger(logger)
16
+ lambdaLocal.getLogger().level = 'alert'
23
17
 
24
18
  // The netlify-lambda builder can't be enabled or disabled on a per-function
25
19
  // basis and its detection mechanism is also quite expensive, so we detect