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.
@@ -1,12 +1,19 @@
1
1
  // @ts-check
2
2
  import { Buffer } from 'buffer'
3
+ import { promises as fs } from 'fs'
3
4
 
4
5
  import express from 'express'
5
6
  import expressLogging from 'express-logging'
6
7
  import jwtDecode from 'jwt-decode'
7
8
 
8
9
  import { NETLIFYDEVERR, NETLIFYDEVLOG, error as errorExit, log } from '../../utils/command-helpers.mjs'
9
- import { CLOCKWORK_USERAGENT, getFunctionsDistPath, getInternalFunctionsDir } from '../../utils/functions/index.mjs'
10
+ import { isFeatureFlagEnabled } from '../../utils/feature-flags.mjs'
11
+ import {
12
+ CLOCKWORK_USERAGENT,
13
+ getFunctionsDistPath,
14
+ getFunctionsServePath,
15
+ getInternalFunctionsDir,
16
+ } from '../../utils/functions/index.mjs'
10
17
  import { NFFunctionName, NFFunctionRoute } from '../../utils/headers.mjs'
11
18
  import { headers as efHeaders } from '../edge-functions/headers.mjs'
12
19
  import { getGeoLocation } from '../geo-location.mjs'
@@ -244,12 +251,14 @@ const getFunctionsServer = (options) => {
244
251
  * @param {*} options.loadDistFunctions
245
252
  * @param {*} options.settings
246
253
  * @param {*} options.site
254
+ * @param {*} options.siteInfo
247
255
  * @param {string} options.siteUrl
248
256
  * @param {*} options.timeouts
249
257
  * @returns {Promise<import('./registry.mjs').FunctionsRegistry | undefined>}
250
258
  */
251
259
  export const startFunctionsServer = async (options) => {
252
- const { capabilities, command, config, debug, loadDistFunctions, settings, site, siteUrl, timeouts } = options
260
+ const { capabilities, command, config, debug, loadDistFunctions, settings, site, siteInfo, siteUrl, timeouts } =
261
+ options
253
262
  const internalFunctionsDir = await getInternalFunctionsDir({ base: site.root })
254
263
  const functionsDirectories = []
255
264
 
@@ -270,6 +279,14 @@ export const startFunctionsServer = async (options) => {
270
279
  functionsDirectories.push(...sourceDirectories)
271
280
  }
272
281
 
282
+ try {
283
+ const functionsServePath = getFunctionsServePath({ base: site.root })
284
+
285
+ await fs.rm(functionsServePath, { force: true, recursive: true })
286
+ } catch {
287
+ // no-op
288
+ }
289
+
273
290
  if (functionsDirectories.length === 0) {
274
291
  return
275
292
  }
@@ -279,6 +296,7 @@ export const startFunctionsServer = async (options) => {
279
296
  config,
280
297
  debug,
281
298
  isConnected: Boolean(siteUrl),
299
+ logLambdaCompat: isFeatureFlagEnabled('cli_log_lambda_compat', siteInfo),
282
300
  // functions always need to be inside the packagePath if set inside a monorepo
283
301
  projectRoot: command.workingDir,
284
302
  settings,
@@ -3,11 +3,18 @@ import { Buffer } from 'buffer'
3
3
 
4
4
  import { isStream } from 'is-stream'
5
5
 
6
- import { chalk, log, NETLIFYDEVERR } from '../../utils/command-helpers.mjs'
6
+ import { chalk, logPadded, NETLIFYDEVERR } from '../../utils/command-helpers.mjs'
7
7
  import renderErrorTemplate from '../render-error-template.mjs'
8
8
 
9
9
  import { detectAwsSdkError } from './utils.mjs'
10
10
 
11
+ /**
12
+ * @typedef InvocationError
13
+ * @property {string} errorType
14
+ * @property {string} errorMessage
15
+ * @property {Array<string>} stackTrace
16
+ */
17
+
11
18
  const addHeaders = (headers, response) => {
12
19
  if (!headers) {
13
20
  return
@@ -26,12 +33,21 @@ export const handleSynchronousFunction = function ({
26
33
  result,
27
34
  }) {
28
35
  if (invocationError) {
36
+ const error = getNormalizedError(invocationError)
37
+
38
+ logPadded(
39
+ `${NETLIFYDEVERR} Function ${chalk.yellow(functionName)} has returned an error: ${
40
+ error.errorMessage
41
+ }\n${chalk.dim(error.stackTrace.join('\n'))}`,
42
+ )
43
+
29
44
  return handleErr(invocationError, request, response)
30
45
  }
31
46
 
32
47
  const { error } = validateLambdaResponse(result)
33
48
  if (error) {
34
- log(`${NETLIFYDEVERR} ${error}`)
49
+ logPadded(`${NETLIFYDEVERR} ${error}`)
50
+
35
51
  return handleErr(error, request, response)
36
52
  }
37
53
 
@@ -41,9 +57,13 @@ export const handleSynchronousFunction = function ({
41
57
  addHeaders(result.headers, response)
42
58
  addHeaders(result.multiValueHeaders, response)
43
59
  } catch (headersError) {
44
- formatError(headersError)
60
+ const normalizedError = getNormalizedError(headersError)
45
61
 
46
- log(`${NETLIFYDEVERR} Failed to set header in function ${chalk.yellow(functionName)}: ${headersError.message}`)
62
+ logPadded(
63
+ `${NETLIFYDEVERR} Failed to set header in function ${chalk.yellow(functionName)}: ${
64
+ normalizedError.errorMessage
65
+ }`,
66
+ )
47
67
 
48
68
  return handleErr(headersError, request, response)
49
69
  }
@@ -60,20 +80,56 @@ export const handleSynchronousFunction = function ({
60
80
  response.end()
61
81
  }
62
82
 
63
- const formatError = (err) => {
64
- err.errorType = err.code
65
- err.errorMessage = err.message
66
- err.stackTrace = err.trace
83
+ /**
84
+ * Accepts an error generated by `lambda-local` or an instance of `Error` and
85
+ * returns a normalized error that we can treat in the same way.
86
+ *
87
+ * @param {InvocationError|Error} error
88
+ * @returns {InvocationError}
89
+ */
90
+ const getNormalizedError = (error) => {
91
+ if (error instanceof Error) {
92
+ const normalizedError = {
93
+ errorMessage: error.message,
94
+ errorType: error.name,
95
+ stackTrace: error.stack ? error.stack.split('\n') : [],
96
+ }
97
+
98
+ if ('code' in error && error.code === 'ERR_REQUIRE_ESM') {
99
+ return {
100
+ ...normalizedError,
101
+ errorMessage:
102
+ 'a CommonJS file cannot import ES modules. Consider switching your function to ES modules. For more information, refer to https://ntl.fyi/functions-runtime.',
103
+ }
104
+ }
105
+
106
+ return normalizedError
107
+ }
108
+
109
+ // Formatting stack trace lines in the same way that Node.js formats native
110
+ // errors.
111
+ const stackTrace = error.stackTrace.map((line) => ` at ${line}`)
112
+
113
+ return {
114
+ errorType: error.errorType,
115
+ errorMessage: error.errorMessage,
116
+ stackTrace,
117
+ }
67
118
  }
68
119
 
69
- const formatLambdaLocalError = (err, acceptsHtml) =>
70
- acceptsHtml
71
- ? JSON.stringify({
72
- errorType: err.errorType,
73
- errorMessage: err.errorMessage,
74
- trace: err.stackTrace,
75
- })
76
- : `${err.errorType}: ${err.errorMessage}\n ${err.stackTrace?.join('\n ')}`
120
+ const formatLambdaLocalError = (rawError, acceptsHTML) => {
121
+ const error = getNormalizedError(rawError)
122
+
123
+ if (acceptsHTML) {
124
+ return JSON.stringify({
125
+ ...error,
126
+ stackTrace: undefined,
127
+ trace: error.stackTrace,
128
+ })
129
+ }
130
+
131
+ return `${error.errorType}: ${error.errorMessage}\n ${error.stackTrace.join('\n')}`
132
+ }
77
133
 
78
134
  const handleErr = async (err, request, response) => {
79
135
  detectAwsSdkError({ err })
@@ -163,6 +163,12 @@ export const log = (message = '', ...args) => {
163
163
  process.stdout.write(`${format(message, ...args)}\n`)
164
164
  }
165
165
 
166
+ export const logPadded = (message = '', ...args) => {
167
+ log('')
168
+ log(message, ...args)
169
+ log('')
170
+ }
171
+
166
172
  /**
167
173
  * logs a warning message
168
174
  * @param {string} message
@@ -262,4 +268,4 @@ export const watchDebounced = async (
262
268
  return watcher
263
269
  }
264
270
 
265
- export const getTerminalLink = (text, url) => terminalLink(text, url, { fallback: () => `${text} ${url}` })
271
+ export const getTerminalLink = (text, url) => terminalLink(text, url, { fallback: () => `${text} (${url})` })
@@ -33,6 +33,12 @@ export const getFunctionsDistPath = async ({ base, packagePath = '' }) => {
33
33
  return isDirectory ? path : null
34
34
  }
35
35
 
36
+ export const getFunctionsServePath = ({ base, packagePath = '' }) => {
37
+ const path = resolve(base, packagePath, getPathInProject([SERVE_FUNCTIONS_FOLDER]))
38
+
39
+ return path
40
+ }
41
+
36
42
  /**
37
43
  * Retrieves the internal functions directory and creates it if ensureExists is provided
38
44
  * @param {object} config
@@ -27,6 +27,7 @@ import {
27
27
  isEdgeFunctionsRequest,
28
28
  } from '../lib/edge-functions/proxy.mjs'
29
29
  import { fileExistsAsync, isFileAsync } from '../lib/fs.mjs'
30
+ import { DEFAULT_FUNCTION_URL_EXPRESSION } from '../lib/functions/registry.mjs'
30
31
  import renderErrorTemplate from '../lib/render-error-template.mjs'
31
32
 
32
33
  import { NETLIFYDEVLOG, NETLIFYDEVWARN, log, chalk } from './command-helpers.mjs'
@@ -87,7 +88,7 @@ function isInternal(url) {
87
88
  * @param {string} url
88
89
  */
89
90
  function isFunction(functionsPort, url) {
90
- return functionsPort && url.match(/^\/.netlify\/(functions|builders)\/.+/)
91
+ return functionsPort && url.match(DEFAULT_FUNCTION_URL_EXPRESSION)
91
92
  }
92
93
 
93
94
  /**
@@ -328,13 +329,12 @@ const serveRedirect = async function ({ env, functionsRegistry, match, options,
328
329
  return proxy.web(req, res, { target: options.functionsServer })
329
330
  }
330
331
 
331
- const functionWithCustomRoute =
332
- functionsRegistry && (await functionsRegistry.getFunctionForURLPath(destURL, req.method))
332
+ const matchingFunction = functionsRegistry && (await functionsRegistry.getFunctionForURLPath(destURL, req.method))
333
333
  const destStaticFile = await getStatic(dest.pathname, options.publicFolder)
334
334
  let statusValue
335
335
  if (
336
336
  match.force ||
337
- (!staticFile && ((!options.framework && destStaticFile) || isInternal(destURL) || functionWithCustomRoute))
337
+ (!staticFile && ((!options.framework && destStaticFile) || isInternal(destURL) || matchingFunction))
338
338
  ) {
339
339
  req.url = destStaticFile ? destStaticFile + dest.search : destURL
340
340
  const { status } = match
@@ -342,10 +342,11 @@ const serveRedirect = async function ({ env, functionsRegistry, match, options,
342
342
  console.log(`${NETLIFYDEVLOG} Rewrote URL to`, req.url)
343
343
  }
344
344
 
345
- if (isFunction(options.functionsPort, req.url) || functionWithCustomRoute) {
346
- const functionHeaders = functionWithCustomRoute
347
- ? { [NFFunctionName]: functionWithCustomRoute.func.name, [NFFunctionRoute]: functionWithCustomRoute.route }
348
- : {}
345
+ if (matchingFunction) {
346
+ const functionHeaders = {
347
+ [NFFunctionName]: matchingFunction.func.name,
348
+ [NFFunctionRoute]: matchingFunction.route,
349
+ }
349
350
  const url = reqToURL(req, originalURL)
350
351
  req.headers['x-netlify-original-pathname'] = url.pathname
351
352
  req.headers['x-netlify-original-search'] = url.search
@@ -597,18 +598,17 @@ const onRequest = async (
597
598
  return proxy.web(req, res, { target: edgeFunctionsProxyURL })
598
599
  }
599
600
 
600
- // Does the request match a function on the fixed URL path?
601
- if (isFunction(settings.functionsPort, req.url)) {
602
- return proxy.web(req, res, { target: functionsServer })
603
- }
604
-
605
- // Does the request match a function on a custom URL path?
606
- const functionMatch = functionsRegistry ? await functionsRegistry.getFunctionForURLPath(req.url, req.method) : null
601
+ const functionMatch = functionsRegistry && (await functionsRegistry.getFunctionForURLPath(req.url, req.method))
607
602
 
608
603
  if (functionMatch) {
609
604
  // Setting an internal header with the function name so that we don't
610
605
  // have to match the URL again in the functions server.
611
- const headers = { [NFFunctionName]: functionMatch.func.name, [NFFunctionRoute]: functionMatch.route.pattern }
606
+ /** @type {Record<string, string>} */
607
+ const headers = { [NFFunctionName]: functionMatch.func.name }
608
+
609
+ if (functionMatch.route) {
610
+ headers[NFFunctionRoute] = functionMatch.route.pattern
611
+ }
612
612
 
613
613
  return proxy.web(req, res, { headers, target: functionsServer })
614
614
  }