netlify-cli 15.11.0 → 16.0.0-alpha.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 (44) hide show
  1. package/bin/run.mjs +6 -5
  2. package/npm-shrinkwrap.json +628 -42
  3. package/package.json +4 -5
  4. package/src/commands/base-command.mjs +295 -118
  5. package/src/commands/build/build.mjs +9 -1
  6. package/src/commands/deploy/deploy.mjs +42 -18
  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 +3 -3
  11. package/src/commands/functions/functions-serve.mjs +1 -0
  12. package/src/commands/init/init.mjs +1 -1
  13. package/src/commands/link/link.mjs +5 -5
  14. package/src/commands/serve/serve.mjs +10 -6
  15. package/src/commands/sites/sites-create-template.mjs +1 -1
  16. package/src/commands/sites/sites-create.mjs +1 -1
  17. package/src/functions-templates/javascript/google-analytics/package.json +1 -1
  18. package/src/functions-templates/typescript/scheduled-function/package.json +1 -1
  19. package/src/lib/edge-functions/deploy.mjs +11 -4
  20. package/src/lib/edge-functions/internal.mjs +5 -3
  21. package/src/lib/edge-functions/proxy.mjs +29 -5
  22. package/src/lib/functions/runtimes/js/builders/zisi.mjs +20 -3
  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/deploy/deploy-site.mjs +4 -4
  29. package/src/utils/deploy/hash-fns.mjs +2 -2
  30. package/src/utils/detect-server-settings.mjs +133 -245
  31. package/src/utils/framework-server.mjs +6 -5
  32. package/src/utils/functions/functions.mjs +8 -5
  33. package/src/utils/get-repo-data.mjs +5 -6
  34. package/src/utils/init/config-github.mjs +2 -2
  35. package/src/utils/init/config-manual.mjs +24 -7
  36. package/src/utils/init/utils.mjs +68 -68
  37. package/src/utils/proxy-server.mjs +7 -4
  38. package/src/utils/proxy.mjs +4 -3
  39. package/src/utils/read-repo-url.mjs +4 -0
  40. package/src/utils/run-build.mjs +58 -32
  41. package/src/utils/shell.mjs +24 -7
  42. package/src/utils/state-config.mjs +5 -1
  43. package/src/utils/static-server.mjs +4 -0
  44. package/src/utils/init/frameworks.mjs +0 -23
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
  import { stat } from 'fs/promises'
3
3
  import { basename, resolve } from 'path'
4
- import { cwd, env } from 'process'
4
+ import { env } from 'process'
5
5
 
6
6
  import { runCoreSteps } from '@netlify/build'
7
7
  import { restoreConfig, updateConfig } from '@netlify/config'
