netlify-cli 12.6.0 → 12.7.1

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.
@@ -0,0 +1,189 @@
1
+ // @ts-check
2
+ import process from 'process'
3
+
4
+ import { Option } from 'commander'
5
+
6
+ import { promptEditorHelper } from '../../lib/edge-functions/editor-helper.mjs'
7
+ import { startFunctionsServer } from '../../lib/functions/server.mjs'
8
+ import { printBanner } from '../../utils/banner.mjs'
9
+ import {
10
+ chalk,
11
+ exit,
12
+ log,
13
+ NETLIFYDEVERR,
14
+ NETLIFYDEVLOG,
15
+ NETLIFYDEVWARN,
16
+ normalizeConfig,
17
+ } from '../../utils/command-helpers.mjs'
18
+ import detectServerSettings, { getConfigWithPlugins } from '../../utils/detect-server-settings.mjs'
19
+ import { getSiteInformation, injectEnvVariables } from '../../utils/dev.mjs'
20
+ import { getEnvelopeEnv, normalizeContext } from '../../utils/env/index.mjs'
21
+ import { getInternalFunctionsDir } from '../../utils/functions/functions.mjs'
22
+ import { ensureNetlifyIgnore } from '../../utils/gitignore.mjs'
23
+ import openBrowser from '../../utils/open-browser.mjs'
24
+ import { generateInspectSettings, startProxyServer } from '../../utils/proxy-server.mjs'
25
+ import { runBuildTimeline } from '../../utils/run-build.mjs'
26
+ import { getGeoCountryArgParser } from '../../utils/validation.mjs'
27
+
28
+ /**
29
+ * The serve command
30
+ * @param {import('commander').OptionValues} options
31
+ * @param {import('../base-command.mjs').default} command
32
+ */
33
+ const serve = async (options, command) => {
34
+ const { api, cachedConfig, config, repositoryRoot, site, siteInfo, state } = command.netlify
35
+ config.dev = { ...config.dev }
36
+ config.build = { ...config.build }
37
+ /** @type {import('../dev/types').DevConfig} */
38
+ const devConfig = {
39
+ ...(config.functionsDirectory && { functions: config.functionsDirectory }),
40
+ ...(config.build.publish && { publish: config.build.publish }),
41
+ ...config.dev,
42
+ ...options,
43
+ // Override the `framework` value so that we start a static server and not
44
+ // the framework's development server.
45
+ framework: '#static',
46
+ }
47
+
48
+ let { env } = cachedConfig
49
+
50
+ if (!options.offline && siteInfo.use_envelope) {
51
+ env = await getEnvelopeEnv({ api, context: options.context, env, siteInfo })
52
+ log(`${NETLIFYDEVLOG} Injecting environment variable values for ${chalk.yellow('all scopes')}`)
53
+ }
54
+
55
+ await injectEnvVariables({ devConfig, env, site })
56
+ await promptEditorHelper({ chalk, config, log, NETLIFYDEVLOG, repositoryRoot, state })
57
+
58
+ const { addonsUrls, capabilities, siteUrl, timeouts } = await getSiteInformation({
59
+ // inherited from base command --offline
60
+ offline: options.offline,
61
+ api,
62
+ site,
63
+ siteInfo,
64
+ })
65
+
66
+ // Ensure the internal functions directory exists so that the functions
67
+ // server and registry are initialized, and any functions created by
68
+ // Netlify Build are loaded.
69
+ await getInternalFunctionsDir({ base: site.root, ensureExists: true })
70
+
71
+ /** @type {Partial<import('../../utils/types').ServerSettings>} */
72
+ let settings = {}
73
+ try {
74
+ settings = await detectServerSettings(devConfig, options, site.root)
75
+
76
+ cachedConfig.config = getConfigWithPlugins(cachedConfig.config, settings)
77
+ } catch (error_) {
78
+ log(NETLIFYDEVERR, error_.message)
79
+ exit(1)
80
+ }
81
+
82
+ command.setAnalyticsPayload({ projectType: settings.framework || 'custom', live: options.live, graph: options.graph })
83
+
84
+ log(`${NETLIFYDEVLOG} Building site for production`)
85
+ log(
86
+ `${NETLIFYDEVWARN} Changes will not be hot-reloaded, so if you need to rebuild your site you must exit and run 'netlify serve' again`,
87
+ )
88
+
89
+ const { configPath: configPathOverride } = await runBuildTimeline({ cachedConfig, options, settings, site })
90
+
91
+ await startFunctionsServer({
92
+ api,
93
+ command,
94
+ config,
95
+ debug: options.debug,
96
+ loadDistFunctions: true,
97
+ settings,
98
+ site,
99
+ siteInfo,
100
+ siteUrl,
101
+ capabilities,
102
+ timeouts,
103
+ })
104
+
105
+ // Try to add `.netlify` to `.gitignore`.
106
+ try {
107
+ await ensureNetlifyIgnore(repositoryRoot)
108
+ } catch {
109
+ // no-op
110
+ }
111
+
112
+ // TODO: We should consolidate this with the existing config watcher.
113
+ const getUpdatedConfig = async () => {
114
+ const cwd = options.cwd || process.cwd()
115
+ const { config: newConfig } = await command.getConfig({ cwd, offline: true, state })
116
+ const normalizedNewConfig = normalizeConfig(newConfig)
117
+
118
+ return normalizedNewConfig
119
+ }
120
+
121
+ const inspectSettings = generateInspectSettings(options.edgeInspect, options.edgeInspectBrk)
122
+ const url = await startProxyServer({
123
+ addonsUrls,
124
+ config,
125
+ configPath: configPathOverride,
126
+ env,
127
+ geolocationMode: options.geo,
128
+ geoCountry: options.country,
129
+ getUpdatedConfig,
130
+ inspectSettings,
131
+ offline: options.offline,
132
+ settings,
133
+ site,
134
+ siteInfo,
135
+ state,
136
+ })
137
+
138
+ if (devConfig.autoLaunch !== false) {
139
+ await openBrowser({ url, silentBrowserNoneError: true })
140
+ }
141
+
142
+ process.env.URL = url
143
+ process.env.DEPLOY_URL = url
144
+
145
+ printBanner({ url })
146
+ }
147
+
148
+ /**
149
+ * Creates the `netlify serve` command
150
+ * @param {import('../base-command.mjs').default} program
151
+ * @returns
152
+ */
153
+ export const createServeCommand = (program) =>
154
+ program
155
+ .command('serve')
156
+ .description(
157
+ '(Beta) Build the site for production and serve locally. This does not watch the code for changes, so if you need to rebuild your site then you must exit and run `serve` again.',
158
+ )
159
+ .option(
160
+ '--context <context>',
161
+ 'Specify a deploy context or branch for environment variables (contexts: "production", "deploy-preview", "branch-deploy", "dev")',
162
+ normalizeContext,
163
+ )
164
+ .option('-p ,--port <port>', 'port of netlify dev', (value) => Number.parseInt(value))
165
+ .option('-d ,--dir <path>', 'dir with static files')
166
+ .option('-f ,--functions <folder>', 'specify a functions folder to serve')
167
+ .option('-o ,--offline', 'disables any features that require network access')
168
+ .option('--functionsPort <port>', 'port of functions server', (value) => Number.parseInt(value))
169
+ .addOption(
170
+ new Option(
171
+ '--geo <mode>',
172
+ 'force geolocation data to be updated, use cached data from the last 24h if found, or use a mock location',
173
+ )
174
+ .choices(['cache', 'mock', 'update'])
175
+ .default('cache'),
176
+ )
177
+ .addOption(
178
+ new Option(
179
+ '--country <geoCountry>',
180
+ 'Two-letter country code (https://ntl.fyi/country-codes) to use as mock geolocation (enables --geo=mock automatically)',
181
+ ).argParser(getGeoCountryArgParser('netlify dev --geo=mock --country=FR')),
182
+ )
183
+ .addOption(
184
+ new Option('--staticServerPort <port>', 'port of the static app server used when no framework is detected')
185
+ .argParser((value) => Number.parseInt(value))
186
+ .hideHelp(),
187
+ )
188
+ .addExamples(['netlify serve', 'BROWSER=none netlify serve # disable browser auto opening'])
189
+ .action(serve)
@@ -16,7 +16,7 @@
16
16
  "dependencies": {
17
17
  "cookie-parser": "^1.4.5",
18
18
  "express": "^4.17.1",
19
- "jsonwebtoken": "^8.5.1",
19
+ "jsonwebtoken": "^9.0.0",
20
20
  "passport": "^0.6.0",
21
21
  "passport-github2": "^0.1.12",
22
22
  "passport-jwt": "^4.0.0",
@@ -213,17 +213,14 @@ export class EdgeFunctionsRegistry {
213
213
  if (
214
214
  variable.sources.includes('ui') ||
215
215
  variable.sources.includes('account') ||
216
- variable.sources.includes('addons')
216
+ variable.sources.includes('addons') ||
217
+ variable.sources.includes('internal')
217
218
  ) {
218
219
  env[key] = variable.value
219
220
  }
220
221
  })
