netlify-cli 16.0.3 → 16.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
3
  "description": "Netlify command line tool",
4
- "version": "16.0.3",
4
+ "version": "16.2.0",
5
5
  "author": "Netlify Inc.",
6
6
  "type": "module",
7
7
  "engines": {
@@ -44,13 +44,13 @@
44
44
  "dependencies": {
45
45
  "@bugsnag/js": "7.20.2",
46
46
  "@fastify/static": "6.10.2",
47
- "@netlify/build": "29.20.4",
48
- "@netlify/build-info": "7.7.3",
47
+ "@netlify/build": "29.20.8",
48
+ "@netlify/build-info": "7.7.4",
49
49
  "@netlify/config": "20.8.0",
50
- "@netlify/edge-bundler": "8.17.1",
50
+ "@netlify/edge-bundler": "8.19.0",
51
51
  "@netlify/local-functions-proxy": "1.1.1",
52
- "@netlify/serverless-functions-api": "1.5.2",
53
- "@netlify/zip-it-and-ship-it": "9.15.1",
52
+ "@netlify/serverless-functions-api": "1.7.3",
53
+ "@netlify/zip-it-and-ship-it": "9.17.0",
54
54
  "@octokit/rest": "19.0.13",
55
55
  "ansi-escapes": "6.2.0",
56
56
  "ansi-styles": "6.2.1",
@@ -106,7 +106,7 @@
106
106
  "isexe": "2.0.0",
107
107
  "jsonwebtoken": "9.0.1",
108
108
  "jwt-decode": "3.1.2",
109
- "lambda-local": "2.1.1",
109
+ "lambda-local": "2.1.2",
110
110
  "listr": "0.14.3",
111
111
  "locate-path": "7.2.0",
112
112
  "lodash": "4.17.21",
@@ -116,7 +116,7 @@
116
116
  "multiparty": "4.2.3",
117
117
  "netlify": "13.1.10",
118
118
  "netlify-headers-parser": "7.1.2",
119
- "netlify-redirect-parser": "14.1.3",
119
+ "netlify-redirect-parser": "14.2.0",
120
120
  "netlify-redirector": "0.4.0",
121
121
  "node-fetch": "2.6.12",
122
122
  "node-version-alias": "3.4.1",
@@ -574,7 +574,7 @@ export default class BaseCommand extends Command {
574
574
  // configuration file and the API
575
575
  // ==================================================
576
576
  const cachedConfig = await actionCommand.getConfig({
577
- cwd: this.jsWorkspaceRoot || this.workingDir,
577
+ cwd: flags.cwd ? this.workingDir : this.jsWorkspaceRoot || this.workingDir,
578
578
  repositoryRoot: rootDir,
579
579
  packagePath: this.workspacePackage,
580
580
  // The config flag needs to be resolved from the actual process working directory
@@ -161,7 +161,7 @@ const dev = async (options, command) => {
161
161
  },
162
162
  })
163
163
 
164
- await startFunctionsServer({
164
+ const functionsRegistry = await startFunctionsServer({
165
165
  api,
166
166
  command,
167
167
  config,
@@ -217,6 +217,7 @@ const dev = async (options, command) => {
217
217
  geolocationMode: options.geo,
218
218
  geoCountry: options.country,
219
219
  accountId,
220
+ functionsRegistry,
220
221
  })
221
222
 
222
223
  if (devConfig.autoLaunch !== false) {
@@ -93,7 +93,7 @@ const serve = async (options, command) => {
93
93
  options,
94
94
  })
95
95
 
96
- await startFunctionsServer({
96
+ const functionsRegistry = await startFunctionsServer({
97
97
  api,
98
98
  command,
99
99
  config,
@@ -132,7 +132,9 @@ const serve = async (options, command) => {
132
132
  addonsUrls,
133
133
  config,
134
134
  configPath: configPathOverride,
135
+ debug: options.debug,
135
136
  env,
137
+ functionsRegistry,
136
138
  geolocationMode: options.geo,
137
139
  geoCountry: options.country,
138
140
  getUpdatedConfig,
@@ -1,5 +1,5 @@
1
1
  import { env } from 'process'
2
2
 
3
- const latestBootstrapURL = 'https://64c264287e9cbb0008621df3--edge.netlify.com/bootstrap/index-combined.ts'
3
+ const latestBootstrapURL = 'https://64e7783fce8cfe0008496c72--edge.netlify.com/bootstrap/index-combined.ts'
4
4
 
5
5
  export const getBootstrapURL = () => env.NETLIFY_EDGE_BOOTSTRAP || latestBootstrapURL
@@ -318,23 +318,38 @@ export class EdgeFunctionsRegistry {
318
318
  functions: this.#functions,
319
319
  featureFlags,
320
320
  })
321
- const invocationMetadata = {
322
- function_config: manifest.function_config,
323
- routes: manifest.routes.map((route) => ({ function: route.function, pattern: route.pattern })),
324
- }
325
321
  const routes = [...manifest.routes, ...manifest.post_cache_routes].map((route) => ({
326
322
  ...route,
327
323
  pattern: new RegExp(route.pattern),
328
324
  }))
329
- const functionNames = routes
330
- .filter(({ pattern }) => pattern.test(urlPath))
331
- .filter(({ function: name }) => {
332
- const isExcluded = manifest.function_config[name]?.excluded_patterns?.some((pattern) =>
333
- new RegExp(pattern).test(urlPath),
334
- )
335
- return !isExcluded
336
- })
337
- .map((route) => route.function)
325
+
326
+ /** @type string[] */
327
+ const functionNames = []
328
+
329
+ /** @type number[] */
330
+ const routeIndexes = []
331
+
332
+ routes.forEach((route, index) => {
333
+ if (!route.pattern.test(urlPath)) {
334
+ return
335
+ }
336
+
337
+ const isExcluded = manifest.function_config[route.function]?.excluded_patterns?.some((pattern) =>
338
+ new RegExp(pattern).test(urlPath),
339
+ )
340
+
341
+ if (isExcluded) {
342
+ return
343
+ }
344
+
345
+ functionNames.push(route.function)
346
+ routeIndexes.push(index)
347
+ })
348
+ const invocationMetadata = {
349
+ function_config: manifest.function_config,
350
+ req_routes: routeIndexes,
351
+ routes: manifest.routes.map((route) => ({ function: route.function, path: route.path, pattern: route.pattern })),
352
+ }
338
353
  const orphanedDeclarations = this.#matchURLPathAgainstOrphanedDeclarations(urlPath)
339
354
 
340
355
  return { functionNames, invocationMetadata, orphanedDeclarations }
@@ -158,6 +158,28 @@ export default class NetlifyFunction {
158
158
  }
159
159
  }
160
160
 
161
+ async matchURLPath(rawPath) {
162
+ await this.buildQueue
163
+
164
+ const path = (rawPath.endsWith('/') ? rawPath.slice(0, -1) : rawPath).toLowerCase()
165
+ const { routes = [] } = this.buildData
166
+ const isMatch = routes.some(({ expression, literal }) => {
167
+ if (literal !== undefined) {
168
+ return path === literal
169
+ }
170
+
171
+ if (expression !== undefined) {
172
+ const regex = new RegExp(expression)
173
+
174
+ return regex.test(path)
175
+ }
176
+
177
+ return false
178
+ })
179
+
180
+ return isMatch
181
+ }
182
+
161
183
  get url() {
162
184
  // This line fixes the issue here https://github.com/netlify/cli/issues/4116
163
185
  // Not sure why `settings.port` was used here nor does a valid reference exist.
@@ -122,6 +122,16 @@ export class FunctionsRegistry {
122
122
  return this.functions.get(name)
123
123
  }
124
124
 
125
+ async getFunctionForURLPath(urlPath) {
126
+ for (const func of this.functions.values()) {
127
+ const isMatch = await func.matchURLPath(urlPath)
128
+
129
+ if (isMatch) {
130
+ return func
131
+ }
132
+ }
133
+ }
134
+
125
135
  async registerFunction(name, funcBeforeHook) {
126
136
  const { runtime } = funcBeforeHook
127
137
 
@@ -58,6 +58,7 @@ const buildFunction = async ({
58
58
  includedFiles,
59
59
  inputs,
60
60
  path: functionPath,
61
+ routes,
61
62
  runtimeAPIVersion,
62
63
  schedule,
63
64
  } = await memoizedBuild({
@@ -81,7 +82,7 @@ const buildFunction = async ({
81
82
 
82
83
  clearFunctionsCache(targetDirectory)
83
84
 
84
- return { buildPath, includedFiles, runtimeAPIVersion, srcFiles, schedule }
85
+ return { buildPath, includedFiles, routes, runtimeAPIVersion, srcFiles, schedule }
85
86
  }
86
87
 
87
88
  /**
@@ -7,6 +7,7 @@ import jwtDecode from 'jwt-decode'
7
7
 
8
8
  import { NETLIFYDEVERR, NETLIFYDEVLOG, error as errorExit, log } from '../../utils/command-helpers.mjs'
9
9
  import { CLOCKWORK_USERAGENT, getFunctionsDistPath, getInternalFunctionsDir } from '../../utils/functions/index.mjs'
10
+ import { NFFunctionName } from '../../utils/headers.mjs'
10
11
  import { headers as efHeaders } from '../edge-functions/headers.mjs'
11
12
  import { getGeoLocation } from '../geo-location.mjs'
12
13
 
@@ -55,9 +56,20 @@ export const createHandler = function (options) {
55
56
  const { functionsRegistry } = options
56
57
 
57
58
  return async function handler(request, response) {
58
- // handle proxies without path re-writes (http-servr)
59
- const cleanPath = request.path.replace(/^\/.netlify\/(functions|builders)/, '')
60
- const functionName = cleanPath.split('/').find(Boolean)
59
+ // If this header is set, it means we've already matched a function and we
60
+ // can just grab its name directly. We delete the header from the request
61
+ // because we don't want to expose it to user code.
62
+ let functionName = request.header(NFFunctionName)
63
+ delete request.headers[NFFunctionName]
64
+
65
+ // If we didn't match a function with a custom route, let's try to match
66
+ // using the fixed URL format.
67
+ if (!functionName) {
68
+ const cleanPath = request.path.replace(/^\/.netlify\/(functions|builders)/, '')
69
+
70
+ functionName = cleanPath.split('/').find(Boolean)
71
+ }
72
+
61
73
  const func = functionsRegistry.get(functionName)
62
74
 
63
75
  if (func === undefined) {
@@ -231,7 +243,7 @@ const getFunctionsServer = (options) => {
231
243
  * @param {*} options.site
232
244
  * @param {string} options.siteUrl
233
245
  * @param {*} options.timeouts
234
- * @returns
246
+ * @returns {Promise<import('./registry.mjs').FunctionsRegistry | undefined>}
235
247
  */
236
248
  export const startFunctionsServer = async (options) => {
237
249
  const { capabilities, command, config, debug, loadDistFunctions, settings, site, siteUrl, timeouts } = options
@@ -272,9 +284,11 @@ export const startFunctionsServer = async (options) => {
272
284
 
273
285
  await functionsRegistry.scan(functionsDirectories)
274
286
 
275
- const server = await getFunctionsServer(Object.assign(options, { functionsRegistry }))
287
+ const server = getFunctionsServer(Object.assign(options, { functionsRegistry }))
276
288
 
277
289
  await startWebServer({ server, settings, debug })
290
+
291
+ return functionsRegistry
278
292
  }
279
293
 
280
294
  /**
@@ -46,4 +46,5 @@ const getErrorMessage = function ({ message }) {
46
46
  return message
47
47
  }
48
48
 
49
+ export const NFFunctionName = 'x-nf-function-name'
49
50
  export const NFRequestID = 'x-nf-request-id'
@@ -52,6 +52,7 @@ export const generateInspectSettings = (edgeInspect, edgeInspectBrk) => {
52
52
  * @param {*} params.siteInfo
53
53
  * @param {string} params.projectDir
54
54
  * @param {import('./state-config.mjs').default} params.state
55
+ * @param {import('../lib/functions/registry.mjs').FunctionsRegistry=} params.functionsRegistry
55
56
  * @returns
56
57
  */
57
58
  export const startProxyServer = async ({
@@ -61,6 +62,7 @@ export const startProxyServer = async ({
61
62
  configPath,
62
63
  debug,
63
64
  env,
65
+ functionsRegistry,
64
66
  geoCountry,
65
67
  geolocationMode,
66
68
  getUpdatedConfig,
@@ -78,6 +80,7 @@ export const startProxyServer = async ({
78
80
  configPath: configPath || site.configPath,
79
81
  debug,
80
82
  env,
83
+ functionsRegistry,
81
84
  geolocationMode,
82
85
  geoCountry,
83
86
  getUpdatedConfig,
@@ -31,7 +31,7 @@ import renderErrorTemplate from '../lib/render-error-template.mjs'
31
31
 
32
32
  import { NETLIFYDEVLOG, NETLIFYDEVWARN, log, chalk } from './command-helpers.mjs'
33
33
  import createStreamPromise from './create-stream-promise.mjs'
34
- import { headersForPath, parseHeaders, NFRequestID } from './headers.mjs'
34
+ import { headersForPath, parseHeaders, NFFunctionName, NFRequestID } from './headers.mjs'
35
35
  import { generateRequestID } from './request-id.mjs'
36
36
  import { createRewriter, onChanges } from './rules-proxy.mjs'
37
37
  import { signRedirect } from './sign-redirect.mjs'
@@ -75,19 +75,35 @@ const formatEdgeFunctionError = (errorBuffer, acceptsHtml) => {
75
75
  })
76
76
  }
77
77
 
78
- const isInternal = function (url) {
78
+ /**
79
+ * @param {string} url
80
+ */
81
+ function isInternal(url) {
79
82
  return url.startsWith('/.netlify/')
80
83
  }
81
- const isFunction = function (functionsPort, url) {
84
+
85
+ /**
86
+ * @param {boolean|number|undefined} functionsPort
87
+ * @param {string} url
88
+ */
89
+ function isFunction(functionsPort, url) {
82
90
  return functionsPort && url.match(/^\/.netlify\/(functions|builders)\/.+/)
83
91
  }
84
92
 
85
- const getAddonUrl = function (addonsUrls, req) {
86
- const matches = req.url.match(/^\/.netlify\/([^/]+)(\/.*)/)
93
+ /**
94
+ * @param {Record<string, string>} addonsUrls
95
+ * @param {http.IncomingMessage} req
96
+ */
97
+ function getAddonUrl(addonsUrls, req) {
98
+ const matches = req.url?.match(/^\/.netlify\/([^/]+)(\/.*)/)
87
99
  const addonUrl = matches && addonsUrls[matches[1]]
88
100
  return addonUrl ? `${addonUrl}${matches[2]}` : null
89
101
  }
90
102
 
103
+ /**
104
+ * @param {string} pathname
105
+ * @param {string} publicFolder
106
+ */
91
107
  const getStatic = async function (pathname, publicFolder) {
92
108
  const alternatives = [pathname, ...alternativePathsFor(pathname)].map((filePath) =>
93
109
  path.resolve(publicFolder, filePath.slice(1)),
@@ -165,7 +181,7 @@ const alternativePathsFor = function (url) {
165
181
  return paths
166
182
  }
167
183
 
168
- const serveRedirect = async function ({ env, match, options, proxy, req, res, siteInfo }) {
184
+ const serveRedirect = async function ({ env, functionsRegistry, match, options, proxy, req, res, siteInfo }) {
169
185
  if (!match) return proxy.web(req, res, options)
170
186
 
171
187
  options = options || req.proxyOptions || {}
@@ -198,6 +214,7 @@ const serveRedirect = async function ({ env, match, options, proxy, req, res, si
198
214
  if (isFunction(options.functionsPort, req.url)) {
199
215
  return proxy.web(req, res, { target: options.functionsServer })
200
216
  }
217
+
201
218
  const urlForAddons = getAddonUrl(options.addonsUrls, req)
202
219
  if (urlForAddons) {
203
220
  return handleAddonUrl({ req, res, addonUrl: urlForAddons })
@@ -311,22 +328,28 @@ const serveRedirect = async function ({ env, match, options, proxy, req, res, si
311
328
  return proxy.web(req, res, { target: options.functionsServer })
312
329
  }
313
330
 
331
+ const functionWithCustomRoute = functionsRegistry && (await functionsRegistry.getFunctionForURLPath(destURL))
314
332
  const destStaticFile = await getStatic(dest.pathname, options.publicFolder)
315
333
  let statusValue
316
- if (match.force || (!staticFile && ((!options.framework && destStaticFile) || isInternal(destURL)))) {
334
+ if (
335
+ match.force ||
336
+ (!staticFile && ((!options.framework && destStaticFile) || isInternal(destURL) || functionWithCustomRoute))
337
+ ) {
317
338
  req.url = destStaticFile ? destStaticFile + dest.search : destURL
318
339
  const { status } = match
319
340
  statusValue = status
320
341
  console.log(`${NETLIFYDEVLOG} Rewrote URL to`, req.url)
321
342
  }
322
343
 
323
- if (isFunction(options.functionsPort, req.url)) {
344
+ if (isFunction(options.functionsPort, req.url) || functionWithCustomRoute) {
345
+ const functionHeaders = functionWithCustomRoute ? { [NFFunctionName]: functionWithCustomRoute.name } : {}
324
346
  const url = reqToURL(req, originalURL)
325
347
  req.headers['x-netlify-original-pathname'] = url.pathname
326
348
  req.headers['x-netlify-original-search'] = url.search
327
349
 
328
- return proxy.web(req, res, { target: options.functionsServer })
350
+ return proxy.web(req, res, { headers: functionHeaders, target: options.functionsServer })
329
351
  }
352
+
330
353
  const addonUrl = getAddonUrl(options.addonsUrls, req)
331
354
  if (addonUrl) {
332
355
  return handleAddonUrl({ req, res, addonUrl })
@@ -418,12 +441,22 @@ const initializeProxy = async function ({ configPath, distDir, env, host, port,
418
441
  }
419
442
 
420
443
  if (proxyRes.statusCode === 404 || proxyRes.statusCode === 403) {
444
+ // If a request for `/path` has failed, we'll a few variations like
445
+ // `/path/index.html` to mimic the CDN behavior.
421
446
  if (req.alternativePaths && req.alternativePaths.length !== 0) {
422
447
  req.url = req.alternativePaths.shift()
423
448
  return proxy.web(req, res, req.proxyOptions)
424
449
  }
450
+
451
+ // The request has failed but we might still have a matching redirect
452
+ // rule (without `force`) that should kick in. This is how we mimic the
453
+ // file shadowing behavior from the CDN.
425
454
  if (req.proxyOptions && req.proxyOptions.match) {
426
455
  return serveRedirect({
456
+ // We don't want to match functions at this point because any redirects
457
+ // to functions will have already been processed, so we don't supply a
458
+ // functions registry to `serveRedirect`.
459
+ functionsRegistry: null,
427
460
  req,
428
461
  res,
429
462
  proxy: handlers,
@@ -437,7 +470,19 @@ const initializeProxy = async function ({ configPath, distDir, env, host, port,
437
470
 
438
471
  if (req.proxyOptions.staticFile && isRedirect({ status: proxyRes.statusCode }) && proxyRes.headers.location) {
439
472
  req.url = proxyRes.headers.location
440
- return serveRedirect({ req, res, proxy: handlers, match: null, options: req.proxyOptions, siteInfo, env })
473
+ return serveRedirect({
474
+ // We don't want to match functions at this point because any redirects
475
+ // to functions will have already been processed, so we don't supply a
476
+ // functions registry to `serveRedirect`.
477
+ functionsRegistry: null,
478
+ req,
479
+ res,
480
+ proxy: handlers,
481
+ match: null,
482
+ options: req.proxyOptions,
483
+ siteInfo,
484
+ env,
485
+ })
441
486
  }
442
487
 
443
488
  const responseData = []
@@ -535,7 +580,7 @@ const initializeProxy = async function ({ configPath, distDir, env, host, port,
535
580
  }
536
581
 
537
582
  const onRequest = async (
538
- { addonsUrls, edgeFunctionsProxy, env, functionsServer, proxy, rewriter, settings, siteInfo },
583
+ { addonsUrls, edgeFunctionsProxy, env, functionsRegistry, functionsServer, proxy, rewriter, settings, siteInfo },
539
584
  req,
540
585
  res,
541
586
  ) => {
@@ -549,9 +594,22 @@ const onRequest = async (
549
594
  return proxy.web(req, res, { target: edgeFunctionsProxyURL })
550
595
  }
551
596
 
597
+ // Does the request match a function on the fixed URL path?
552
598
  if (isFunction(settings.functionsPort, req.url)) {
553
599
  return proxy.web(req, res, { target: functionsServer })
554
600
  }
601
+
602
+ // Does the request match a function on a custom URL path?
603
+ const functionMatch = functionsRegistry ? await functionsRegistry.getFunctionForURLPath(req.url) : null
604
+
605
+ if (functionMatch) {
606
+ // Setting an internal header with the function name so that we don't
607
+ // have to match the URL again in the functions server.
608
+ const headers = { [NFFunctionName]: functionMatch.name }
609
+
610
+ return proxy.web(req, res, { headers, target: functionsServer })
611
+ }
612
+
555
613
  const addonUrl = getAddonUrl(addonsUrls, req)
556
614
  if (addonUrl) {
557
615
  return handleAddonUrl({ req, res, addonUrl })
@@ -575,7 +633,7 @@ const onRequest = async (
575
633
  // We don't want to generate an ETag for 3xx redirects.
576
634
  req[shouldGenerateETag] = ({ statusCode }) => statusCode < 300 || statusCode >= 400
577
635
 
578
- return serveRedirect({ req, res, proxy, match, options, siteInfo, env })
636
+ return serveRedirect({ req, res, proxy, match, options, siteInfo, env, functionsRegistry })
579
637
  }
580
638
 
581
639
  // The request will be served by the framework server, which means we want to
@@ -612,6 +670,7 @@ export const startProxy = async function ({
612
670
  configPath,
613
671
  debug,
614
672
  env,
673
+ functionsRegistry,
615
674
  geoCountry,
616
675
  geolocationMode,
617
676
  getUpdatedConfig,
@@ -665,6 +724,7 @@ export const startProxy = async function ({
665
724
  rewriter,
666
725
  settings,
667
726
  addonsUrls,
727
+ functionsRegistry,
668
728
  functionsServer,
669
729
  edgeFunctionsProxy,
670
730
  siteInfo,
@@ -80,7 +80,7 @@ export const runNetlifyBuild = async ({ command, env = {}, options, settings, ti
80
80
  const devCommand = async (settingsOverrides = {}) => {
81
81
  let cwd = command.workingDir
82
82
 
83
- if (command.project.workspace?.packages.length) {
83
+ if (!options.cwd && command.project.workspace?.packages.length) {
84
84
  cwd = join(command.project.jsWorkspaceRoot, settings.baseDirectory || '')
85
85
  }
86
86
 
@@ -13,14 +13,18 @@ import isValidEventName from './validation.mjs'
13
13
 
14
14
  const dirPath = dirname(fileURLToPath(import.meta.url))
15
15
 
16
- const send = function (type, payload) {
16
+ /**
17
+ * @param {'track' | 'identify'} type
18
+ * @param {object} payload
19
+ */
20
+ function send(type, payload) {
17
21
  const requestFile = join(dirPath, 'request.mjs')
18
22
  const options = JSON.stringify({
19
23
  data: payload,
20
24
  type,
21
25
  })
22
26
 
23
- const args = [process.execPath, [requestFile, options]]
27
+ const args = /** @type {const} */ ([process.execPath, [requestFile, options]])
24
28
  if (process.env.NETLIFY_TEST_TELEMETRY_WAIT === 'true') {
25
29
  return execa(...args, {
26
30
  stdio: 'inherit',
@@ -46,7 +50,12 @@ const eventConfig = {
46
50
  ],
47
51
  }
48
52
 
49
- export const track = async function (eventName, payload = {}) {
53
+ /**
54
+ * Tracks a custom event with the provided payload
55
+ * @param {string} eventName
56
+ * @param {{status?: string, duration?: number, [key: string]: unknown}} [payload]
57
+ */
58
+ export async function track(eventName, payload = {}) {
50
59
  if (isCI) {
51
60
  return
52
61
  }
@@ -83,7 +92,14 @@ export const track = async function (eventName, payload = {}) {
83
92
  return send('track', defaultData)
84
93
  }
85
94
 
86
- export const identify = async function (payload) {
95
+ /**
96
+ * @param {object} payload
97
+ * @param {string} payload.name
98
+ * @param {string} payload.email
99
+ * @param {string} payload.userId
100
+ * @returns
101
+ */
102
+ export async function identify(payload) {
87
103
  if (isCI) {
88
104
  return
89
105
  }
@@ -1,6 +1,10 @@
1
1
  // @ts-check
2
2
  import { BANG, chalk } from './command-helpers.mjs'
3
3
 
4
+ /**
5
+ * @param {string} exampleCommand
6
+ * @returns {(value:string, previous: unknown) => unknown}
7
+ */
4
8
  export const getGeoCountryArgParser = (exampleCommand) => (arg) => {
5
9
  // Validate that the arg passed is two letters only for country
6
10
  // See https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes