netlify-cli 15.9.1-rc.0 → 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.
@@ -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'
@@ -30,8 +30,10 @@ const templatesDir = path.resolve(dirname(fileURLToPath(import.meta.url)), '../.
30
30
 
31
31
  const showRustTemplates = process.env.NETLIFY_EXPERIMENTAL_BUILD_RUST_SOURCE === 'true'
32
32
 
33
- // Ensure that there's a sub-directory in `src/functions-templates` named after
34
- // 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
+ */
35
37
  const languages = [
36
38
  { name: 'JavaScript', value: 'javascript' },
37
39
  { name: 'TypeScript', value: 'typescript' },
@@ -90,23 +92,28 @@ const filterRegistry = function (registry, input) {
90
92
  })
91
93
  }
92
94
 
95
+ /**
96
+ * @param {string} lang
97
+ * @param {'edge' | 'serverless'} funcType
98
+ */
93
99
  const formatRegistryArrayForInquirer = async function (lang, funcType) {
94
- const folderNames = await readdir(path.join(templatesDir, lang))
100
+ const folders = await readdir(path.join(templatesDir, lang), { withFileTypes: true })
95
101
 
96
102
  const imports = await Promise.all(
97
- folderNames
98
- // filter out markdown files
99
- .filter((folderName) => !folderName.endsWith('.md'))
100
- .map(async (folderName) => {
101
- const templatePath = path.join(templatesDir, lang, folderName, '.netlify-function-template.mjs')
102
- const template = await import(pathToFileURL(templatePath))
103
-
104
- 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
+ }
105
113
  }),
106
114
  )
107
-
108
115
  const registry = imports