221
222
 
222
223
  env.DENO_REGION = 'local'
223
- env.NETLIFY_DEV = 'true'
224
- // We use it in the bootstrap layer to detect whether we're running in production or not
225
- // (see https://github.com/netlify/edge-functions-bootstrap/blob/main/src/bootstrap/environment.ts#L2)
226
- // env.DENO_DEPLOYMENT_ID = 'xxx='
227
224
 
228
225
  return env
229
226
  }
@@ -1,19 +1,22 @@
1
1
  // @ts-check
2
2
  import { mkdir } from 'fs/promises'
3
- import { extname, isAbsolute, join } from 'path'
3
+ import { extname, isAbsolute, join, resolve } from 'path'
4
4
  import { env } from 'process'
5
5
 
6
+ import extractZip from 'extract-zip'
7
+
6
8
  import {
7
9
  chalk,
8
10
  getTerminalLink,
9
11
  log,
10
12
  NETLIFYDEVERR,
11
13
  NETLIFYDEVLOG,
12
- NETLIFYDEVWARN,
13
14
  warn,
14
15
  watchDebounced,
15
16
  } from '../../utils/command-helpers.mjs'
17
+ import { SERVE_FUNCTIONS_FOLDER } from '../../utils/functions/functions.mjs'
16
18
  import { getLogMessage } from '../log.mjs'
