netlify-cli 15.9.1 → 15.10.0-rc.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.
Files changed (43) hide show
  1. package/bin/run.mjs +6 -5
  2. package/npm-shrinkwrap.json +369 -266
  3. package/package.json +8 -9
  4. package/src/commands/base-command.mjs +295 -116
  5. package/src/commands/build/build.mjs +9 -1
  6. package/src/commands/deploy/deploy.mjs +22 -9
  7. package/src/commands/dev/dev.mjs +22 -17
  8. package/src/commands/functions/functions-create.mjs +118 -89
  9. package/src/commands/functions/functions-invoke.mjs +10 -7
  10. package/src/commands/functions/functions-list.mjs +2 -2
  11. package/src/commands/init/init.mjs +1 -1
  12. package/src/commands/link/link.mjs +5 -5
  13. package/src/commands/serve/serve.mjs +10 -6
  14. package/src/commands/sites/sites-create-template.mjs +1 -1
  15. package/src/commands/sites/sites-create.mjs +1 -1
  16. package/src/functions-templates/typescript/hello-world/package-lock.json +6 -6
  17. package/src/lib/edge-functions/bootstrap.mjs +1 -1
  18. package/src/lib/edge-functions/headers.mjs +1 -0
  19. package/src/lib/edge-functions/internal.mjs +5 -3
  20. package/src/lib/edge-functions/proxy.mjs +29 -4
  21. package/src/lib/functions/runtimes/js/index.mjs +1 -1
  22. package/src/lib/functions/runtimes/js/worker.mjs +1 -1
  23. package/src/lib/functions/server.mjs +3 -2
  24. package/src/lib/spinner.mjs +1 -1
  25. package/src/recipes/vscode/index.mjs +24 -6
  26. package/src/utils/build-info.mjs +100 -0
  27. package/src/utils/command-helpers.mjs +16 -7
  28. package/src/utils/detect-server-settings.mjs +133 -245
  29. package/src/utils/framework-server.mjs +6 -5
  30. package/src/utils/functions/functions.mjs +8 -5
  31. package/src/utils/get-repo-data.mjs +5 -6
  32. package/src/utils/init/config-github.mjs +2 -2
  33. package/src/utils/init/config-manual.mjs +24 -7
  34. package/src/utils/init/utils.mjs +62 -63
  35. package/src/utils/proxy-server.mjs +7 -4
  36. package/src/utils/proxy.mjs +4 -3
  37. package/src/utils/read-repo-url.mjs +4 -0
  38. package/src/utils/run-build.mjs +58 -32
  39. package/src/utils/shell.mjs +24 -7
  40. package/src/utils/state-config.mjs +5 -1
  41. package/src/utils/static-server.mjs +4 -0
  42. package/src/utils/telemetry/report-error.mjs +8 -4
  43. package/src/utils/init/frameworks.mjs +0 -23
@@ -11,11 +11,11 @@ import { track } from '../../utils/telemetry/index.mjs'
11
11
 
12
12
  /**
13
13
  *
14
- * @param {import('../base-command.mjs').NetlifyOptions} netlify
14
+ * @param {import('../base-command.mjs').default} command
15
15
  * @param {import('commander').OptionValues} options
16
16
  */
17
- const linkPrompt = async (netlify, options) => {
18
- const { api, state } = netlify
17
+ const linkPrompt = async (command, options) => {
18
+ const { api, state } = command.netlify
19
19
 
20
20
  const SITE_NAME_PROMPT = 'Search by full or partial site name'
21
21
  const SITE_LIST_PROMPT = 'Choose from a list of your recently updated sites'
@@ -24,7 +24,7 @@ const linkPrompt = async (netlify, options) => {
24
24
  let GIT_REMOTE_PROMPT = 'Use the current git remote origin URL'
25
25
  let site
26
26
  // Get git remote data if exists
27
- const repoData = await getRepoData({ remoteName: options.gitRemoteName })
27
+ const repoData = await getRepoData({ workingDir: command.workingDir, remoteName: options.gitRemoteName })
28
28
 
29
29
  let linkChoices = [SITE_NAME_PROMPT, SITE_LIST_PROMPT, SITE_ID_PROMPT]
30
30
 
@@ -326,7 +326,7 @@ export const link = async (options, command) => {
326
326
  kind: 'byName',
327
327
  })
328
328
  } else {
329
- siteData = await linkPrompt(command.netlify, options)
329
+ siteData = await linkPrompt(command, options)
330
330
  }
331
331
  return siteData
332
332
  }
