netlify-cli 12.6.0 → 12.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,311 +1,35 @@
1
1
  // @ts-check
2
- import events from 'events'
3
- import path from 'path'
4
2
  import process from 'process'
5
3
 
6
- import fastifyStatic from '@fastify/static'
7
- import boxen from 'boxen'
8
4
  import { Option } from 'commander'
9
- import execa from 'execa'
10
- import Fastify from 'fastify'
11
- import stripAnsiCc from 'strip-ansi-control-characters'
12
- import waitPort from 'wait-port'
13
5
 
14
6
  import { promptEditorHelper } from '../../lib/edge-functions/editor-helper.mjs'
15
7
  import { startFunctionsServer } from '../../lib/functions/server.mjs'
16
- import {
17
- OneGraphCliClient,
18
- loadCLISession,
19
- markCliSessionInactive,
20
- persistNewOperationsDocForSession,
21
- startOneGraphCLISession,
22
- } from '../../lib/one-graph/cli-client.mjs'
23
- import {
24
- defaultExampleOperationsDoc,
25
- getGraphEditUrlBySiteId,
26
- getNetlifyGraphConfig,
27
- readGraphQLOperationsSourceFile,
28
- } from '../../lib/one-graph/cli-netlify-graph.mjs'
29
- import { startSpinner, stopSpinner } from '../../lib/spinner.mjs'
8
+ import { printBanner } from '../../utils/banner.mjs'
30
9
  import {
31
10
  BANG,
32
11
  chalk,
33
- error,
34
12
  exit,
35
- getToken,
36
13
  log,
37
14
  NETLIFYDEV,
38
15
  NETLIFYDEVERR,
39
16
  NETLIFYDEVLOG,
40
17
  NETLIFYDEVWARN,
41
18
  normalizeConfig,
42
- warn,
43
- watchDebounced,
44
19
  } from '../../utils/command-helpers.mjs'
45
- import detectServerSettings from '../../utils/detect-server-settings.mjs'
46
- import { generateNetlifyGraphJWT, getSiteInformation, injectEnvVariables, processOnExit } from '../../utils/dev.mjs'
20
+ import detectServerSettings, { getConfigWithPlugins } from '../../utils/detect-server-settings.mjs'
21
+ import { getSiteInformation, injectEnvVariables } from '../../utils/dev.mjs'
47
22
  import { getEnvelopeEnv, normalizeContext } from '../../utils/env/index.mjs'
48
23
  import { ensureNetlifyIgnore } from '../../utils/gitignore.mjs'
24
+ import { startNetlifyGraph, startPollingForAPIAuthentication } from '../../utils/graph.mjs'
49
25
  import { startLiveTunnel } from '../../utils/live-tunnel.mjs'
50
26
  import openBrowser from '../../utils/open-browser.mjs'
51
- import { startProxy } from '../../utils/proxy.mjs'
27
+ import { generateInspectSettings, startProxyServer } from '../../utils/proxy-server.mjs'
28
+ import { runDevTimeline } from '../../utils/run-build.mjs'
29
+ import { getGeoCountryArgParser } from '../../utils/validation.mjs'
52
30
 
53
31
  import { createDevExecCommand } from './dev-exec.mjs'
54
32
 