19
+ import { getPathInProject } from '../settings.mjs'
17
20
 
18
21
  import NetlifyFunction from './netlify-function.mjs'
19
22
  import runtimes from './runtimes/index.mjs'
@@ -21,9 +24,10 @@ import runtimes from './runtimes/index.mjs'
21
24
  const ZIP_EXTENSION = '.zip'
22
25
 
23
26
  export class FunctionsRegistry {
24
- constructor({ capabilities, config, isConnected = false, projectRoot, settings, timeouts }) {
27
+ constructor({ capabilities, config, debug = false, isConnected = false, projectRoot, settings, timeouts }) {
25
28
  this.capabilities = capabilities
26
29
  this.config = config
30
+ this.debug = debug
27
31
  this.isConnected = isConnected
28
32
  this.projectRoot = projectRoot
29
33
  this.timeouts = timeouts
@@ -123,7 +127,7 @@ export class FunctionsRegistry {
123
127
  return this.functions.get(name)
124
128
  }
125
129
 
126
- registerFunction(name, funcBeforeHook) {
130
+ async registerFunction(name, funcBeforeHook) {
127
131
  const { runtime } = funcBeforeHook
128
132
 
129
133
  // The `onRegister` hook allows runtimes to modify the function before it's
@@ -145,11 +149,16 @@ export class FunctionsRegistry {
145
149
  )
146
150
  }
147
151
 
148
- // This fixes the bug described here https://github.com/netlify/zip-it-and-ship-it/issues/637
149
- // If the current function's file is a zip bundle, we ignore it and log a helpful message.
152
+ // If the function file is a ZIP, we extract it and rewire its main file to
153
+ // the new location.
150
154
  if (extname(func.mainFile) === ZIP_EXTENSION) {
151
- log(`${NETLIFYDEVWARN} Skipped bundled function ${chalk.yellow(name)}. Unzip the archive to load it from source.`)
152
- return
155
+ const unzippedDirectory = await this.unzipFunction(func)
156
+
157
+ if (this.debug) {
158
+ log(`${NETLIFYDEVLOG} ${chalk.green('Extracted')} function ${chalk.yellow(name)} from ${func.mainFile}.`)
159
+ }
160
+
161
+ func.mainFile = join(unzippedDirectory, `${func.name}.js`)
153
162
  }
154
163
 
155
164
  this.functions.set(name, func)
@@ -197,34 +206,36 @@ export class FunctionsRegistry {
197
206
 
198
207
  await Promise.all(deletedFunctions.map((func) => this.unregisterFunction(func.name)))
199
208
 
200
- functions.forEach(({ mainFile, name, runtime: runtimeName }) => {
201
- const runtime = runtimes[runtimeName]
202
-
203
- // If there is no matching runtime, it means this function is not yet
204
- // supported in Netlify Dev.
205
- if (runtime === undefined) {
206
- return
207
- }
209
+ await Promise.all(
210
+ functions.map(async ({ mainFile, name, runtime: runtimeName }) => {
211
+ const runtime = runtimes[runtimeName]
208
212
 
209
- // If this function has already been registered, we skip it.
210
- if (this.functions.has(name)) {
211
- return
212
- }
213
+ // If there is no matching runtime, it means this function is not yet
214
+ // supported in Netlify Dev.
215
+ if (runtime === undefined) {
216
+ return
217
+ }
213
218
 
214
- const func = new NetlifyFunction({
215
- config: this.config,
216
- directory: directories.find((directory) => mainFile.startsWith(directory)),
217
- mainFile,
218
- name,
219
- projectRoot: this.projectRoot,
220
- runtime,
221
- timeoutBackground: this.timeouts.backgroundFunctions,
222
- timeoutSynchronous: this.timeouts.syncFunctions,
223
- settings: this.settings,
224
- })
219
+ // If this function has already been registered, we skip it.
220
+ if (this.functions.has(name)) {
221
+ return
222
+ }
225
223
 
226
- this.registerFunction(name, func)
227
- })
224
+ const func = new NetlifyFunction({
225
+ config: this.config,
226
+ directory: directories.find((directory) => mainFile.startsWith(directory)),
227
+ mainFile,
228
+ name,
229
+ projectRoot: this.projectRoot,
230
+ runtime,
231
+ timeoutBackground: this.timeouts.backgroundFunctions,
232
+ timeoutSynchronous: this.timeouts.syncFunctions,
233
+ settings: this.settings,
234
+ })
235
+
236
+ await this.registerFunction(name, func)
237
+ }),
238
+ )
228
239
 
229
240
  await Promise.all(directories.map((path) => this.setupDirectoryWatcher(path)))
230
241
  }
@@ -261,4 +272,15 @@ export class FunctionsRegistry {
261
272
  await watcher.close()
262
273
  }
263
274
  }
275
+
276
+ async unzipFunction(func) {
277
+ const targetDirectory = resolve(
278
+ this.projectRoot,
279
+ getPathInProject([SERVE_FUNCTIONS_FOLDER, '.unzipped', func.name]),
280
+ )
281
+
282
+ await extractZip(func.mainFile, { dir: targetDirectory })
283
+
284
+ return targetDirectory
285
+ }
264
286
  }
@@ -7,6 +7,7 @@ import readPkgUp from 'read-pkg-up'
7
7
  import sourceMapSupport from 'source-map-support'
8
8
 
9
9
  import { NETLIFYDEVERR } from '../../../../../utils/command-helpers.mjs'
10
+ import { SERVE_FUNCTIONS_FOLDER } from '../../../../../utils/functions/functions.mjs'
10
11
  import { getPathInProject } from '../../../../settings.mjs'
11
12
  import { normalizeFunctionsConfig } from '../../../config.mjs'
12
13
  import { memoizedBuild } from '../../../memoized-build.mjs'
@@ -94,7 +95,7 @@ const clearFunctionsCache = (functionsPath) => {
94
95
  }
95
96
 
96
97
  const getTargetDirectory = async ({ errorExit }) => {
97
- const targetDirectory = path.resolve(getPathInProject(['functions-serve']))
98
+ const targetDirectory = path.resolve(getPathInProject([SERVE_FUNCTIONS_FOLDER]))
98
99
 
99
100
  try {
100
101
  await mkdir(targetDirectory, { recursive: true })
@@ -7,6 +7,7 @@ import findUp from 'find-up'
7
7
  import toml from 'toml'
8
8
 
9
9
  import execa from '../../../../utils/execa.mjs'
10
+ import { SERVE_FUNCTIONS_FOLDER } from '../../../../utils/functions/functions.mjs'
10
11
  import { getPathInProject } from '../../../settings.mjs'
11
12
  import { runFunctionsProxy } from '../../local-proxy.mjs'
12
13
 
@@ -16,7 +17,7 @@ export const name = 'rs'
16
17
 
17
18
  const build = async ({ func }) => {
18
19
  const functionDirectory = dirname(func.mainFile)
19
- const cacheDirectory = resolve(getPathInProject(['functions-serve']))
20
+ const cacheDirectory = resolve(getPathInProject([SERVE_FUNCTIONS_FOLDER]))
20
21
  const targetDirectory = join(cacheDirectory, func.name)
21
22
  const crateName = await getCrateName(functionDirectory)
22
23
  const binaryName = `${crateName}${isWindows ? '.exe' : ''}`
@@ -4,7 +4,7 @@ import jwtDecode from 'jwt-decode'
4
4
 
5
5
  import { NETLIFYDEVERR, NETLIFYDEVLOG, error as errorExit, log } from '../../utils/command-helpers.mjs'
6
6
  import { generateNetlifyGraphJWT } from '../../utils/dev.mjs'
7
- import { CLOCKWORK_USERAGENT, getInternalFunctionsDir } from '../../utils/functions/index.mjs'
7
+ import { CLOCKWORK_USERAGENT, getFunctionsDistPath, getInternalFunctionsDir } from '../../utils/functions/index.mjs'
8
8
 
9
9
  import { handleBackgroundFunction, handleBackgroundFunctionResult } from './background.mjs'
10
10
  import { createFormSubmissionHandler } from './form-submissions-handler.mjs'
@@ -208,29 +208,46 @@ const getFunctionsServer = async function (options) {
208
208
  }
209
209
 
210
210
  export const startFunctionsServer = async (options) => {
211
- const { capabilities, config, settings, site, siteUrl, timeouts } = options
211
+ const { capabilities, config, debug, loadDistFunctions, settings, site, siteUrl, timeouts } = options
212
212
  const internalFunctionsDir = await getInternalFunctionsDir({ base: site.root })
213
+ const functionsDirectories = []
213
214
 
214
- // The order of the function directories matters. Leftmost directories take
215
- // precedence.
216
- const functionsDirectories = [settings.functions, internalFunctionsDir].filter(Boolean)
217
-
218
- if (functionsDirectories.length !== 0) {
219
- const functionsRegistry = new FunctionsRegistry({
220
- capabilities,
221
- config,
222
- isConnected: Boolean(siteUrl),
223
- projectRoot: site.root,
224
- settings,
225
- timeouts,
226
- })
215
+ // If the `loadDistFunctions` parameter is sent, the functions server will
216
+ // use the built functions created by zip-it-and-ship-it rather than building
217
+ // them from source.
218
+ if (loadDistFunctions) {
219
+ const distPath = await getFunctionsDistPath({ base: site.root })
227
220
 
228
- await functionsRegistry.scan(functionsDirectories)
221
+ if (distPath) {
222
+ functionsDirectories.push(distPath)
223
+ }
224
+ } else {
225
+ // The order of the function directories matters. Leftmost directories take
226
+ // precedence.
227
+ const sourceDirectories = [settings.functions, internalFunctionsDir].filter(Boolean)
229
228
 
230
- const server = await getFunctionsServer(Object.assign(options, { functionsRegistry }))
229
+ functionsDirectories.push(...sourceDirectories)
230
+ }
231
231
 
232
- await startWebServer({ server, settings })
232
+ if (functionsDirectories.length === 0) {
233
+ return
233
234
  }
235
+
236
+ const functionsRegistry = new FunctionsRegistry({
237
+ capabilities,
238
+ config,
239
+ debug,
240
+ isConnected: Boolean(siteUrl),
241
+ projectRoot: site.root,
242
+ settings,
243
+ timeouts,
244
+ })
245
+
246
+ await functionsRegistry.scan(functionsDirectories)
247
+
248
+ const server = await getFunctionsServer(Object.assign(options, { functionsRegistry }))
249
+
250
+ await startWebServer({ server, settings })
234
251
  }
235
252
 
236
253
  const startWebServer = async ({ server, settings }) => {
@@ -0,0 +1,17 @@
1
+ // @ts-check
2
+ import boxen from 'boxen'
3
+
4
+ import { chalk, log, NETLIFYDEVLOG } from './command-helpers.mjs'
5
+
6
+ export const printBanner = ({ url }) => {
7
+ const banner = chalk.bold(`${NETLIFYDEVLOG} Server now ready on ${url}`)
8
+
9
+ log(
10
+ boxen(banner, {
11
+ padding: 1,
12
+ margin: 1,
13
+ align: 'center',
14
+ borderColor: '#00c7b7',
15
+ }),
16
+ )
17
+ }
@@ -11,7 +11,7 @@ import isPlainObject from 'is-plain-obj'
11
11
 
12
12
  import { NETLIFYDEVWARN, chalk, log } from './command-helpers.mjs'
13
13
  import { acquirePort } from './dev.mjs'
14
- import { getInternalFunctionsDir } from './functions/index.mjs'
14
+ import { getInternalFunctionsDir } from './functions/functions.mjs'
15
15
 
16
16
  const formatProperty = (str) => chalk.magenta(`'${str}'`)
17
17
  const formatValue = (str) => chalk.green(`'${str}'`)
@@ -364,4 +364,38 @@ const formatSettingsArrForInquirer = function (frameworks) {
364
364
  return formattedArr.flat()
365
365
  }
366
366
 
367
+ /**
368
+ * Returns a copy of the provided config with any plugins provided by the
369
+ * server settings
370
+ * @param {*} config
371
+ * @param {Partial<import('./types').ServerSettings>} settings
372
+ * @returns {*} Modified config
373
+ */
374
+ export const getConfigWithPlugins = (config, settings) => {
375
+ if (!settings.plugins) {
376
+ return config
377
+ }
378
+
379
+ // If there are plugins that we should be running for this site, add them
380
+ // to the config as if they were declared in netlify.toml. We must check
381
+ // whether the plugin has already been added by another source (like the
382
+ // TOML file or the UI), as we don't want to run the same plugin twice.
383
+ const { plugins: existingPlugins = [] } = config
384
+ const existingPluginNames = new Set(existingPlugins.map((plugin) => plugin.package))
385
+ const newPlugins = settings.plugins
386
+ .map((pluginName) => {
387
+ if (existingPluginNames.has(pluginName)) {
388
+ return
389
+ }
390
+
391
+ return { package: pluginName, origin: 'config', inputs: {} }
392
+ })
393
+ .filter(Boolean)
394
+
395
+ return {
396
+ ...config,
397
+ plugins: [...newPlugins, ...config.plugins],
398
+ }
399
+ }
400
+
367
401
  export default detectServerSettings
package/src/utils/dev.mjs CHANGED
@@ -148,6 +148,10 @@ export const injectEnvVariables = async ({ devConfig, env, site }) => {
148
148
  const newSourceName = `${file} file`
149
149
  const sources = environment.has(key) ? [newSourceName, ...environment.get(key).sources] : [newSourceName]
150
150
 
151
+ if (sources.includes('internal')) {
152
+ return
153
+ }
154
+
151
155
  environment.set(key, {
152
156
  sources,
153
157
  value: fileEnv[key],
@@ -160,6 +164,7 @@ export const injectEnvVariables = async ({ devConfig, env, site }) => {
160
164
  const existsInProcess = process.env[key] !== undefined
161
165
  const [usedSource, ...overriddenSources] = existsInProcess ? ['process', ...variable.sources] : variable.sources
162
166
  const usedSourceName = getEnvSourceName(usedSource)
167
+ const isInternal = variable.sources.includes('internal')
163
168
 
164
169
  overriddenSources.forEach((source) => {
165
170
  const sourceName = getEnvSourceName(source)
@@ -173,17 +178,15 @@ export const injectEnvVariables = async ({ devConfig, env, site }) => {
173
178
  )
174
179
  })
175
180
 
176
- if (!existsInProcess) {
177
- // Omitting `general` env vars to reduce noise in the logs.
178
- if (usedSource !== 'general') {
181
+ if (!existsInProcess || isInternal) {
182
+ // Omitting `general` and `internal` env vars to reduce noise in the logs.
183
+ if (usedSource !== 'general' && !isInternal) {
179
184
  log(`${NETLIFYDEVLOG} Injected ${usedSourceName} env var: ${chalk.yellow(key)}`)
180
185
  }
181
186
 
182
187
  process.env[key] = variable.value
183
188
  }
184
189
  }
185
-
186
- process.env.NETLIFY_DEV = 'true'
187
190
  }
188
191
 
189
192
  export const acquirePort = async ({ configuredPort, defaultPort, errorMessage }) => {
@@ -0,0 +1,66 @@
1
+ // @ts-check
2
+ import waitPort from 'wait-port'
3
+
4
+ import { startSpinner, stopSpinner } from '../lib/spinner.mjs'
5
+
6
+ import { error, exit, log, NETLIFYDEVERR, NETLIFYDEVLOG } from './command-helpers.mjs'
7
+ import { runCommand } from './shell.mjs'
8
+ import { startStaticServer } from './static-server.mjs'
9
+
10
+ // 10 minutes
11
+ const FRAMEWORK_PORT_TIMEOUT = 6e5
12
+
13
+ /**
14
+ * @typedef StartReturnObject
15
+ * @property {4 | 6 | undefined=} ipVersion The version the open port was found on
16
+ */
17
+
18
+ /**
19
+ * Start a static server if the `useStaticServer` is provided or a framework specific server
20
+ * @param {object} config
21
+ * @param {Partial<import('./types').ServerSettings>} config.settings
22
+ * @returns {Promise<StartReturnObject>}
23
+ */
24
+ export const startFrameworkServer = async function ({ settings }) {
25
+ if (settings.useStaticServer) {
26
+ if (settings.command) {
27
+ runCommand(settings.command, settings.env)
28
+ }
29
+ await startStaticServer({ settings })
30
+
31
+ return {}
32
+ }
33
+
34
+ log(`${NETLIFYDEVLOG} Starting Netlify Dev with ${settings.framework || 'custom config'}`)
35
+
36
+ const spinner = startSpinner({
37
+ text: `Waiting for framework port ${settings.frameworkPort}. This can be configured using the 'targetPort' property in the netlify.toml`,
38
+ })
39
+
40
+ runCommand(settings.command, settings.env, spinner)
41
+
42
+ let port
43
+ try {
44
+ port = await waitPort({
45
+ port: settings.frameworkPort,
46
+ host: 'localhost',
47
+ output: 'silent',
48
+ timeout: FRAMEWORK_PORT_TIMEOUT,
49
+ ...(settings.pollingStrategies.includes('HTTP') && { protocol: 'http' }),
50
+ })
51
+
52
+ if (!port.open) {
53
+ throw new Error(`Timed out waiting for port '${settings.frameworkPort}' to be open`)
54
+ }
55
+
56
+ stopSpinner({ error: false, spinner })
57
+ } catch (error_) {
58
+ stopSpinner({ error: true, spinner })
59
+ log(NETLIFYDEVERR, `Netlify Dev could not start or connect to localhost:${settings.frameworkPort}.`)
60
+ log(NETLIFYDEVERR, `Please make sure your framework server is running on port ${settings.frameworkPort}`)
61
+ error(error_)
62
+ exit(1)
63
+ }
64
+
65
+ return { ipVersion: port?.ipVersion }
66
+ }