netlify-cli 16.5.1 → 16.6.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": "16.5.1",
4
+ "version": "16.6.0",
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.1",
48
+ "@netlify/build-info": "7.10.0",
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,53 @@ 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
+ * Returns the first function in the registry that matches a given URL path
231
+ * and an HTTP method. If no match is found, `undefined` is returned.
232
+ *
233
+ * @param {string} url
234
+ * @param {string} method
235
+ */
236
+ async getFunctionForURLPath(url, method) {
237
+ // We're constructing a URL object just so that we can extract the path from
238
+ // the incoming URL. It doesn't really matter that we don't have the actual
239
+ // local URL with the correct port.
240
+ const urlPath = new URL(url, 'http://localhost').pathname
241
+ const defaultURLMatch = urlPath.match(DEFAULT_FUNCTION_URL_EXPRESSION)
242
+
243
+ if (defaultURLMatch) {
244
+ const func = this.get(defaultURLMatch[2])
245
+
246
+ if (!func) {
247
+ return
248
+ }
249
+
250
+ const { routes = [] } = await func.getBuildData()
251
+
252
+ if (routes.length !== 0) {
253
+ const paths = routes.map((route) => chalk.underline(route.pattern)).join(', ')
254
+
255
+ warn(
256
+ `Function ${chalk.yellow(func.name)} cannot be invoked on ${chalk.underline(
257
+ urlPath,
258
+ )}, because the function has the following URL paths defined: ${paths}`,
259
+ )
260
+
261
+ return
262
+ }
263
+
264
+ return { func, route: null }
265
+ }
266
+
126
267
  for (const func of this.functions.values()) {
127
268
  const route = await func.matchURLPath(urlPath, method)
128
269
 
@@ -132,7 +273,90 @@ export class FunctionsRegistry {
132
273
  }
133
274
  }
134
275
 
135
- async registerFunction(name, funcBeforeHook) {
276
+ /**
277
+ * Logs an event associated with functions.
278
+ *
279
+ * @param {FunctionEvent} event
280
+ * @param {object} data
281
+ * @param {NetlifyFunction} [data.func]
282
+ * @param {string[]} [data.warnings]
283
+ * @returns
284
+ */
285
+ static logEvent(event, { func, warnings = [] }) {
286
+ let warningsText = ''
287
+
288
+ if (warnings.length !== 0) {
289
+ warningsText = ` with warnings:\n${warnings.map((warning) => ` - ${warning}`).join('\n')}`
290
+ }
291
+
292
+ if (event === 'buildError') {
293
+ log(
294
+ `${NETLIFYDEVERR} ${chalk.red('Failed to load')} function ${chalk.yellow(func?.displayName)}: ${
295
+ func?.buildError?.message
296
+ }`,
297
+ )
298
+ }
299
+
300
+ if (event === 'extracted') {
301
+ log(
302
+ `${NETLIFYDEVLOG} ${chalk.green('Extracted')} function ${chalk.yellow(func?.displayName)} from ${
303
+ func?.mainFile
304
+ }.`,
305
+ )
306
+
307
+ return
308
+ }
309
+
310
+ if (event === 'loaded') {
311
+ const icon = warningsText ? NETLIFYDEVWARN : NETLIFYDEVLOG
312
+ const color = warningsText ? chalk.yellow : chalk.green
313
+ const mode =
314
+ func?.runtimeAPIVersion === 1 && this.logLambdaCompat
315
+ ? ` in ${getTerminalLink('Lambda compatibility mode', 'https://ntl.fyi/lambda-compat')}`
316
+ : ''
317
+
318
+ log(`${icon} ${color('Loaded')} function ${chalk.yellow(func?.displayName)}${mode}${warningsText}`)
319
+
320
+ return
321
+ }
322
+
323
+ if (event === 'missing-types-package') {
324
+ log(
325
+ `${NETLIFYDEVWARN} For a better experience with TypeScript functions, consider installing the ${chalk.underline(
326
+ TYPES_PACKAGE,
327
+ )} package. Refer to https://ntl-fyi/function-types for more information.`,
328
+ )
329
+ }
330
+
331
+ if (event === 'reloaded') {
332
+ const icon = warningsText ? NETLIFYDEVWARN : NETLIFYDEVLOG
333
+ const color = warningsText ? chalk.yellow : chalk.green
334
+
335
+ log(`${icon} ${color('Reloaded')} function ${chalk.yellow(func?.displayName)}${warningsText}`)
336
+
337
+ return
338
+ }
339
+
340
+ if (event === 'reloading') {
341
+ log(`${NETLIFYDEVLOG} ${chalk.magenta('Reloading')} function ${chalk.yellow(func?.displayName)}...`)
342
+
343
+ return
344
+ }
345
+
346
+ if (event === 'removed') {
347
+ log(`${NETLIFYDEVLOG} ${chalk.magenta('Removed')} function ${chalk.yellow(func?.displayName)}`)
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Adds a function to the registry
353
+ *
354
+ * @param {string} name
355
+ * @param {NetlifyFunction} funcBeforeHook
356
+ * @param {boolean} [isReload]
357
+ * @returns
358
+ */
359
+ async registerFunction(name, funcBeforeHook, isReload = false) {
136
360
  const { runtime } = funcBeforeHook
137
361
 
138
362
  // The `onRegister` hook allows runtimes to modify the function before it's
@@ -160,7 +384,7 @@ export class FunctionsRegistry {
160
384
  const unzippedDirectory = await this.unzipFunction(func)
161
385
 
162
386
  if (this.debug) {
163
- log(`${NETLIFYDEVLOG} ${chalk.green('Extracted')} function ${chalk.yellow(name)} from ${func.mainFile}.`)
387
+ FunctionsRegistry.logEvent('extracted', { func })
164
388
  }
165
389
 
166
390
  func.mainFile = join(unzippedDirectory, `${func.name}.js`)
@@ -168,15 +392,27 @@ export class FunctionsRegistry {
168
392
 
169
393
  this.functions.set(name, func)
170
394
 
171
- this.buildFunctionAndWatchFiles(func, true)
395
+ this.buildFunctionAndWatchFiles(func, !isReload)
172
396
  }
173
397
 
174
- // This function is here so we can mock it in tests
398
+ /**
399
+ * A proxy to zip-it-and-ship-it's `listFunctions` method. It exists just so
400
+ * that we can mock it in tests.
401
+ * @param {Parameters<listFunctions>} args
402
+ * @returns
403
+ */
175
404
  // eslint-disable-next-line class-methods-use-this
176
405
  async listFunctions(...args) {
177
406
  return await listFunctions(...args)
178
407
  }
179
408
 
409
+ /**
410
+ * Takes a list of directories and scans for functions. It keeps tracks of
411
+ * any functions in those directories that we've previously seen, and takes
412
+ * care of registering and unregistering functions as they come and go.
413
+ *
414
+ * @param {string[]} relativeDirs
415
+ */
180
416
  async scan(relativeDirs) {
181
417
  const directories = relativeDirs.filter(Boolean).map((dir) => (isAbsolute(dir) ? dir : join(this.projectRoot, dir)))
182
418
 
@@ -199,15 +435,16 @@ export class FunctionsRegistry {
199
435
  // the previous list but are missing from the new one. We unregister them.
200
436
  const deletedFunctions = [...this.functions.values()].filter((oldFunc) => {
201
437
  const isFound = functions.some(
202
- (newFunc) => newFunc.name === oldFunc.name && newFunc.runtime === oldFunc.runtime.name,
438
+ (newFunc) => newFunc.name === oldFunc.name && newFunc.mainFile === oldFunc.mainFile,
203
439
  )
204
440
 
205
441
  return !isFound
206
442
  })
207
443
 
208
- await Promise.all(deletedFunctions.map((func) => this.unregisterFunction(func.name)))
444
+ await Promise.all(deletedFunctions.map((func) => this.unregisterFunction(func)))
209
445
 
210
- await Promise.all(
446
+ const deletedFunctionNames = new Set(deletedFunctions.map((func) => func.name))
447
+ const addedFunctions = await Promise.all(
211
448
  // zip-it-and-ship-it returns an array sorted based on which extension should have precedence,
212
449
  // where the last ones precede the previous ones. This is why
213
450
  // we reverse the array so we get the right functions precedence in the CLI.
@@ -238,16 +475,38 @@ export class FunctionsRegistry {
238
475
  settings: this.settings,
239
476
  })
240
477
 
241
- await this.registerFunction(name, func)
478
+ // If a function we're registering was also unregistered in this run,
479
+ // then it was a rename. Let's flag it as such so that the messaging
480
+ // is adjusted accordingly.
481
+ const isReload = deletedFunctionNames.has(name)
482
+
483
+ await this.registerFunction(name, func, isReload)
484
+
485
+ return func
242
486
  }),
243
487
  )
488
+ const addedFunctionNames = new Set(addedFunctions.filter(Boolean).map((func) => func?.name))
489
+
490
+ deletedFunctions.forEach((func) => {
491
+ // If a function we've unregistered was also registered in this run, then
492
+ // it was a rename that we've already logged. Nothing to do in this case.
493
+ if (addedFunctionNames.has(func.name)) {
494
+ return
495
+ }
496
+
497
+ FunctionsRegistry.logEvent('removed', { func })
498
+ })
244
499
 
245
500
  await Promise.all(directories.map((path) => this.setupDirectoryWatcher(path)))
246
501
  }
247
502
 
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.
503
+ /**
504
+ * Creates a watcher that looks at files being added or removed from a
505
+ * functions directory. It doesn't care about files being changed, because
506
+ * those will be handled by each functions' watcher.
507
+ *
508
+ * @param {string} directory
509
+ */
251
510
  async setupDirectoryWatcher(directory) {
252
511
  if (this.directoryWatchers.has(directory)) {
253
512
  return
@@ -266,18 +525,30 @@ export class FunctionsRegistry {
266
525
  this.directoryWatchers.set(directory, watcher)
267
526
  }
268
527
 
269
- async unregisterFunction(name) {
270
- this.functions.delete(name)
528
+ /**
529
+ * Removes a function from the registry and closes its file watchers.
530
+ *
531
+ * @param {NetlifyFunction} func
532
+ */
533
+ async unregisterFunction(func) {
534
+ const { name } = func
271
535
 
272
- log(`${NETLIFYDEVLOG} ${chalk.magenta('Removed')} function ${chalk.yellow(name)}.`)
536
+ this.functions.delete(name)
273
537
 
274
538
  const watcher = this.functionWatchers.get(name)
275
539
 
276
540
  if (watcher) {
277
541
  await watcher.close()
278
542
  }
543
+
544
+ this.functionWatchers.delete(name)
279
545
  }
280
546
 
547
+ /**
548
+ * Takes a zipped function and extracts its contents to an internal directory.
549
+ *
550
+ * @param {NetlifyFunction} func
551
+ */
281
552
  async unzipFunction(func) {
282
553
  const targetDirectory = resolve(
283
554
  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