55
- const netlifyBuildPromise = import('@netlify/build')
56
-
57
- const startStaticServer = async ({ settings }) => {
58
- const server = Fastify()
59
- const rootPath = path.resolve(settings.dist)
60
- server.register(fastifyStatic, {
61
- root: rootPath,
62
- etag: false,
63
- acceptRanges: false,
64
- lastModified: false,
65
- })
66
-
67
- server.setNotFoundHandler((_req, res) => {
68
- res.code(404).sendFile('404.html', rootPath)
69
- })
70
-
71
- server.addHook('onRequest', (req, reply, done) => {
72
- reply.header('X-Powered-by', 'netlify-dev')
73
- const validMethods = ['GET', 'HEAD']
74
- if (!validMethods.includes(req.method)) {
75
- reply.code(405).send('Method Not Allowed')
76
- }
77
- done()
78
- })
79
-
80
- await server.listen({ port: settings.frameworkPort })
81
- log(`\n${NETLIFYDEVLOG} Static server listening to`, settings.frameworkPort)
82
- }
83
-
84
- const isNonExistingCommandError = ({ command, error: commandError }) => {
85
- // `ENOENT` is only returned for non Windows systems
86
- // See https://github.com/sindresorhus/execa/pull/447
87
- if (commandError.code === 'ENOENT') {
88
- return true
89
- }
90
-
91
- // if the command is a package manager we let it report the error
92
- if (['yarn', 'npm'].includes(command)) {
93
- return false
94
- }
95
-
96
- // this only works on English versions of Windows
97
- return (
98
- typeof commandError.message === 'string' &&
99
- commandError.message.includes('is not recognized as an internal or external command')
100
- )
101
- }
102
-
103
- /**
104
- * @type {(() => Promise<void>)[]} - array of functions to run before the process exits
105
- */
106
- const cleanupWork = []
107
-
108
- let cleanupStarted = false
109
-
110
- /**
111
- * @param {object} input
112
- * @param {number=} input.exitCode The exit code to return when exiting the process after cleanup
113
- */
114
- const cleanupBeforeExit = async ({ exitCode }) => {
115
- // If cleanup has started, then wherever started it will be responsible for exiting
116
- if (!cleanupStarted) {
117
- cleanupStarted = true
118
- try {
119
- // eslint-disable-next-line no-unused-vars
120
- const cleanupFinished = await Promise.all(cleanupWork.map((cleanup) => cleanup()))
121
- } finally {
122
- process.exit(exitCode)
123
- }
124
- }
125
- }
126
-
127
- /**
128
- * Run a command and pipe stdout, stderr and stdin
129
- * @param {string} command
130
- * @param {NodeJS.ProcessEnv} env
131
- * @returns {execa.ExecaChildProcess<string>}
132
- */
133
- const runCommand = (command, env = {}, spinner = null) => {
134
- const commandProcess = execa.command(command, {
135
- preferLocal: true,
136
- // we use reject=false to avoid rejecting synchronously when the command doesn't exist
137
- reject: false,
138
- env,
139
- // windowsHide needs to be false for child process to terminate properly on Windows
140
- windowsHide: false,
141
- })
142
-
143
- // This ensures that an active spinner stays at the bottom of the commandline
144
- // even though the actual framework command might be outputting stuff
145
- const pipeDataWithSpinner = (writeStream, chunk) => {
146
- if (spinner && spinner.isSpinning) {
147
- spinner.clear()
148
- spinner.isSilent = true
149
- }
150
- writeStream.write(chunk, () => {
151
- if (spinner && spinner.isSpinning) {
152
- spinner.isSilent = false
153
- spinner.render()
154
- }
155
- })
156
- }
157
-
158
- commandProcess.stdout.pipe(stripAnsiCc.stream()).on('data', pipeDataWithSpinner.bind(null, process.stdout))
159
- commandProcess.stderr.pipe(stripAnsiCc.stream()).on('data', pipeDataWithSpinner.bind(null, process.stderr))
160
- process.stdin.pipe(commandProcess.stdin)
161
-
162
- // we can't try->await->catch since we don't want to block on the framework server which
163
- // is a long running process
164
- // eslint-disable-next-line promise/catch-or-return
165
- commandProcess
166
- // eslint-disable-next-line promise/prefer-await-to-then
167
- .then(async () => {
168
- const result = await commandProcess
169
- const [commandWithoutArgs] = command.split(' ')
170
- if (result.failed && isNonExistingCommandError({ command: commandWithoutArgs, error: result })) {
171
- log(
172
- NETLIFYDEVERR,
173
- `Failed running command: ${command}. Please verify ${chalk.magenta(`'${commandWithoutArgs}'`)} exists`,
174
- )
175
- } else {
176
- const errorMessage = result.failed
177
- ? `${NETLIFYDEVERR} ${result.shortMessage}`
178
- : `${NETLIFYDEVWARN} "${command}" exited with code ${result.exitCode}`
179
-
180
- log(`${errorMessage}. Shutting down Netlify Dev server`)
181
- }
182
-
183
- return await cleanupBeforeExit({ exitCode: 1 })
184
- })
185
- processOnExit(async () => await cleanupBeforeExit({}))
186
-
187
- return commandProcess
188
- }
189
-
190
- /**
191
- * @typedef StartReturnObject
192
- * @property {4 | 6 | undefined=} ipVersion The version the open port was found on
193
- */
194
-
195
- /**
196
- * Start a static server if the `useStaticServer` is provided or a framework specific server
197
- * @param {object} config
198
- * @param {Partial<import('../../utils/types').ServerSettings>} config.settings
199
- * @returns {Promise<StartReturnObject>}
200
- */
201
- const startFrameworkServer = async function ({ settings }) {
202
- if (settings.useStaticServer) {
203
- if (settings.command) {
204
- runCommand(settings.command, settings.env)
205
- }
206
- await startStaticServer({ settings })
207
-
208
- return {}
209
- }
210
-
211
- log(`${NETLIFYDEVLOG} Starting Netlify Dev with ${settings.framework || 'custom config'}`)
212
-
213
- const spinner = startSpinner({
214
- text: `Waiting for framework port ${settings.frameworkPort}. This can be configured using the 'targetPort' property in the netlify.toml`,
215
- })
216
-
217
- runCommand(settings.command, settings.env, spinner)
218
-
219
- let port
220
- try {
221
- port = await waitPort({
222
- port: settings.frameworkPort,
223
- host: 'localhost',
224
- output: 'silent',
225
- timeout: FRAMEWORK_PORT_TIMEOUT,
226
- ...(settings.pollingStrategies.includes('HTTP') && { protocol: 'http' }),
227
- })
228
-
229
- if (!port.open) {
230
- throw new Error(`Timed out waiting for port '${settings.frameworkPort}' to be open`)
231
- }
232
-
233
- stopSpinner({ error: false, spinner })
234
- } catch (error_) {
235
- stopSpinner({ error: true, spinner })
236
- log(NETLIFYDEVERR, `Netlify Dev could not start or connect to localhost:${settings.frameworkPort}.`)
237
- log(NETLIFYDEVERR, `Please make sure your framework server is running on port ${settings.frameworkPort}`)
238
- error(error_)
239
- exit(1)
240
- }
241
-
242
- return { ipVersion: port?.ipVersion }
243
- }
244
-
245
- // 10 minutes
246
- const FRAMEWORK_PORT_TIMEOUT = 6e5
247
-
248
- /**
249
- * @typedef {Object} InspectSettings
250
- * @property {boolean} enabled - Inspect enabled
251
- * @property {boolean} pause - Pause on breakpoints
252
- * @property {string|undefined} address - Host/port override (optional)
253
- */
254
-
255
- /**
256
- *
257
- * @param {object} params
258
- * @param {*} params.addonsUrls
259
- * @param {import('../base-command.mjs').NetlifyOptions["config"]} params.config
260
- * @param {import('../base-command.mjs').NetlifyOptions["cachedConfig"]['env']} params.env
261
- * @param {InspectSettings} params.inspectSettings
262
- * @param {() => Promise<object>} params.getUpdatedConfig
263
- * @param {string} params.geolocationMode
264
- * @param {string} params.geoCountry
265
- * @param {*} params.settings
266
- * @param {boolean} params.offline
267
- * @param {*} params.site
268
- * @param {*} params.siteInfo
269
- * @param {import('../../utils/state-config.mjs').default} params.state
270
- * @returns
271
- */
272
- const startProxyServer = async ({
273
- addonsUrls,
274
- config,
275
- env,
276
- geoCountry,
277
- geolocationMode,
278
- getUpdatedConfig,
279
- inspectSettings,
280
- offline,
281
- settings,
282
- site,
283
- siteInfo,
284
- state,
285
- }) => {
286
- const url = await startProxy({
287
- addonsUrls,
288
- config,
289
- configPath: site.configPath,
290
- env,
291
- geolocationMode,
292
- geoCountry,
293
- getUpdatedConfig,
294
- inspectSettings,
295
- offline,
296
- projectDir: site.root,
297
- settings,
298
- state,
299
- siteInfo,
300
- })
301
- if (!url) {
302
- log(NETLIFYDEVERR, `Unable to start proxy server on port '${settings.port}'`)
303
- exit(1)
304
- }
305
-
306
- return url
307
- }
308
-
309
33
  /**
310
34
  *
311
35
  * @param {object} config
@@ -327,83 +51,6 @@ const handleLiveTunnel = async ({ api, options, settings, site }) => {
327
51
  }
328
52
  }
329
53
 
330
- const printBanner = ({ url }) => {
331
- const banner = chalk.bold(`${NETLIFYDEVLOG} Server now ready on ${url}`)
332
-
333
- log(
334
- boxen(banner, {
335
- padding: 1,
336
- margin: 1,
337
- align: 'center',
338
- borderColor: '#00c7b7',
339
- }),
340
- )
341
- }
342
-
343
- const startPollingForAPIAuthentication = async function (options) {
344
- const { api, command, config, site, siteInfo } = options
345
- const frequency = 5000
346
-
347
- const helper = async (maybeSiteData) => {
348
- const siteData = await (maybeSiteData || api.getSite({ siteId: site.id }))
349
- const authlifyTokenId = siteData && siteData.authlify_token_id
350
-
351
- const existingAuthlifyTokenId = config && config.netlifyGraphConfig && config.netlifyGraphConfig.authlifyTokenId
352
- if (authlifyTokenId && authlifyTokenId !== existingAuthlifyTokenId) {
353
- const netlifyToken = await command.authenticate()
354
- // Only inject the authlify config if a token ID exists. This prevents
355
- // calling command.authenticate() (which opens a browser window) if the
356
- // user hasn't enabled API Authentication
357
- const netlifyGraphConfig = {
358
- netlifyToken,
359
- authlifyTokenId: siteData.authlify_token_id,
360
- siteId: site.id,
361
- }
362
- config.netlifyGraphConfig = netlifyGraphConfig
363
-
364
- const netlifyGraphJWT = generateNetlifyGraphJWT(netlifyGraphConfig)
365
-
366
- if (netlifyGraphJWT != null) {
367
- // XXX(anmonteiro): this name is deprecated. Delete after 3/31/2022
368
- process.env.ONEGRAPH_AUTHLIFY_TOKEN = netlifyGraphJWT
369
- process.env.NETLIFY_GRAPH_TOKEN = netlifyGraphJWT
370
- }
371
- } else if (!authlifyTokenId) {
372
- // If there's no `authlifyTokenId`, it's because the user disabled API
373
- // Auth. Delete the config in this case.
374
- delete config.netlifyGraphConfig
375
- }
376
-
377
- setTimeout(helper, frequency)
378
- }
379
-
380
- await helper(siteInfo)
381
- }
382
-
383
- /**
384
- * @param {boolean|string} edgeInspect
385
- * @param {boolean|string} edgeInspectBrk
386
- * @returns {InspectSettings}
387
- */
388
- const generateInspectSettings = (edgeInspect, edgeInspectBrk) => {
389
- const enabled = Boolean(edgeInspect) || Boolean(edgeInspectBrk)
390
- const pause = Boolean(edgeInspectBrk)
391
- const getAddress = () => {
392
- if (edgeInspect) {
393
- return typeof edgeInspect === 'string' ? edgeInspect : undefined
394
- }
395
- if (edgeInspectBrk) {
396
- return typeof edgeInspectBrk === 'string' ? edgeInspectBrk : undefined
397
- }
398
- }
399
-
400
- return {
401
- enabled,
402
- pause,
403
- address: getAddress(),
404
- }
405
- }
406
-
407
54
  const validateShortFlagArgs = (args) => {
408
55
  if (args.startsWith('=')) {
409
56
  throw new Error(
@@ -420,19 +67,6 @@ const validateShortFlagArgs = (args) => {
420
67
  return args
421
68
  }
422
69
 
423
- const validateGeoCountryCode = (arg) => {
424
- // Validate that the arg passed is two letters only for country
425
- // See https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes
426
- if (!/^[a-z]{2}$/i.test(arg)) {
427
- throw new Error(
428
- `The geo country code must use a two letter abbreviation.
429
- ${chalk.red(BANG)} Example:
430
- netlify dev --geo=mock --country=FR`,
431
- )
432
- }
433
- return arg.toUpperCase()
434
- }
435
-
436
70
  /**
437
71
  * The dev command
438
72
  * @param {import('commander').OptionValues} options
@@ -441,7 +75,6 @@ const validateGeoCountryCode = (arg) => {
441
75
  const dev = async (options, command) => {
442
76
  log(`${NETLIFYDEV}`)
443
77
  const { api, cachedConfig, config, repositoryRoot, site, siteInfo, state } = command.netlify
444
- const netlifyBuild = await netlifyBuildPromise
445
78
  config.dev = { ...config.dev }
446
79
  config.build = { ...config.build }
447
80
  /** @type {import('./types').DevConfig} */
@@ -455,6 +88,8 @@ const dev = async (options, command) => {
455
88
 
456
89
  let { env } = cachedConfig
457
90
 
91
+ env.NETLIFY_DEV = { sources: ['internal'], value: 'true' }
92
+
458
93
  if (!options.offline && siteInfo.use_envelope) {
459
94
  env = await getEnvelopeEnv({ api, context: options.context, env, siteInfo })
460
95
  log(`${NETLIFYDEVLOG} Injecting environment variable values for ${chalk.yellow('all scopes')}`)
@@ -476,25 +111,7 @@ const dev = async (options, command) => {
476
111
  try {
477
112
  settings = await detectServerSettings(devConfig, options, site.root)
478
113
 
479
- // If there are plugins that we should be running for this site, add them
480
- // to the config as if they were declared in netlify.toml. We must check
481
- // whether the plugin has already been added by another source (like the
482
- // TOML file or the UI), as we don't want to run the same plugin twice.
483
- if (settings.plugins) {
484
- const { plugins: existingPlugins = [] } = cachedConfig.config
485
- const existingPluginNames = new Set(existingPlugins.map((plugin) => plugin.package))
486
- const newPlugins = settings.plugins
487
- .map((pluginName) => {
488
- if (existingPluginNames.has(pluginName)) {
489
- return
490
- }
491
-
492
- return { package: pluginName, origin: 'config', inputs: {} }
493
- })
494
- .filter(Boolean)
495
-
496
- cachedConfig.config.plugins = [...newPlugins, ...cachedConfig.config.plugins]
497
- }
114
+ cachedConfig.config = getConfigWithPlugins(cachedConfig.config, settings)
498
115
  } catch (error_) {
499
116
  log(NETLIFYDEVERR, error_.message)
500
117
  exit(1)
@@ -507,10 +124,15 @@ const dev = async (options, command) => {
507
124
  startPollingForAPIAuthentication({ api, command, config, site, siteInfo })
508
125
  }
509
126
 
127
+ log(`${NETLIFYDEVWARN} Setting up local development server`)
128
+
129
+ const { configPath: configPathOverride } = await runDevTimeline({ cachedConfig, options, settings, site })
130
+
510
131
  await startFunctionsServer({
511
132
  api,
512
133
  command,
513
134
  config,
135
+ debug: options.debug,
514
136
  settings,
515
137
  site,
516
138
  siteInfo,
@@ -519,23 +141,6 @@ const dev = async (options, command) => {
519
141
  timeouts,
520
142
  })
521
143
 
522
- log(`${NETLIFYDEVWARN} Setting up local development server`)
523
-
524
- const devCommand = async () => {
525
- const { ipVersion } = await startFrameworkServer({ settings })
526
-
527
- settings.frameworkHost = ipVersion === 6 ? '::1' : '127.0.0.1'
528
- }
529
- const startDevOptions = getBuildOptions({
530
- cachedConfig,
531
- options,
532
- })
533
- const { error: startDevError, success } = await netlifyBuild.startDev(devCommand, startDevOptions)
534
-
535
- if (!success) {
536
- error(`Could not start local development server\n\n${startDevError.message}\n\n${startDevError.stack}`)
537
- }
538
-
539
144
  // Try to add `.netlify` to `.gitignore`.
540
145
  try {
541
146
  await ensureNetlifyIgnore(repositoryRoot)
@@ -557,7 +162,8 @@ const dev = async (options, command) => {
557
162
  let url = await startProxyServer({
558
163
  addonsUrls,
559
164
  config,
560
- env: command.netlify.cachedConfig.env,
165
+ configPath: configPathOverride,
166
+ env,
561
167
  geolocationMode: options.geo,
562
168
  geoCountry: options.country,
563
169
  getUpdatedConfig,
@@ -579,121 +185,19 @@ const dev = async (options, command) => {
579
185
  process.env.URL = url
580
186
  process.env.DEPLOY_URL = url
581
187
 
582
- if (startNetlifyGraphWatcher && options.offline) {
583
- warn(`Unable to start Netlify Graph in offline mode`)
584
- } else if (startNetlifyGraphWatcher && !site.id) {
585
- error(
586
- `No siteId defined, unable to start Netlify Graph. To enable, run ${chalk.yellow(
587
- 'netlify init',
588
- )} or ${chalk.yellow('netlify link')}.`,
589
- )
590
- } else if (startNetlifyGraphWatcher) {
591
- const netlifyToken = await command.authenticate()
592
- await OneGraphCliClient.ensureAppForSite(netlifyToken, site.id)
593
-
594
- let stopWatchingCLISessions
595
-
596
- let liveConfig = { ...config }
597
- let isRestartingSession = false
598
-
599
- const createOrResumeSession = async function () {
600
- const netlifyGraphConfig = await getNetlifyGraphConfig({ command, options, settings })
601
-
602
- let graphqlDocument = readGraphQLOperationsSourceFile(netlifyGraphConfig)
603
-
604
- if (!graphqlDocument || graphqlDocument.trim().length === 0) {
605
- graphqlDocument = defaultExampleOperationsDoc
606
- }
607
-
608
- stopWatchingCLISessions = await startOneGraphCLISession({
609
- config: liveConfig,
610
- netlifyGraphConfig,
611
- netlifyToken,
612
- site,
613
- state,
614
- oneGraphSessionId: options.sessionId,
615
- })
616
-
617
- // Should be created by startOneGraphCLISession
618
- const oneGraphSessionId = loadCLISession(state)
619
-
620
- await persistNewOperationsDocForSession({
621
- config: liveConfig,
622
- netlifyGraphConfig,
623
- netlifyToken,
624
- oneGraphSessionId,
625
- operationsDoc: graphqlDocument,
626
- siteId: site.id,
627
- siteRoot: site.root,
628
- })
629
-
630
- return oneGraphSessionId
631
- }
632
-
633
- const configWatcher = new events.EventEmitter()
634
-
635
- // Only set up a watcher if we know the config path.
636
- const { configPath } = command.netlify.site
637
- if (configPath) {
638
- // chokidar handle
639
- command.configWatcherHandle = await watchDebounced(configPath, {
640
- depth: 1,
641
- onChange: async () => {
642
- const cwd = options.cwd || process.cwd()
643
- const [token] = await getToken(options.auth)
644
- const { config: newConfig } = await command.getConfig({ cwd, state, token, ...command.netlify.apiUrlOpts })
645
-
646
- const normalizedNewConfig = normalizeConfig(newConfig)
647
- configWatcher.emit('change', normalizedNewConfig)
648
- },
649
- })
650
-
651
- processOnExit(async () => {
652
- await command.configWatcherHandle.close()
653
- })
654
- }
655
-
656
- // Set up a handler for config changes.
657
- configWatcher.on('change', async (newConfig) => {
658
- command.netlify.config = newConfig
659
- liveConfig = newConfig
660
- if (isRestartingSession) {
661
- return
662
- }
663
- stopWatchingCLISessions && stopWatchingCLISessions()
664
- isRestartingSession = true
665
- await createOrResumeSession()
666
- isRestartingSession = false
667
- })
668
-
669
- const oneGraphSessionId = await createOrResumeSession()
670
- const cleanupSession = () => markCliSessionInactive({ netlifyToken, sessionId: oneGraphSessionId, siteId: site.id })
671
-
672
- cleanupWork.push(cleanupSession)
673
-
674
- const graphEditUrl = getGraphEditUrlBySiteId({ siteId: site.id, oneGraphSessionId })
675
-
676
- log(
677
- `Starting Netlify Graph session, to edit your library visit ${graphEditUrl} or run \`netlify graph:edit\` in another tab`,
678
- )
679
- }
188
+ await startNetlifyGraph({
189
+ command,
190
+ config,
191
+ options,
192
+ settings,
193
+ site,
194
+ startNetlifyGraphWatcher,
195
+ state,
196
+ })
680
197
 
681
198
  printBanner({ url })
682
199
  }
683
200
 
684
- const getBuildOptions = ({ cachedConfig, options: { context, cwd = process.cwd(), debug, dry, offline }, token }) => ({
685
- cachedConfig,
686
- token,
687
- dry,
688
- debug,
689
- context,
690
- mode: 'cli',
691
- telemetry: false,
692
- buffer: false,
693
- offline,
694
- cwd,
695
- })
696
-
697
201
  /**
698
202
  * Creates the `netlify dev` command
699
203
  * @param {import('../base-command.mjs').default} program
@@ -713,7 +217,6 @@ export const createDevCommand = (program) => {
713
217
  '--context <context>',
714
218
  'Specify a deploy context or branch for environment variables (contexts: "production", "deploy-preview", "branch-deploy", "dev")',
715
219
  normalizeContext,
716
- 'dev',
717
220
  )
718
221
  .option('-p ,--port <port>', 'port of netlify dev', (value) => Number.parseInt(value))
719
222
  .option('--targetPort <port>', 'port of target app server', (value) => Number.parseInt(value))
@@ -735,7 +238,7 @@ export const createDevCommand = (program) => {
735
238
  new Option(
736
239
  '--country <geoCountry>',
737
240
  'Two-letter country code (https://ntl.fyi/country-codes) to use as mock geolocation (enables --geo=mock automatically)',
738
- ).argParser(validateGeoCountryCode),
241
+ ).argParser(getGeoCountryArgParser('netlify dev --geo=mock --country=FR')),
739
242
  )
740
243
  .addOption(
741
244
  new Option('--staticServerPort <port>', 'port of the static app server used when no framework is detected')
@@ -16,8 +16,11 @@ const functionsServe = async (options, command) => {
16
16
  const { api, config, site, siteInfo } = command.netlify
17
17
 
18
18
  const functionsDir = getFunctionsDir({ options, config }, join('netlify', 'functions'))
19
+ const { env } = command.netlify.cachedConfig
19
20
 
20
- await injectEnvVariables({ devConfig: { ...config.dev }, env: command.netlify.cachedConfig.env, site })
21
+ env.NETLIFY_DEV = { sources: ['internal'], value: 'true' }
22
+
23
+ await injectEnvVariables({ devConfig: { ...config.dev }, env, site })
21
24
 
22
25
  const { capabilities, siteUrl, timeouts } = await getSiteInformation({
23
26
  offline: options.offline,
@@ -34,6 +37,7 @@ const functionsServe = async (options, command) => {
34
37
 
35
38
  await startFunctionsServer({
36
39
  config,
40
+ debug: options.debug,
37
41
  api,
38
42
  settings: { functions: functionsDir, functionsPort },
39
43
  site,
@@ -28,6 +28,7 @@ import { createLoginCommand } from './login/index.mjs'
28
28
  import { createLogoutCommand } from './logout/index.mjs'
29
29
  import { createOpenCommand } from './open/index.mjs'
30
30
  import { createRecipesCommand } from './recipes/index.mjs'
31
+ import { createServeCommand } from './serve/serve.mjs'
31
32
  import { createSitesCommand } from './sites/index.mjs'
32
33
  import { createStatusCommand } from './status/index.mjs'
33
34
  import { createSwitchCommand } from './switch/index.mjs'
@@ -173,6 +174,7 @@ export const createMainCommand = () => {
173
174
  createLoginCommand(program)
174
175
  createLogoutCommand(program)
175
176
  createOpenCommand(program)
177
+ createServeCommand(program)
176
178
  createSitesCommand(program)
177
179
  createStatusCommand(program)
178
180
  createSwitchCommand(program)