@@ -38,6 +38,7 @@ const serve = async (options, command) => {
38
38
  const devConfig = {
39
39
  ...(config.functionsDirectory && { functions: config.functionsDirectory }),
40
40
  ...(config.build.publish && { publish: config.build.publish }),
41
+
41
42
  ...config.dev,
42
43
  ...options,
43
44
  // Override the `framework` value so that we start a static server and not
@@ -69,10 +70,9 @@ const serve = async (options, command) => {
69
70
  // Netlify Build are loaded.
70
71
  await getInternalFunctionsDir({ base: site.root, ensureExists: true })
71
72
 
72
- /** @type {Partial<import('../../utils/types').ServerSettings>} */
73
- let settings = {}
73
+ let settings = /** @type {import('../../utils/types.js').ServerSettings} */ ({})
74
74
  try {
75
- settings = await detectServerSettings(devConfig, options, site.root)
75
+ settings = await detectServerSettings(devConfig, options, command)
76
76
 
77
77
  cachedConfig.config = getConfigWithPlugins(cachedConfig.config, settings)
78
78
  } catch (error_) {
@@ -87,7 +87,11 @@ const serve = async (options, command) => {
87
87
  `${NETLIFYDEVWARN} Changes will not be hot-reloaded, so if you need to rebuild your site you must exit and run 'netlify serve' again`,
88
88
  )
89
89
 
90
- const { configPath: configPathOverride } = await runBuildTimeline({ cachedConfig, options, settings, site })
90
+ const { configPath: configPathOverride } = await runBuildTimeline({
91
+ command,
92
+ settings,
93
+ options,
94
+ })
91
95
 
92
96
  await startFunctionsServer({
93
97
  api,
@@ -117,8 +121,7 @@ const serve = async (options, command) => {
117
121
 
118
122
  // TODO: We should consolidate this with the existing config watcher.
119
123
  const getUpdatedConfig = async () => {
120
- const cwd = options.cwd || process.cwd()
121
- const { config: newConfig } = await command.getConfig({ cwd, offline: true, state })
124
+ const { config: newConfig } = await command.getConfig({ cwd: command.workingDir, offline: true, state })
122
125
  const normalizedNewConfig = normalizeConfig(newConfig)
123
126
 
124
127
  return normalizedNewConfig
@@ -135,6 +138,7 @@ const serve = async (options, command) => {
135
138
  getUpdatedConfig,
136
139
  inspectSettings,
137
140
  offline: options.offline,
141
+ projectDir: command.workingDir,
138
142
  settings,
139
143
  site,
140
144
  siteInfo,
@@ -197,7 +197,7 @@ const sitesCreateTemplate = async (repository, options, command) => {
197
197
 
198
198
  if (options.withCi) {
199
199
  log('Configuring CI')
200
- const repoData = await getRepoData()
200
+ const repoData = await getRepoData({ workingDir: command.workingDir })
201
201
  await configureRepo({ command, siteId: site.id, repoData, manual: options.manual })
202
202
  }
203
203
 
@@ -102,7 +102,7 @@ export const sitesCreate = async (options, command) => {
102
102
 
103
103
  if (options.withCi) {
104
104
  log('Configuring CI')
105
- const repoData = await getRepoData()
105
+ const repoData = await getRepoData({ workingDir: command.workingDir })
106
106
  await configureRepo({ command, siteId: site.id, repoData, manual: options.manual })
107
107
  }
108
108
 
@@ -26,9 +26,9 @@
26
26
  }
27
27
  },
28
28
  "node_modules/@types/node": {
29
- "version": "14.18.53",
30
- "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.53.tgz",
31
- "integrity": "sha512-soGmOpVBUq+gaBMwom1M+krC/NNbWlosh4AtGA03SyWNDiqSKtwp7OulO1M6+mg8YkHMvJ/y0AkCeO8d1hNb7A=="
29
+ "version": "14.18.54",
30
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.54.tgz",
31
+ "integrity": "sha512-uq7O52wvo2Lggsx1x21tKZgqkJpvwCseBBPtX/nKQfpVlEsLOb11zZ1CRsWUKvJF0+lzuA9jwvA7Pr2Wt7i3xw=="
32
32
  },
33
33
  "node_modules/is-promise": {
34
34
  "version": "4.0.0",
@@ -58,9 +58,9 @@
58
58
  }
59
59
  },
60
60
  "@types/node": {
61
- "version": "14.18.53",
62
- "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.53.tgz",
63
- "integrity": "sha512-soGmOpVBUq+gaBMwom1M+krC/NNbWlosh4AtGA03SyWNDiqSKtwp7OulO1M6+mg8YkHMvJ/y0AkCeO8d1hNb7A=="
61
+ "version": "14.18.54",
62
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.54.tgz",
63
+ "integrity": "sha512-uq7O52wvo2Lggsx1x21tKZgqkJpvwCseBBPtX/nKQfpVlEsLOb11zZ1CRsWUKvJF0+lzuA9jwvA7Pr2Wt7i3xw=="
64
64
  },
65
65
  "is-promise": {
66
66
  "version": "4.0.0",
@@ -1,5 +1,5 @@
1
1
  import { env } from 'process'
2
2
 
3
- const latestBootstrapURL = 'https://6494585a67d46e0008867e60--edge.netlify.com/bootstrap/index-combined.ts'
3
+ const latestBootstrapURL = 'https://64c264287e9cbb0008621df3--edge.netlify.com/bootstrap/index-combined.ts'
4
4
 
5
5
  export const getBootstrapURL = () => env.NETLIFY_EDGE_BOOTSTRAP || latestBootstrapURL
@@ -2,6 +2,7 @@
2
2
  import { Buffer } from 'buffer'
3
3
 
4
4
  export const headers = {
5
+ DeployID: 'x-nf-deploy-id',
5
6
  FeatureFlags: 'x-nf-feature-flags',
6
7
  ForwardedHost: 'x-forwarded-host',
7
8
  Functions: 'x-nf-edge-functions',
@@ -1,14 +1,16 @@
1
1
  // @ts-check
2
2
  import { readFile, stat } from 'fs/promises'
3
3
  import { dirname, join, resolve } from 'path'
4
- import { cwd } from 'process'
5
4
 
6
5
  import { getPathInProject } from '../settings.mjs'
7
6
 
8
7
  import { INTERNAL_EDGE_FUNCTIONS_FOLDER } from './consts.mjs'
9
8
 
10
- export const getInternalFunctions = async () => {
11
- const path = join(cwd(), getPathInProject([INTERNAL_EDGE_FUNCTIONS_FOLDER]))
9
+ /**
10
+ * @param {string} workingDir
11
+ */
12
+ export const getInternalFunctions = async (workingDir) => {
13
+ const path = join(workingDir, getPathInProject([INTERNAL_EDGE_FUNCTIONS_FOLDER]))
12
14
 
13
15
  try {
14
16
  const stats = await stat(path)
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
  import { Buffer } from 'buffer'
3
3
  import { relative } from 'path'
4
- import { cwd, env } from 'process'
4
+ import { env } from 'process'
5
5
 
6
6
  // eslint-disable-next-line import/no-namespace
7
7
  import * as bundler from '@netlify/edge-bundler'
@@ -62,6 +62,26 @@ export const createAccountInfoHeader = (accountInfo = {}) => {
62
62
  return Buffer.from(accountString).toString('base64')
63
63
  }
64
64
 
65
+ /**
66
+ *
67
+ * @param {object} config
68
+ * @param {*} config.accountId
69
+ * @param {*} config.config
70
+ * @param {*} config.configPath
71
+ * @param {*} config.debug
72
+ * @param {*} config.env
73
+ * @param {*} config.geoCountry
74
+ * @param {*} config.geolocationMode
75
+ * @param {*} config.getUpdatedConfig
76
+ * @param {*} config.inspectSettings
77
+ * @param {*} config.mainPort
78
+ * @param {boolean=} config.offline
79
+ * @param {*} config.passthroughPort
80
+ * @param {*} config.projectDir
81
+ * @param {*} config.siteInfo
82
+ * @param {*} config.state
83
+ * @returns
84
+ */
65
85
  export const initializeProxy = async ({
66
86
  accountId,
67
87
  config,
@@ -79,7 +99,11 @@ export const initializeProxy = async ({
79
99
  siteInfo,
80
100
  state,
81
101
  }) => {
82
- const { functions: internalFunctions, importMap, path: internalFunctionsPath } = await getInternalFunctions()
102
+ const {
103
+ functions: internalFunctions,
104
+ importMap,
105
+ path: internalFunctionsPath,
106
+ } = await getInternalFunctions(projectDir)
83
107
  const userFunctionsPath = config.build.edge_functions
84
108
  const isolatePort = await getAvailablePort()
85
109
 
@@ -114,7 +138,8 @@ export const initializeProxy = async ({
114
138
  if (!registry) return
115
139
 
116
140
  // Setting header with geolocation and site info.
117
- req.headers[headers.Geo] = JSON.stringify(geoLocation)
141
+ req.headers[headers.Geo] = Buffer.from(JSON.stringify(geoLocation)).toString('base64')
142
+ req.headers[headers.DeployID] = '0'
118
143
  req.headers[headers.Site] = createSiteInfoHeader(siteInfo)
119
144
  req.headers[headers.Account] = createAccountInfoHeader({ id: accountId })
120
145
 
@@ -132,7 +157,7 @@ export const initializeProxy = async ({
132
157
  )} matches declaration for edge function ${chalk.yellow(
133
158
  functionName,
134
159
  )}, but there's no matching function file in ${chalk.yellow(
135
- relative(cwd(), userFunctionsPath),
160
+ relative(projectDir, userFunctionsPath),
136
161
  )}. Please visit ${chalk.blue('https://ntl.fyi/edge-create')} for more information.`,
137
162
  )
138
163
  })
@@ -3,7 +3,7 @@ import { dirname } from 'path'
3
3
  import { pathToFileURL } from 'url'
4
4
  import { Worker } from 'worker_threads'
5
5
 
6
- import lambdaLocal from '@skn0tt/lambda-local'
6
+ import lambdaLocal from 'lambda-local'
7
7
  import winston from 'winston'
8
8
 
9
9
  import detectNetlifyLambdaBuilder from './builders/netlify-lambda.mjs'
@@ -1,8 +1,8 @@
1
1
  import { createServer } from 'net'
2
2
  import { isMainThread, workerData, parentPort } from 'worker_threads'
3
3
 
4
- import lambdaLocal from '@skn0tt/lambda-local'
5
4
  import { isStream } from 'is-stream'
5
+ import lambdaLocal from 'lambda-local'
6
6
  import sourceMapSupport from 'source-map-support'
7
7
 
8
8
  if (isMainThread) {
@@ -1,4 +1,6 @@
1
1
  // @ts-check
2
+ import { Buffer } from 'buffer'
3
+
2
4
  import express from 'express'
3
5
  import expressLogging from 'express-logging'
4
6
  import jwtDecode from 'jwt-decode'
@@ -47,7 +49,6 @@ const hasBody = (req) =>
47
49
  // eslint-disable-next-line unicorn/prefer-number-properties
48
50
  (req.header('transfer-encoding') !== undefined || !isNaN(req.header('content-length'))) &&
49
51
  // we expect a string or a buffer, because we use the two bodyParsers(text, raw) from express
50
- // eslint-disable-next-line n/prefer-global/buffer
51
52
  (typeof req.body === 'string' || Buffer.isBuffer(req.body))
52
53
 
53
54
  export const createHandler = function (options) {
@@ -114,7 +115,7 @@ export const createHandler = function (options) {
114
115
  'client-ip': [remoteAddress],
115
116
  'x-nf-client-connection-ip': [remoteAddress],
116
117
  'x-nf-account-id': [options.accountId],
117
- [efHeaders.Geo]: JSON.stringify(geoLocation),
118
+ [efHeaders.Geo]: Buffer.from(JSON.stringify(geoLocation)).toString('base64'),
118
119
  }).reduce((prev, [key, value]) => ({ ...prev, [key]: Array.isArray(value) ? value : [value] }), {})
119
120
  const rawQuery = new URLSearchParams(requestQuery).toString()
120
121
  const protocol = options.config?.dev?.https ? 'https' : 'http'
@@ -17,7 +17,7 @@ export const startSpinner = ({ text }) =>
17
17
  * Stops the spinner with the following text
18
18
  * @param {object} config
19
19
  * @param {ora.Ora} config.spinner
20
- * @param {object} [config.error]
20
+ * @param {boolean} [config.error]
21
21
  * @param {string} [config.text]
22
22
  * @returns {void}
23
23
  */
@@ -1,3 +1,4 @@
1
+ // @ts-check
1
2
  import { join } from 'path'
2
3
 
3
4
  import { DenoBridge } from '@netlify/edge-bundler'
@@ -27,15 +28,24 @@ const getPrompt = ({ fileExists, path }) => {
27
28
  const getEdgeFunctionsPath = ({ config, repositoryRoot }) =>
28
29
  config.build.edge_functions || join(repositoryRoot, 'netlify', 'edge-functions')
29
30
 
31
+ /**
32
+ * @param {string} repositoryRoot
33
+ */
30
34
  const getSettingsPath = (repositoryRoot) => join(repositoryRoot, '.vscode', 'settings.json')
31
35
 
32
- const hasDenoVSCodeExt = async () => {
33
- const { stdout: extensions } = await execa('code', ['--list-extensions'], { stderr: 'inherit' })
36
+ /**
37
+ * @param {string} repositoryRoot
38
+ */
39
+ const hasDenoVSCodeExt = async (repositoryRoot) => {
40
+ const { stdout: extensions } = await execa('code', ['--list-extensions'], { stderr: 'inherit', cwd: repositoryRoot })
34
41
  return extensions.split('\n').includes('denoland.vscode-deno')
35
42
  }
36
43
 
37
- const getDenoVSCodeExt = async () => {
38
- await execa('code', ['--install-extension', 'denoland.vscode-deno'], { stdio: 'inherit' })
44
+ /**
45
+ * @param {string} repositoryRoot
46
+ */
47
+ const getDenoVSCodeExt = async (repositoryRoot) => {
48
+ await execa('code', ['--install-extension', 'denoland.vscode-deno'], { stdio: 'inherit', cwd: repositoryRoot })
39
49
  }
40
50
 
41
51
  const getDenoExtPrompt = () => {
@@ -49,6 +59,12 @@ const getDenoExtPrompt = () => {
49
59
  })
50
60
  }
51
61
 
62
+ /**
63
+ * @param {object} params
64
+ * @param {*} params.config
65
+ * @param {string} params.repositoryRoot
66
+ * @returns
67
+ */
52
68
  export const run = async ({ config, repositoryRoot }) => {
53
69
  const deno = new DenoBridge({
54
70
  onBeforeDownload: () =>
@@ -66,9 +82,11 @@ export const run = async ({ config, repositoryRoot }) => {
66
82
  }
67
83
 
68
84
  try {
69
- if (!(await hasDenoVSCodeExt())) {
85
+ if (!(await hasDenoVSCodeExt(repositoryRoot))) {
70
86
  const { confirm: denoExtConfirm } = await getDenoExtPrompt()
71
- if (denoExtConfirm) getDenoVSCodeExt()
87
+ if (denoExtConfirm) {
88
+ getDenoVSCodeExt(repositoryRoot)
89
+ }
72
90
  }
73
91
  } catch {
74
92
  log(
@@ -0,0 +1,100 @@
1
+ // @ts-check
2
+
3
+ import fuzzy from 'fuzzy'
4
+ import inquirer from 'inquirer'
5
+
6
+ import { chalk, log } from './command-helpers.mjs'
7
+
8
+ /**
9
+ * Filters the inquirer settings based on the input
10
+ * @param {ReturnType<typeof formatSettingsArrForInquirer>} scriptInquirerOptions
11
+ * @param {string} input
12
+ */
13
+ const filterSettings = function (scriptInquirerOptions, input) {
14
+ const filterOptions = scriptInquirerOptions.map((scriptInquirerOption) => scriptInquirerOption.name)
15
+ // TODO: remove once https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1394 is fixed
16
+ // eslint-disable-next-line unicorn/no-array-method-this-argument
17
+ const filteredSettings = fuzzy.filter(input, filterOptions)
18
+ const filteredSettingNames = new Set(
19
+ filteredSettings.map((filteredSetting) => (input ? filteredSetting.string : filteredSetting)),
20
+ )
21
+ return scriptInquirerOptions.filter((t) => filteredSettingNames.has(t.name))
22
+ }
23
+
24
+ /** @typedef {import('@netlify/build-info').Settings} Settings */
25
+
26
+ /**
27
+ * @param {Settings[]} settings
28
+ * @param {'dev' | 'build'} type The type of command (dev or build)
29
+ */
30
+ const formatSettingsArrForInquirer = function (settings, type = 'dev') {
31
+ return settings.map((setting) => {
32
+ const cmd = type === 'dev' ? setting.devCommand : setting.buildCommand
33
+ return {
34
+ name: `[${chalk.yellow(setting.framework.name)}] '${cmd}'`,
35
+ value: { ...setting, commands: [cmd] },
36
+ short: `${setting.name}-${cmd}`,
37
+ }
38
+ })
39
+ }
40
+
41
+ /**
42
+ * Uses @netlify/build-info to detect the dev settings and port based on the framework
43
+ * and the build system that is used.
44
+ * @param {import('../commands/base-command.mjs').default} command
45
+ * @param {'dev' | 'build'} type The type of command (dev or build)
46
+ * @returns {Promise<Settings | undefined>}
47
+ */
48
+ export const detectFrameworkSettings = async (command, type = 'dev') => {
49
+ const { relConfigFilePath } = command.netlify
50
+ const settings = await detectBuildSettings(command)
51
+ if (settings.length === 1) {
52
+ return settings[0]
53
+ }
54
+
55
+ if (settings.length > 1) {
56
+ /** multiple matching detectors, make the user choose */
57
+ const scriptInquirerOptions = formatSettingsArrForInquirer(settings, type)
58
+ /** @type {{chosenSettings: Settings}} */
59
+ const { chosenSettings } = await inquirer.prompt({
60
+ name: 'chosenSettings',
61
+ message: `Multiple possible ${type} commands found`,
62
+ type: 'autocomplete',
63
+ source(/** @type {string} */ _, input = '') {
64
+ if (!input) return scriptInquirerOptions
65
+ // only show filtered results
66
+ return filterSettings(scriptInquirerOptions, input)
67
+ },
68
+ })
69
+
70
+ log(`
71
+ Update your ${relConfigFilePath} to avoid this selection prompt next time:
72
+
73
+ [build]
74
+ command = "${chosenSettings.buildCommand}"
75
+ publish = "${chosenSettings.dist}"
76
+
77
+ [dev]
78
+ command = "${chosenSettings.devCommand}"
79
+ `)
80
+ return chosenSettings
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Detects and filters the build setting for a project and a command
86
+ * @param {import('../commands/base-command.mjs').default} command
87
+ */
88
+ export const detectBuildSettings = async (command) => {
89
+ const { project, workspacePackage } = command
90
+ const buildSettings = await project.getBuildSettings(project.workspace ? workspacePackage : '')
91
+ return buildSettings
92
+ .filter((setting) => {
93
+ if (project.workspace && project.relativeBaseDirectory && setting.packagePath) {
94
+ return project.relativeBaseDirectory.startsWith(setting.packagePath)
95
+ }
96
+
97
+ return true
98
+ })
99
+ .filter((setting) => setting.devCommand)
100
+ }
@@ -24,7 +24,7 @@ const argv = process.argv.slice(2)
24
24
  * Chalk instance for CLI that can be initialized with no colors mode
25
25
  * needed for json outputs where we don't want to have colors
26
26
  * @param {boolean} noColors - disable chalk colors
27
- * @return {object} - default or custom chalk instance
27
+ * @return {import('chalk').ChalkInstance} - default or custom chalk instance
28
28
  */
29
29
  const safeChalk = function (noColors) {
30
30
  if (noColors) {
@@ -174,12 +174,18 @@ export const warn = (message = '') => {
174
174
 
175
175
  /**
176
176
  * throws an error or log it
177
- * @param {string|Error} message
177
+ * @param {unknown} message
178
178
  * @param {object} [options]
179
179
  * @param {boolean} [options.exit]
180
180
  */
181
181
  export const error = (message = '', options = {}) => {
182
- const err = message instanceof Error ? message : new Error(message)
182
+ const err =
183
+ message instanceof Error
184
+ ? message
185
+ : // eslint-disable-next-line unicorn/no-nested-ternary
186
+ typeof message === 'string'
187
+ ? new Error(message)
188
+ : /** @type {Error} */ ({ message, stack: undefined, name: 'Error' })
183
189
 
184
190
  if (options.exit === false) {
185
191
  const bang = chalk.red(BANG)
@@ -198,10 +204,13 @@ export const exit = (code = 0) => {
198
204
  process.exit(code)
199
205
  }
200
206
 
201
- // When `build.publish` is not set by the user, the CLI behavior differs in
202
- // several ways. It detects it by checking if `build.publish` is `undefined`.
203
- // However, `@netlify/config` adds a default value to `build.publish`.
204
- // This removes 'publish' and 'publishOrigin' in this case.
207
+ /**
208
+ * When `build.publish` is not set by the user, the CLI behavior differs in
209
+ * several ways. It detects it by checking if `build.publish` is `undefined`.
210
+ * However, `@netlify/config` adds a default value to `build.publish`.
211
+ * This removes 'publish' and 'publishOrigin' in this case.
212
+ * @param {*} config
213
+ */
205
214
  export const normalizeConfig = (config) => {
206
215
  // Unused var here is in order to omit 'publish' from build config
207
216
  // eslint-disable-next-line no-unused-vars