109
- .filter((template) => template.functionType === funcType)
116
+ .filter((template) => template?.functionType === funcType)
110
117
  .sort((templateA, templateB) => {
111
118
  const priorityDiff = (templateA.priority || DEFAULT_PRIORITY) - (templateB.priority || DEFAULT_PRIORITY)
112
119
 
@@ -135,7 +142,7 @@ const formatRegistryArrayForInquirer = async function (lang, funcType) {
135
142
  /**
136
143
  * pick template from our existing templates
137
144
  * @param {import('commander').OptionValues} config
138
- *
145
+ * @param {'edge' | 'serverless'} funcType
139
146
  */
140
147
  const pickTemplate = async function ({ language: languageFromFlag }, funcType) {
141
148
  const specialCommands = [
@@ -204,6 +211,7 @@ const pickTemplate = async function ({ language: languageFromFlag }, funcType) {
204
211
 
205
212
  const DEFAULT_PRIORITY = 999
206
213
 
214
+ /** @returns {Promise<'edge' | 'serverless'>} */
207
215
  const selectTypeOfFunc = async () => {
208
216
  const functionTypes = [
209
217
  { name: 'Edge function (Deno)', value: 'edge' },
@@ -221,92 +229,100 @@ const selectTypeOfFunc = async () => {
221
229
  return functionType
222
230
  }
223
231
 
232
+ /**
233
+ * @param {import('../base-command.mjs').default} command
234
+ */
224
235
  const ensureEdgeFuncDirExists = function (command) {
225
236
  const { config, site } = command.netlify
226
237
  const siteId = site.id
227
- let functionsDirHolder = config.build.edge_functions
228
238
 
229
239
  if (!siteId) {
230
240
  error(`${NETLIFYDEVERR} No site id found, please run inside a site directory or \`netlify link\``)
231
241
  }
232
242
 
233
- if (!functionsDirHolder) {
234
- functionsDirHolder = 'netlify/edge-functions'
235
- }
243
+ const functionsDir = config.build?.edge_functions ?? join(command.workingDir, 'netlify/edge-functions')
244
+ const relFunctionsDir = relative(command.workingDir, functionsDir)
236
245
 
237
- if (!fs.existsSync(functionsDirHolder)) {
246
+ if (!fs.existsSync(functionsDir)) {
238
247
  log(
239
248
  `${NETLIFYDEVLOG} Edge Functions directory ${chalk.magenta.inverse(
240
- functionsDirHolder,
249
+ relFunctionsDir,
241
250
  )} does not exist yet, creating it...`,
242
251
  )
243
252
 
244
- fs.mkdirSync(functionsDirHolder, { recursive: true })
253
+ fs.mkdirSync(functionsDir, { recursive: true })
245
254
 
246
- log(`${NETLIFYDEVLOG} Edge Functions directory ${chalk.magenta.inverse(functionsDirHolder)} created.`)
255
+ log(`${NETLIFYDEVLOG} Edge Functions directory ${chalk.magenta.inverse(relFunctionsDir)} created.`)
247
256
  }
248
- return functionsDirHolder
257
+
258
+ return functionsDir
249
259
  }
250
260
 
251
261
  /**
252
- * Get functions directory (and make it if necessary)
262
+ * Prompts the user to choose a functions directory
253
263
  * @param {import('../base-command.mjs').default} command
254
- * @returns {Promise<string|never>} - functions directory or throws an error
264
+ * @returns {Promise<string>} - functions directory or throws an error
255
265
  */
256
- const ensureFunctionDirExists = async function (command) {
257
- const { api, config, site } = command.netlify
258
- const siteId = site.id
259
- let functionsDirHolder = config.functionsDirectory
260
-
261
- if (!functionsDirHolder) {
262
- log(`${NETLIFYDEVLOG} functions directory not specified in netlify.toml or UI settings`)
263
-
264
- if (!siteId) {
265
- error(`${NETLIFYDEVERR} No site id found, please run inside a site directory or \`netlify link\``)
266
- }
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`)
267
269
 
268
- const { functionsDir } = await inquirer.prompt([
269
- {
270
- type: 'input',
271
- name: 'functionsDir',
272
- message:
273
- 'Enter the path, relative to your site’s base directory in your repository, where your functions should live:',
274
- default: 'netlify/functions',
275
- },
276
- ])
270
+ if (!site.id) {
271
+ error(`${NETLIFYDEVERR} No site id found, please run inside a site directory or \`netlify link\``)
272
+ }
277
273
 
278
- 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
+ ])
279
282
 
280
- try {
281
- log(`${NETLIFYDEVLOG} updating site settings with ${chalk.magenta.inverse(functionsDirHolder)}`)
282
-
283
- // @ts-ignore Typings of API are not correct
284
- await api.updateSite({
285
- siteId: site.id,
286
- body: {
287
- build_settings: {
288
- functions_dir: functionsDirHolder,
289
- },
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,
290
292
  },
291
- })
293
+ },
294
+ })
292
295
 
293
- log(`${NETLIFYDEVLOG} functions directory ${chalk.magenta.inverse(functionsDirHolder)} updated in site settings`)
294
- } catch {
295
- throw error('Error updating site settings')
296
- }
296
+ log(`${NETLIFYDEVLOG} functions directory ${chalk.magenta.inverse(functionsDir)} updated in site settings`)
297
+ } catch {
298
+ throw error('Error updating site settings')
297
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)
298
313
 
299
- if (!(await fileExistsAsync(functionsDirHolder))) {
314
+ if (!fs.existsSync(functionsDirHolder)) {
300
315
  log(
301
316
  `${NETLIFYDEVLOG} functions directory ${chalk.magenta.inverse(
302
- functionsDirHolder,
317
+ relFunctionsDirHolder,
303
318
  )} does not exist yet, creating it...`,
304
319
  )
305
320
 
306
321
  await mkdir(functionsDirHolder, { recursive: true })
307
322
 
308
- log(`${NETLIFYDEVLOG} functions directory ${chalk.magenta.inverse(functionsDirHolder)} created`)
323
+ log(`${NETLIFYDEVLOG} functions directory ${chalk.magenta.inverse(relFunctionsDirHolder)} created`)
309
324
  }
325
+
310
326
  return functionsDirHolder
311
327
  }
312
328
 
@@ -367,20 +383,24 @@ const downloadFromURL = async function (command, options, argumentName, function
367
383
  }
368
384
  }
369
385
 
370
- // Takes a list of existing packages and a list of packages required by a
371
- // function, and returns the packages from the latter that aren't present
372
- // in the former. The packages are returned as an array of strings with the
373
- // 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
+ */
374
392
  const getNpmInstallPackages = (existingPackages = {}, neededPackages = {}) =>
375
393
  Object.entries(neededPackages)
376
394
  .filter(([name]) => existingPackages[name] === undefined)
377
395
  .map(([name, version]) => `${name}@${version}`)
378
396
 
379
- // When installing a function's dependencies, we first try to find a site-level
380
- // `package.json` file. If we do, we look for any dependencies of the function
381
- // that aren't already listed as dependencies of the site and install them. If
382
- // we don't do this check, we may be upgrading the version of a module used in
383
- // 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
+ */
384
404
  const installDeps = async ({ functionPackageJson, functionPath, functionsDir }) => {
385
405
  const { dependencies: functionDependencies, devDependencies: functionDevDependencies } = require(functionPackageJson)
386
406
  const sitePackageJson = await findUp('package.json', { cwd: functionsDir })
@@ -427,8 +447,8 @@ const installDeps = async ({ functionPackageJson, functionPath, functionsDir })
427
447
  * @param {import('../base-command.mjs').default} command
428
448
  * @param {import('commander').OptionValues} options
429
449
  * @param {string} argumentName
430
- * @param {string} functionsDir
431
- * @param {string} funcType
450
+ * @param {string} functionsDir Absolute path of the functions directory
451
+ * @param {'edge' | 'serverless'} funcType
432
452
  */
433
453
  // eslint-disable-next-line max-params
434
454
  const scaffoldFromTemplate = async function (command, options, argumentName, functionsDir, funcType) {
@@ -440,7 +460,7 @@ const scaffoldFromTemplate = async function (command, options, argumentName, fun
440
460
  name: 'chosenUrl',
441
461
  message: 'URL to clone: ',
442
462
  type: 'input',
443
- validate: (val) => Boolean(validateRepoURL(val)),
463
+ validate: (/** @type {string} */ val) => Boolean(validateRepoURL(val)),
444
464
  // make sure it is not undefined and is a valid filename.
445
465
  // this has some nuance i have ignored, eg crossenv and i18n concerns
446
466
  },
@@ -503,7 +523,7 @@ const scaffoldFromTemplate = async function (command, options, argumentName, fun
503
523
  }
504
524
 
505
525
  if (funcType === 'edge') {
506
- registerEFInToml(name)
526
+ registerEFInToml(name, command.netlify)
507
527
  }
508
528
 
509
529
  await installAddons(command, addons, path.resolve(functionPath))
@@ -628,9 +648,15 @@ const installAddons = async function (command, functionAddons, fnPath) {
628
648
  return Promise.all(arr)
629
649
  }
630
650
 
631
- const registerEFInToml = async (funcName) => {
632
- if (!fs.existsSync('netlify.toml')) {
633
- 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...`)
634
660
  }
635
661
 
636
662
  let { funcPath } = await inquirer.prompt([
@@ -653,17 +679,22 @@ const registerEFInToml = async (funcName) => {
653
679
  const functionRegister = `\n\n[[edge_functions]]\nfunction = "${funcName}"\npath = "${funcPath}"`
654
680
 
655
681
  try {
656
- fs.promises.appendFile('netlify.toml', functionRegister)
682
+ fs.promises.appendFile(configFilePath, functionRegister)
657
683
  log(
658
- `${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.`,
659
685
  )
660
686
  } catch {
661
- error(`${NETLIFYDEVERR} Unable to register function. Please check your \`netlify.toml\` file.`)
687
+ error(`${NETLIFYDEVERR} Unable to register function. Please check your \`${relConfigFilePath}\` file.`)
662
688
  }
663
689
  }
664
690
 
665
- // we used to allow for a --dir command,
666
- // 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
+ */
667
698
  const ensureFunctionPathIsOk = function (functionsDir, name) {
668
699
  const functionPath = path.join(functionsDir, name)
669
700
  if (fs.existsSync(functionPath)) {
@@ -675,6 +706,7 @@ const ensureFunctionPathIsOk = function (functionsDir, name) {
675
706
 
676
707
  /**
677
708
  * The functions:create command
709
+ * @param {string} name
678
710
  * @param {import('commander').OptionValues} options
679
711
  * @param {import('../base-command.mjs').default} command
680
712
  */
@@ -144,11 +144,11 @@ const getFunctionToTrigger = function (options, argumentName) {
144
144
  * @param {import('../base-command.mjs').default} command
145
145
  */
146
146
  const functionsInvoke = async (nameArgument, options, command) => {
147
- const { config } = command.netlify
147
+ const { config, relConfigFilePath } = command.netlify
148
148
 
149
149
  const functionsDir = options.functions || (config.dev && config.dev.functions) || config.functionsDirectory
150
150
  if (typeof functionsDir === 'undefined') {
151
- error('functions directory is undefined, did you forget to set it in netlify.toml?')
151
+ error(`functions directory is undefined, did you forget to set it in ${relConfigFilePath}?`)
152
152
  }
153
153
 
154
154
  if (!options.port)
@@ -16,7 +16,7 @@ const normalizeFunction = function (deployedFunctions, { name, urlPath: url }) {
16
16
  * @param {import('../base-command.mjs').default} command
17
17
  */
18
18
  const functionsList = async (options, command) => {
19
- const { config, siteInfo } = command.netlify
19
+ const { config, relConfigFilePath, siteInfo } = command.netlify
20
20
 
21
21
  const deploy = siteInfo.published_deploy || {}
22
22
  const deployedFunctions = deploy.available_functions || []
@@ -25,7 +25,7 @@ const functionsList = async (options, command) => {
25
25
 
26
26
  if (typeof functionsDir === 'undefined') {
27
27
  log('Functions directory is undefined')
28
- log('Please verify functions.directory is set in your Netlify configuration file (netlify.toml/yml/json)')
28
+ log(`Please verify functions.directory is set in your Netlify configuration file ${relConfigFilePath}`)
29
29
  log('See https://docs.netlify.com/configure-builds/file-based-configuration/ for more information')
30
30
  exit(1)
31
31
  }
@@ -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,8 +70,7 @@ 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.js').ServerSettings>} */
73
- let settings = {}
73
+ let settings = /** @type {import('../../utils/types.js').ServerSettings} */ ({})
74
74
  try {
75
75
  settings = await detectServerSettings(devConfig, options, command)
76
76
 
@@ -88,11 +88,9 @@ const serve = async (options, command) => {
88
88
  )
89
89
 
90
90
  const { configPath: configPathOverride } = await runBuildTimeline({
91
- cachedConfig,
92
- options,
91
+ command,
93
92
  settings,
94
- projectDir: command.workingDir,
95
- site,
93
+ options,
96
94
  })
97
95
 
98
96
  await startFunctionsServer({
@@ -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://64ae60d920fd0f000865bcfc--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',
@@ -139,6 +139,7 @@ export const initializeProxy = async ({
139
139
 
140
140
  // Setting header with geolocation and site info.
141
141
  req.headers[headers.Geo] = Buffer.from(JSON.stringify(geoLocation)).toString('base64')
142
+ req.headers[headers.DeployID] = '0'
142
143
  req.headers[headers.Site] = createSiteInfoHeader(siteInfo)
143
144
  req.headers[headers.Account] = createAccountInfoHeader({ id: accountId })
144
145
 
@@ -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,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(
@@ -1,5 +1,86 @@
1
1
  // @ts-check
2
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
+
3
84
  /**
4
85
  * Detects and filters the build setting for a project and a command
5
86
  * @param {import('../commands/base-command.mjs').default} command