@@ -64,16 +64,18 @@ const triggerDeploy = async ({ api, options, siteData, siteId }) => {
64
64
  /**
65
65
  * g
66
66
  * @param {object} config
67
+ * @param {string} config.workingDir The process working directory
67
68
  * @param {object} config.config
68
69
  * @param {import('commander').OptionValues} config.options
69
70
  * @param {object} config.site
70
71
  * @param {object} config.siteData
71
72
  * @returns {Promise<string>}
72
73
  */
73
- const getDeployFolder = async ({ config, options, site, siteData }) => {
74
+ const getDeployFolder = async ({ config, options, site, siteData, workingDir }) => {
75
+ console.log()
74
76
  let deployFolder
75
77
  if (options.dir) {
76
- deployFolder = resolve(cwd(), options.dir)
78
+ deployFolder = resolve(site.root, options.dir)
77
79
  } else if (config?.build?.publish) {
78
80
  deployFolder = resolve(site.root, config.build.publish)
79
81
  } else if (siteData?.build_settings?.dir) {
@@ -82,14 +84,13 @@ const getDeployFolder = async ({ config, options, site, siteData }) => {
82
84
 
83
85
  if (!deployFolder) {
84
86
  log('Please provide a publish directory (e.g. "public" or "dist" or "."):')
85
- log(cwd())
86
87
  const { promptPath } = await inquirer.prompt([
87
88
  {
88
89
  type: 'input',
89
90
  name: 'promptPath',
90
91
  message: 'Publish directory',
91
92
  default: '.',
92
- filter: (input) => resolve(cwd(), input),
93
+ filter: (input) => resolve(workingDir, input),
93
94
  },
94
95
  ])
95
96
  deployFolder = promptPath
@@ -98,7 +99,10 @@ const getDeployFolder = async ({ config, options, site, siteData }) => {
98
99
  return deployFolder
99
100
  }
100
101
 
101
- const validateDeployFolder = async ({ deployFolder }) => {
102
+ /**
103
+ * @param {string} deployFolder
104
+ */
105
+ const validateDeployFolder = async (deployFolder) => {
102
106
  /** @type {import('fs').Stats} */
103
107
  let stats
104
108
  try {
@@ -128,14 +132,15 @@ const validateDeployFolder = async ({ deployFolder }) => {
128
132
  * @param {import('commander').OptionValues} config.options
129
133
  * @param {object} config.site
130
134
  * @param {object} config.siteData
131
- * @returns {string}
135
+ * @param {string} config.workingDir // The process working directory
136
+ * @returns {string|undefined}
132
137
  */
133
- const getFunctionsFolder = ({ config, options, site, siteData }) => {
138
+ const getFunctionsFolder = ({ config, options, site, siteData, workingDir }) => {
134
139
  let functionsFolder
135
140
  // Support "functions" and "Functions"
136
141
  const funcConfig = config.functionsDirectory
137
142
  if (options.functions) {
138
- functionsFolder = resolve(cwd(), options.functions)
143
+ functionsFolder = resolve(workingDir, options.functions)
139
144
  } else if (funcConfig) {
140
145
  functionsFolder = resolve(site.root, funcConfig)
141
146
  } else if (siteData?.build_settings?.functions_dir) {
@@ -144,8 +149,12 @@ const getFunctionsFolder = ({ config, options, site, siteData }) => {
144
149
  return functionsFolder
145
150
  }
146
151
 
147
- const validateFunctionsFolder = async ({ functionsFolder }) => {
148
- /** @type {import('fs').Stats} */
152
+ /**
153
+ *
154
+ * @param {string|undefined} functionsFolder
155
+ */
156
+ const validateFunctionsFolder = async (functionsFolder) => {
157
+ /** @type {import('fs').Stats|undefined} */
149
158
  let stats
150
159
  if (functionsFolder) {
151
160
  // we used to hard error if functions folder is specified but doesn't exist
@@ -173,17 +182,26 @@ const validateFunctionsFolder = async ({ functionsFolder }) => {
173
182
  }
174
183
 
175
184
  const validateFolders = async ({ deployFolder, functionsFolder }) => {
176
- const deployFolderStat = await validateDeployFolder({ deployFolder })
177
- const functionsFolderStat = await validateFunctionsFolder({ functionsFolder })
185
+ const deployFolderStat = await validateDeployFolder(deployFolder)
186
+ const functionsFolderStat = await validateFunctionsFolder(functionsFolder)
178
187
  return { deployFolderStat, functionsFolderStat }
179
188
  }
180
189
 
190
+ /**
191
+ * @param {object} config
192
+ * @param {string} config.deployFolder
193
+ * @param {*} config.site
194
+ * @returns
195
+ */
181
196
  const getDeployFilesFilter = ({ deployFolder, site }) => {
182
197
  // site.root === deployFolder can happen when users run `netlify deploy --dir .`
183
198
  // in that specific case we don't want to publish the repo node_modules
184
199
  // when site.root !== deployFolder the behaviour matches our buildbot
185
200
  const skipNodeModules = site.root === deployFolder
186
201
 
202
+ /**
203
+ * @param {string} filename
204
+ */
187
205
  return (filename) => {
188
206
  if (filename == null) {
189
207
  return false
@@ -298,6 +316,7 @@ const deployProgressCb = function () {
298
316
  const runDeploy = async ({
299
317
  alias,
300
318
  api,
319
+ command,
301
320
  configPath,
302
321
  deployFolder,
303
322
  deployTimeout,
@@ -344,7 +363,7 @@ const runDeploy = async ({
344
363
  // pass an existing deployId to update
345
364
  deployId,
346
365
  filter: getDeployFilesFilter({ site, deployFolder }),
347
- rootDir: site.root,
366
+ workingDir: command.workingDir,
348
367
  manifestPath,
349
368
  skipFunctionsCache,
350
369
  })
@@ -403,9 +422,10 @@ const handleBuild = async ({ cachedConfig, options }) => {
403
422
  /**
404
423
  *
405
424
  * @param {object} options Bundling options
425
+ * @param {import('..//base-command.mjs').default} command
406
426
  * @returns
407
427
  */
408
- const bundleEdgeFunctions = async (options) => {
428
+ const bundleEdgeFunctions = async (options, command) => {
409
429
  const statusCb = options.silent ? () => {} : deployProgressCb()
410
430
 
411
431
  statusCb({
@@ -416,6 +436,7 @@ const bundleEdgeFunctions = async (options) => {
416
436
 
417
437
  const { severityCode, success } = await runCoreSteps(['edge_functions_bundling'], {
418
438
  ...options,
439
+ packagePath: command.workspacePackage,
419
440
  buffer: true,
420
441
  featureFlags: edgeFunctionsFeatureFlags,
421
442
  })
@@ -496,6 +517,7 @@ const printResults = ({ deployToProduction, json, results, runBuildCommand }) =>
496
517
  * @param {import('../base-command.mjs').default} command
497
518
  */
498
519
  const deploy = async (options, command) => {
520
+ const { workingDir } = command
499
521
  const { api, site, siteInfo } = command.netlify
500
522
  const alias = options.alias || options.branch
501
523
 
@@ -560,20 +582,21 @@ const deploy = async (options, command) => {
560
582
  siteInfo: siteData,
561
583
  })
562
584
  }
585
+
563
586
  const { configMutations = [], newConfig } = await handleBuild({
564
587
  cachedConfig: command.netlify.cachedConfig,
565
588
  options,
566
589
  })
567
590
  const config = newConfig || command.netlify.config
568
591
 
569
- const deployFolder = await getDeployFolder({ options, config, site, siteData })
570
- const functionsFolder = getFunctionsFolder({ options, config, site, siteData })
592
+ const deployFolder = await getDeployFolder({ workingDir, options, config, site, siteData })
593
+ const functionsFolder = getFunctionsFolder({ workingDir, options, config, site, siteData })
571
594
  const { configPath } = site
572
595
  const edgeFunctionsConfig = command.netlify.config.edge_functions
573
596
 
574
597
  // build flag wasn't used and edge functions exist
575
598
  if (!options.build && edgeFunctionsConfig && edgeFunctionsConfig.length !== 0) {
576
- await bundleEdgeFunctions(options)
599
+ await bundleEdgeFunctions(options, command)
577
600
  }
578
601
 
579
602
  log(
@@ -618,6 +641,7 @@ const deploy = async (options, command) => {
618
641
  const results = await runDeploy({
619
642
  alias,
620
643
  api,
644
+ command,
621
645
  configPath,
622
646
  deployFolder,
623
647
  deployTimeout: options.timeout * SEC_TO_MILLISEC || DEFAULT_DEPLOY_TIMEOUT,
@@ -9,7 +9,6 @@ import { printBanner } from '../../utils/banner.mjs'
9
9
  import {
10
10
  BANG,
11
11
  chalk,
12
- exit,
13
12
  log,
14
13
  NETLIFYDEV,
15
14
  NETLIFYDEVERR,
@@ -35,7 +34,7 @@ import { createDevExecCommand } from './dev-exec.mjs'
35
34
  * @param {object} config
36
35
  * @param {*} config.api
37
36
  * @param {import('commander').OptionValues} config.options
38
- * @param {*} config.settings
37
+ * @param {import('../../utils/types.js').ServerSettings} config.settings
39
38
  * @param {*} config.site
40
39
  * @param {*} config.state
41
40
  * @returns
@@ -68,6 +67,9 @@ const handleLiveTunnel = async ({ api, options, settings, site, state }) => {
68
67
  }
69
68
  }
70
69
 
70
+ /**
71
+ * @param {string} args
72
+ */
71
73
  const validateShortFlagArgs = (args) => {
72
74
  if (args.startsWith('=')) {
73
75
  throw new Error(
@@ -94,11 +96,13 @@ const dev = async (options, command) => {
94
96
  const { api, cachedConfig, config, repositoryRoot, site, siteInfo, state } = command.netlify
95
97
  config.dev = { ...config.dev }
96
98
  config.build = { ...config.build }
97
- /** @type {import('./types').DevConfig} */
99
+ /** @type {import('./types.js').DevConfig} */
98
100
  const devConfig = {
99
101
  framework: '#auto',
102
+ autoLaunch: Boolean(options.open),
100
103
  ...(config.functionsDirectory && { functions: config.functionsDirectory }),
101
104
  ...(config.build.publish && { publish: config.build.publish }),
105
+ ...(config.build.base && { base: config.build.base }),
102
106
  ...config.dev,
103
107
  ...options,
104
108
  }
@@ -124,20 +128,17 @@ const dev = async (options, command) => {
124
128
  siteInfo,
125
129
  })
126
130
 
127
- /** @type {Partial<import('../../utils/types').ServerSettings>} */
128
- let settings = {}
131
+ /** @type {import('../../utils/types.js').ServerSettings} */
132
+ let settings
129
133
  try {
130
- settings = await detectServerSettings(devConfig, options, site.root, {
131
- site: {
132
- id: site.id,
133
- url: siteUrl,
134
- },
135
- })
134
+ settings = await detectServerSettings(devConfig, options, command)
136
135
 
137
136
  cachedConfig.config = getConfigWithPlugins(cachedConfig.config, settings)
138
137
  } catch (error_) {
139
- log(NETLIFYDEVERR, error_.message)
140
- exit(1)
138
+ if (error_ && typeof error_ === 'object' && 'message' in error_) {
139
+ log(NETLIFYDEVERR, error_.message)
140
+ }
141
+ process.exit(1)
141
142
  }
142
143
 
143
144
  command.setAnalyticsPayload({ live: options.live })
@@ -151,10 +152,9 @@ const dev = async (options, command) => {
151
152
  log(`${NETLIFYDEVWARN} Setting up local development server`)
152
153
 
153
154
  const { configPath: configPathOverride } = await runDevTimeline({
154
- cachedConfig,
155
+ command,
155
156
  options,
156
157
  settings,
157
- site,
158
158
  env: {
159
159
  URL: url,
160
160
  DEPLOY_URL: url,
@@ -188,8 +188,11 @@ const dev = async (options, command) => {
188
188
 
189
189
  // TODO: We should consolidate this with the existing config watcher.
190
190
  const getUpdatedConfig = async () => {
191
- const cwd = options.cwd || process.cwd()
192
- const { config: newConfig } = await command.getConfig({ cwd, offline: true, state })
191
+ const { config: newConfig } = await command.getConfig({
192
+ cwd: command.workingDir,
193
+ offline: true,
194
+ state,
195
+ })
193
196
  const normalizedNewConfig = normalizeConfig(newConfig)
194
197
 
195
198
  return normalizedNewConfig
@@ -202,6 +205,7 @@ const dev = async (options, command) => {
202
205
  config,
203
206
  configPath: configPathOverride,
204
207
  debug: options.debug,
208
+ projectDir: command.workingDir,
205
209
  env,
206
210
  getUpdatedConfig,
207
211
  inspectSettings,
@@ -248,6 +252,7 @@ export const createDevCommand = (program) => {
248
252
  .argParser((value) => Number.parseInt(value))
249
253
  .hideHelp(true),
250
254
  )
255
+ .addOption(new Option('--no-open', 'disables the automatic opening of a browser window'))
251
256
  .option('--target-port <port>', 'port of target app server', (value) => Number.parseInt(value))
252
257
  .option('--framework <name>', 'framework to use. Defaults to #auto which automatically detects a framework')
253
258
  .option('-d ,--dir <path>', 'dir with static files')
@@ -3,7 +3,7 @@ import cp from 'child_process'
3
3
  import fs from 'fs'
4
4
  import { mkdir, readdir, unlink } from 'fs/promises'
5
5
  import { createRequire } from 'module'
6
- import path, { dirname } from 'path'
6
+ import path, { dirname, join, relative } from 'path'
7
7
  import process from 'process'
8
8
  import { fileURLToPath, pathToFileURL } from 'url'
9
9
  import { promisify } from 'util'
@@ -12,7 +12,6 @@ import copyTemplateDirOriginal from 'copy-template-dir'
12
12
  import { findUp } from 'find-up'
13
13
  import fuzzy from 'fuzzy'
14
14
  import inquirer from 'inquirer'
15
- import inquirerAutocompletePrompt from 'inquirer-autocomplete-prompt'
16
15
  import fetch from 'node-fetch'
17
16
  import ora from 'ora'
18
17
 
@@ -31,8 +30,10 @@ const templatesDir = path.resolve(dirname(fileURLToPath(import.meta.url)), '../.
31
30
 
32
31
  const showRustTemplates = process.env.NETLIFY_EXPERIMENTAL_BUILD_RUST_SOURCE === 'true'
33
32
 
34
- // Ensure that there's a sub-directory in `src/functions-templates` named after
35
- // each `value` property in this list.
33
+ /**
34
+ * Ensure that there's a sub-directory in `src/functions-templates` named after
35
+ * each `value` property in this list.
36
+ */
36
37
  const languages = [
37
38
  { name: 'JavaScript', value: 'javascript' },
38
39
  { name: 'TypeScript', value: 'typescript' },
@@ -91,23 +92,28 @@ const filterRegistry = function (registry, input) {
91
92
  })
92
93
  }
93
94
 
95
+ /**
96
+ * @param {string} lang
97
+ * @param {'edge' | 'serverless'} funcType
98
+ */
94
99
  const formatRegistryArrayForInquirer = async function (lang, funcType) {
95
- const folderNames = await readdir(path.join(templatesDir, lang))
100
+ const folders = await readdir(path.join(templatesDir, lang), { withFileTypes: true })
96
101
 
97
102
  const imports = await Promise.all(
98
- folderNames
99
- // filter out markdown files
100
- .filter((folderName) => !folderName.endsWith('.md'))
101
- .map(async (folderName) => {
102
- const templatePath = path.join(templatesDir, lang, folderName, '.netlify-function-template.mjs')
103
- const template = await import(pathToFileURL(templatePath))
104
-
105
- return template.default
103
+ folders
104
+ .filter((folder) => Boolean(folder?.isDirectory()))
105
+ .map(async ({ name }) => {
106
+ try {
107
+ const templatePath = path.join(templatesDir, lang, name, '.netlify-function-template.mjs')
108
+ const template = await import(pathToFileURL(templatePath))
109
+ return template.default
110
+ } catch {
111
+ // noop if import fails we don't break the whole inquirer
112
+ }
106
113
  }),
107
114
  )
108
-
109
115
  const registry = imports
110
- .filter((template) => template.functionType === funcType)
116
+ .filter((template) => template?.functionType === funcType)
111
117
  .sort((templateA, templateB) => {
112
118
  const priorityDiff = (templateA.priority || DEFAULT_PRIORITY) - (templateB.priority || DEFAULT_PRIORITY)
113
119
 
@@ -136,7 +142,7 @@ const formatRegistryArrayForInquirer = async function (lang, funcType) {
136
142
  /**
137
143
  * pick template from our existing templates
138
144
  * @param {import('commander').OptionValues} config
139
- *
145
+ * @param {'edge' | 'serverless'} funcType
140
146
  */
141
147
  const pickTemplate = async function ({ language: languageFromFlag }, funcType) {
142
148
  const specialCommands = [
@@ -172,8 +178,6 @@ const pickTemplate = async function ({ language: languageFromFlag }, funcType) {
172
178
  language = languageFromPrompt
173
179
  }
174
180
 
175
- inquirer.registerPrompt('autocomplete', inquirerAutocompletePrompt)
176
-
177
181
  let templatesForLanguage
178
182
 
179
183
  try {
@@ -207,6 +211,7 @@ const pickTemplate = async function ({ language: languageFromFlag }, funcType) {
207
211
 
208
212
  const DEFAULT_PRIORITY = 999
209
213
 
214
+ /** @returns {Promise<'edge' | 'serverless'>} */
210
215
  const selectTypeOfFunc = async () => {
211
216
  const functionTypes = [
212
217
  { name: 'Edge function (Deno)', value: 'edge' },
@@ -224,92 +229,100 @@ const selectTypeOfFunc = async () => {
224
229
  return functionType
225
230
  }
226
231
 
232
+ /**
233
+ * @param {import('../base-command.mjs').default} command
234
+ */
227
235
  const ensureEdgeFuncDirExists = function (command) {
228
236
  const { config, site } = command.netlify
229
237
  const siteId = site.id
230
- let functionsDirHolder = config.build.edge_functions
231
238
 
232
239
  if (!siteId) {
233
240
  error(`${NETLIFYDEVERR} No site id found, please run inside a site directory or \`netlify link\``)
234
241
  }
235
242
 
236
- if (!functionsDirHolder) {
237
- functionsDirHolder = 'netlify/edge-functions'
238
- }
243
+ const functionsDir = config.build?.edge_functions ?? join(command.workingDir, 'netlify/edge-functions')
244
+ const relFunctionsDir = relative(command.workingDir, functionsDir)
239
245
 
240
- if (!fs.existsSync(functionsDirHolder)) {
246
+ if (!fs.existsSync(functionsDir)) {
241
247
  log(
242
248
  `${NETLIFYDEVLOG} Edge Functions directory ${chalk.magenta.inverse(
243
- functionsDirHolder,
249
+ relFunctionsDir,
244
250
  )} does not exist yet, creating it...`,
245
251
  )
246
252
 
247
- fs.mkdirSync(functionsDirHolder, { recursive: true })
253
+ fs.mkdirSync(functionsDir, { recursive: true })
248
254
 
249
- log(`${NETLIFYDEVLOG} Edge Functions directory ${chalk.magenta.inverse(functionsDirHolder)} created.`)
255
+ log(`${NETLIFYDEVLOG} Edge Functions directory ${chalk.magenta.inverse(relFunctionsDir)} created.`)
250
256
  }
251
- return functionsDirHolder
257
+
258
+ return functionsDir
252
259
  }
253
260
 
254
261
  /**
255
- * Get functions directory (and make it if necessary)
262
+ * Prompts the user to choose a functions directory
256
263
  * @param {import('../base-command.mjs').default} command
257
- * @returns {Promise<string|never>} - functions directory or throws an error
264
+ * @returns {Promise<string>} - functions directory or throws an error
258
265
  */
259
- const ensureFunctionDirExists = async function (command) {
260
- const { api, config, site } = command.netlify
261
- const siteId = site.id
262
- let functionsDirHolder = config.functionsDirectory
263
-
264
- if (!functionsDirHolder) {
265
- log(`${NETLIFYDEVLOG} functions directory not specified in netlify.toml or UI settings`)
266
-
267
- if (!siteId) {
268
- error(`${NETLIFYDEVERR} No site id found, please run inside a site directory or \`netlify link\``)
269
- }
266
+ const promptFunctionsDirectory = async (command) => {
267
+ const { api, relConfigFilePath, site } = command.netlify
268
+ log(`\n${NETLIFYDEVLOG} functions directory not specified in ${relConfigFilePath} or UI settings`)
270
269
 
271
- const { functionsDir } = await inquirer.prompt([
272
- {
273
- type: 'input',
274
- name: 'functionsDir',
275
- message:
276
- 'Enter the path, relative to your site’s base directory in your repository, where your functions should live:',
277
- default: 'netlify/functions',
278
- },
279
- ])
270
+ if (!site.id) {
271
+ error(`${NETLIFYDEVERR} No site id found, please run inside a site directory or \`netlify link\``)
272
+ }
280
273
 
281
- functionsDirHolder = functionsDir
274
+ const { functionsDir } = await inquirer.prompt([
275
+ {
276
+ type: 'input',
277
+ name: 'functionsDir',
278
+ message: 'Enter the path, relative to your site, where your functions should live:',
279
+ default: 'netlify/functions',
280
+ },
281
+ ])
282
282
 
283
- try {
284
- log(`${NETLIFYDEVLOG} updating site settings with ${chalk.magenta.inverse(functionsDirHolder)}`)
285
-
286
- // @ts-ignore Typings of API are not correct
287
- await api.updateSite({
288
- siteId: site.id,
289
- body: {
290
- build_settings: {
291
- functions_dir: functionsDirHolder,
292
- },
283
+ try {
284
+ log(`${NETLIFYDEVLOG} updating site settings with ${chalk.magenta.inverse(functionsDir)}`)
285
+
286
+ // @ts-ignore Typings of API are not correct
287
+ await api.updateSite({
288
+ siteId: site.id,
289
+ body: {
290
+ build_settings: {
291
+ functions_dir: functionsDir,
293
292
  },
294
- })
293
+ },
294
+ })
295
295
 
296
- log(`${NETLIFYDEVLOG} functions directory ${chalk.magenta.inverse(functionsDirHolder)} updated in site settings`)
297
- } catch {
298
- throw error('Error updating site settings')
299
- }
296
+ log(`${NETLIFYDEVLOG} functions directory ${chalk.magenta.inverse(functionsDir)} updated in site settings`)
297
+ } catch {
298
+ throw error('Error updating site settings')
300
299
  }
300
+ return functionsDir
301
+ }
302
+
303
+ /**
304
+ * Get functions directory (and make it if necessary)
305
+ * @param {import('../base-command.mjs').default} command
306
+ * @returns {Promise<string>} - functions directory or throws an error
307
+ */
308
+ const ensureFunctionDirExists = async function (command) {
309
+ const { config } = command.netlify
310
+ const functionsDirHolder =
311
+ config.functionsDirectory || join(command.workingDir, await promptFunctionsDirectory(command))
312
+ const relFunctionsDirHolder = relative(command.workingDir, functionsDirHolder)
301
313
 
302
- if (!(await fileExistsAsync(functionsDirHolder))) {
314
+ if (!fs.existsSync(functionsDirHolder)) {
303
315
  log(
304
316
  `${NETLIFYDEVLOG} functions directory ${chalk.magenta.inverse(
305
- functionsDirHolder,
317
+ relFunctionsDirHolder,
306
318
  )} does not exist yet, creating it...`,
307
319
  )
308
320
 
309
321
  await mkdir(functionsDirHolder, { recursive: true })
310
322
 
311
- log(`${NETLIFYDEVLOG} functions directory ${chalk.magenta.inverse(functionsDirHolder)} created`)
323
+ log(`${NETLIFYDEVLOG} functions directory ${chalk.magenta.inverse(relFunctionsDirHolder)} created`)
312
324
  }
325
+
313
326
  return functionsDirHolder
314
327
  }
315
328
 
@@ -370,20 +383,24 @@ const downloadFromURL = async function (command, options, argumentName, function
370
383
  }
371
384
  }
372
385
 
373
- // Takes a list of existing packages and a list of packages required by a
374
- // function, and returns the packages from the latter that aren't present
375
- // in the former. The packages are returned as an array of strings with the
376
- // name and version range (e.g. '@netlify/functions@0.1.0').
386
+ /**
387
+ * Takes a list of existing packages and a list of packages required by a
388
+ * function, and returns the packages from the latter that aren't present
389
+ * in the former. The packages are returned as an array of strings with the
390
+ * name and version range (e.g. '@netlify/functions@0.1.0').
391
+ */
377
392
  const getNpmInstallPackages = (existingPackages = {}, neededPackages = {}) =>
378
393
  Object.entries(neededPackages)
379
394
  .filter(([name]) => existingPackages[name] === undefined)
380
395
  .map(([name, version]) => `${name}@${version}`)
381
396
 
382
- // When installing a function's dependencies, we first try to find a site-level
383
- // `package.json` file. If we do, we look for any dependencies of the function
384
- // that aren't already listed as dependencies of the site and install them. If
385
- // we don't do this check, we may be upgrading the version of a module used in
386
- // another part of the project, which we don't want to do.
397
+ /**
398
+ * When installing a function's dependencies, we first try to find a site-level
399
+ * `package.json` file. If we do, we look for any dependencies of the function
400
+ * that aren't already listed as dependencies of the site and install them. If
401
+ * we don't do this check, we may be upgrading the version of a module used in
402
+ * another part of the project, which we don't want to do.
403
+ */
387
404
  const installDeps = async ({ functionPackageJson, functionPath, functionsDir }) => {
388
405
  const { dependencies: functionDependencies, devDependencies: functionDevDependencies } = require(functionPackageJson)
389
406
  const sitePackageJson = await findUp('package.json', { cwd: functionsDir })
@@ -430,8 +447,8 @@ const installDeps = async ({ functionPackageJson, functionPath, functionsDir })
430
447
  * @param {import('../base-command.mjs').default} command
431
448
  * @param {import('commander').OptionValues} options
432
449
  * @param {string} argumentName
433
- * @param {string} functionsDir
434
- * @param {string} funcType
450
+ * @param {string} functionsDir Absolute path of the functions directory
451
+ * @param {'edge' | 'serverless'} funcType
435
452
  */
436
453
  // eslint-disable-next-line max-params
437
454
  const scaffoldFromTemplate = async function (command, options, argumentName, functionsDir, funcType) {
@@ -443,7 +460,7 @@ const scaffoldFromTemplate = async function (command, options, argumentName, fun
443
460
  name: 'chosenUrl',
444
461
  message: 'URL to clone: ',
445
462
  type: 'input',
446
- validate: (val) => Boolean(validateRepoURL(val)),
463
+ validate: (/** @type {string} */ val) => Boolean(validateRepoURL(val)),
447
464
  // make sure it is not undefined and is a valid filename.
448
465
  // this has some nuance i have ignored, eg crossenv and i18n concerns
449
466
  },
@@ -506,7 +523,7 @@ const scaffoldFromTemplate = async function (command, options, argumentName, fun
506
523
  }
507
524
 
508
525
  if (funcType === 'edge') {
509
- registerEFInToml(name)
526
+ registerEFInToml(name, command.netlify)
510
527
  }
511
528
 
512
529
  await installAddons(command, addons, path.resolve(functionPath))
@@ -631,9 +648,15 @@ const installAddons = async function (command, functionAddons, fnPath) {
631
648
  return Promise.all(arr)
632
649
  }
633
650
 
634
- const registerEFInToml = async (funcName) => {
635
- if (!fs.existsSync('netlify.toml')) {
636
- log(`${NETLIFYDEVLOG} \`netlify.toml\` file does not exist yet. Creating it...`)
651
+ /**
652
+ *
653
+ * @param {string} funcName
654
+ * @param {import('../types.js').NetlifyOptions} options
655
+ */
656
+ const registerEFInToml = async (funcName, options) => {
657
+ const { configFilePath, relConfigFilePath } = options
658
+ if (!fs.existsSync(configFilePath)) {
659
+ log(`${NETLIFYDEVLOG} \`${relConfigFilePath}\` file does not exist yet. Creating it...`)
637
660
  }
638
661
 
639
662
  let { funcPath } = await inquirer.prompt([
@@ -656,17 +679,22 @@ const registerEFInToml = async (funcName) => {
656
679
  const functionRegister = `\n\n[[edge_functions]]\nfunction = "${funcName}"\npath = "${funcPath}"`
657
680
 
658
681
  try {
659
- fs.promises.appendFile('netlify.toml', functionRegister)
682
+ fs.promises.appendFile(configFilePath, functionRegister)
660
683
  log(
661
- `${NETLIFYDEVLOG} Function '${funcName}' registered for route \`${funcPath}\`. To change, edit your \`netlify.toml\` file.`,
684
+ `${NETLIFYDEVLOG} Function '${funcName}' registered for route \`${funcPath}\`. To change, edit your \`${relConfigFilePath}\` file.`,
662
685
  )
663
686
  } catch {
664
- error(`${NETLIFYDEVERR} Unable to register function. Please check your \`netlify.toml\` file.`)
687
+ error(`${NETLIFYDEVERR} Unable to register function. Please check your \`${relConfigFilePath}\` file.`)
665
688
  }
666
689
  }
667
690
 
668
- // we used to allow for a --dir command,
669
- // but have retired that to force every scaffolded function to be a directory
691
+ /**
692
+ * we used to allow for a --dir command,
693
+ * but have retired that to force every scaffolded function to be a directory
694
+ * @param {string} functionsDir
695
+ * @param {string} name
696
+ * @returns
697
+ */
670
698
  const ensureFunctionPathIsOk = function (functionsDir, name) {
671
699
  const functionPath = path.join(functionsDir, name)
672
700
  if (fs.existsSync(functionPath)) {
@@ -678,6 +706,7 @@ const ensureFunctionPathIsOk = function (functionsDir, name) {
678
706
 
679
707
  /**
680
708
  * The functions:create command
709
+ * @param {string} name
681
710
  * @param {import('commander').OptionValues} options
682
711
  * @param {import('../base-command.mjs').default} command
683
712
  */