Skip to content

Commit 9e27cdc

Browse files
committed
perf: lazy load dev, dev-exec commands
Non-scientific benchmark says: On my machine, this makes every command boot 30% faster.
1 parent 3c2c5f3 commit 9e27cdc

File tree

6 files changed

+164
-145
lines changed

6 files changed

+164
-145
lines changed

src/commands/dev-exec/dev-exec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { OptionValues } from 'commander'
2+
import execa from 'execa'
3+
4+
import { getDotEnvVariables, injectEnvVariables } from '../../utils/dev.js'
5+
import { getEnvelopeEnv } from '../../utils/env/index.js'
6+
import BaseCommand from '../base-command.js'
7+
8+
export const devExec = async (cmd: string, options: OptionValues, command: BaseCommand) => {
9+
const { api, cachedConfig, config, site, siteInfo } = command.netlify
10+
11+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
12+
const withEnvelopeEnvVars = await getEnvelopeEnv({ api, context: options.context, env: cachedConfig.env, siteInfo })
13+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
14+
const withDotEnvVars = await getDotEnvVariables({ devConfig: { ...config.dev }, env: withEnvelopeEnvVars, site })
15+
16+
injectEnvVariables(withDotEnvVars)
17+
18+
await execa(cmd, command.args.slice(1), {
19+
stdio: 'inherit',
20+
})
21+
}

src/commands/dev-exec/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { OptionValues } from 'commander'
2+
3+
import type BaseCommand from '../base-command.js'
4+
import { normalizeContext } from '../../utils/env/index.js'
5+
6+
export const createDevExecCommand = (program: BaseCommand) =>
7+
program
8+
.command('dev:exec')
9+
.argument('<...cmd>', `the command that should be executed`)
10+
.option(
11+
'--context <context>',
12+
'Specify a deploy context or branch for environment variables (contexts: "production", "deploy-preview", "branch-deploy", "dev")',
13+
normalizeContext,
14+
'dev',
15+
)
16+
.description(
17+
'Runs a command within the netlify dev environment. For example, with environment variables from any installed add-ons',
18+
)
19+
.allowExcessArguments(true)
20+
.addExamples(['netlify dev:exec npm run bootstrap'])
21+
.action(async (cmd: string, options: OptionValues, command: BaseCommand) => {
22+
const { devExec } = await import('./dev-exec.js')
23+
await devExec(cmd, options, command)
24+
})

src/commands/dev/dev-exec.ts

Lines changed: 0 additions & 36 deletions
This file was deleted.

src/commands/dev/dev.ts

Lines changed: 7 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@ import process from 'process'
22

33
import type { NetlifyAPI } from 'netlify'
44
import { applyMutations } from '@netlify/config'
5-
import { Option, OptionValues } from 'commander'
5+
import { OptionValues } from 'commander'
66

77
import { BLOBS_CONTEXT_VARIABLE, encodeBlobsContext, getBlobsContextWithEdgeAccess } from '../../lib/blobs/blobs.js'
88
import { promptEditorHelper } from '../../lib/edge-functions/editor-helper.js'
99
import { startFunctionsServer } from '../../lib/functions/server.js'
1010
import { printBanner } from '../../utils/banner.js'
1111
import {
12-
BANG,
1312
NETLIFYDEV,
1413
NETLIFYDEVERR,
1514
NETLIFYDEVLOG,
@@ -21,19 +20,17 @@ import {
2120
} from '../../utils/command-helpers.js'
2221
import detectServerSettings, { getConfigWithPlugins } from '../../utils/detect-server-settings.js'
2322
import { UNLINKED_SITE_MOCK_ID, getDotEnvVariables, getSiteInformation, injectEnvVariables } from '../../utils/dev.js'
24-
import { getEnvelopeEnv, normalizeContext } from '../../utils/env/index.js'
23+
import { getEnvelopeEnv } from '../../utils/env/index.js'
2524
import { ensureNetlifyIgnore } from '../../utils/gitignore.js'
2625
import { getLiveTunnelSlug, startLiveTunnel } from '../../utils/live-tunnel.js'
2726
import openBrowser from '../../utils/open-browser.js'
2827
import { generateInspectSettings, startProxyServer } from '../../utils/proxy-server.js'
2928
import { getProxyUrl } from '../../utils/proxy.js'
3029
import { runDevTimeline } from '../../utils/run-build.js'
3130
import type { CLIState, ServerSettings } from '../../utils/types.js'
32-
import { getGeoCountryArgParser } from '../../utils/validation.js'
3331
import type BaseCommand from '../base-command.js'
3432
import type { NetlifySite } from '../types.js'
3533

36-
import { createDevExecCommand } from './dev-exec.js'
3734
import type { DevConfig } from './types.js'
3835

3936
const handleLiveTunnel = async ({
@@ -76,22 +73,6 @@ const handleLiveTunnel = async ({
7673
}
7774
}
7875

79-
const validateShortFlagArgs = (args: string) => {
80-
if (args.startsWith('=')) {
81-
throw new Error(
82-
`Short flag options like -e or -E don't support the '=' sign
83-
${chalk.red(BANG)} Supported formats:
84-
netlify dev -e
85-
netlify dev -e 127.0.0.1:9229
86-
netlify dev -e127.0.0.1:9229
87-
netlify dev -E
88-
netlify dev -E 127.0.0.1:9229
89-
netlify dev -E127.0.0.1:9229`,
90-
)
91-
}
92-
return args
93-
}
94-
9576
export const dev = async (options: OptionValues, command: BaseCommand) => {
9677
log(NETLIFYDEV)
9778
const { api, cachedConfig, config, repositoryRoot, site, siteInfo, state } = command.netlify
@@ -134,6 +115,7 @@ export const dev = async (options: OptionValues, command: BaseCommand) => {
134115

135116
const { accountId, addonsUrls, capabilities, siteUrl, timeouts } = await getSiteInformation({
136117
// inherited from base command --offline
118+
137119
offline: options.offline,
138120
api,
139121
site,
@@ -161,12 +143,14 @@ export const dev = async (options: OptionValues, command: BaseCommand) => {
161143
if (error instanceof Error) {
162144
log(NETLIFYDEVERR, error.message)
163145
}
146+
164147
process.exit(1)
165148
}
166149

167150
command.setAnalyticsPayload({ live: options.live })
168151

169152
const liveTunnelUrl = await handleLiveTunnel({ options, site, api, settings, state })
153+
170154
const url = liveTunnelUrl || getProxyUrl(settings)
171155

172156
process.env.URL = url
@@ -185,12 +169,14 @@ export const dev = async (options: OptionValues, command: BaseCommand) => {
185169
})
186170

187171
// FIXME(serhalp): `applyMutations` is `(any, any) => any)`. Add types in `@netlify/config`.
172+
188173
const mutatedConfig: typeof config = applyMutations(config, configMutations)
189174

190175
const functionsRegistry = await startFunctionsServer({
191176
blobsContext,
192177
command,
193178
config: mutatedConfig,
179+
194180
debug: options.debug,
195181
settings,
196182
site,
@@ -254,90 +240,3 @@ export const dev = async (options: OptionValues, command: BaseCommand) => {
254240

255241
printBanner({ url })
256242
}
257-
258-
export const createDevCommand = (program: BaseCommand) => {
259-
createDevExecCommand(program)
260-
261-
return program
262-
.command('dev')
263-
.alias('develop')
264-
.description(
265-
`Local dev server\nThe dev command will run a local dev server with Netlify's proxy and redirect rules`,
266-
)
267-
.option('-c ,--command <command>', 'command to run')
268-
.option(
269-
'--context <context>',
270-
'Specify a deploy context or branch for environment variables (contexts: "production", "deploy-preview", "branch-deploy", "dev")',
271-
normalizeContext,
272-
)
273-
.option('-p ,--port <port>', 'port of netlify dev', (value) => Number.parseInt(value))
274-
.addOption(new Option('--skip-wait-port', 'disables waiting for target port to become available').hideHelp(true))
275-
.addOption(new Option('--no-open', 'disables the automatic opening of a browser window'))
276-
.option('--target-port <port>', 'port of target app server', (value) => Number.parseInt(value))
277-
.option('--framework <name>', 'framework to use. Defaults to #auto which automatically detects a framework')
278-
.option('-d ,--dir <path>', 'dir with static files')
279-
.option('-f ,--functions <folder>', 'specify a functions folder to serve')
280-
.option('-o, --offline', 'Disables any features that require network access')
281-
.addOption(
282-
new Option('--offline-env', 'disables fetching environment variables from the Netlify API').hideHelp(true),
283-
)
284-
.addOption(
285-
new Option(
286-
'--internal-disable-edge-functions',
287-
"disables edge functions. use this if your environment doesn't support Deno. This option is internal and should not be used by end users.",
288-
).hideHelp(true),
289-
)
290-
.option(
291-
'-l, --live [subdomain]',
292-
'start a public live session; optionally, supply a subdomain to generate a custom URL',
293-
false,
294-
)
295-
.option('--functions-port <port>', 'port of functions server', (value) => Number.parseInt(value))
296-
.addOption(
297-
new Option(
298-
'--geo <mode>',
299-
'force geolocation data to be updated, use cached data from the last 24h if found, or use a mock location',
300-
)
301-
.choices(['cache', 'mock', 'update'])
302-
.default('cache'),
303-
)
304-
.addOption(
305-
new Option(
306-
'--country <geoCountry>',
307-
'Two-letter country code (https://ntl.fyi/country-codes) to use as mock geolocation (enables --geo=mock automatically)',
308-
).argParser(getGeoCountryArgParser('netlify dev --geo=mock --country=FR')),
309-
)
310-
.addOption(
311-
new Option('--staticServerPort <port>', 'port of the static app server used when no framework is detected')
312-
.argParser((value) => Number.parseInt(value))
313-
.hideHelp(),
314-
)
315-
.addOption(
316-
new Option(
317-
'-e, --edge-inspect [address]',
318-
'enable the V8 Inspector Protocol for Edge Functions, with an optional address in the host:port format',
319-
)
320-
.conflicts('edgeInspectBrk')
321-
.argParser(validateShortFlagArgs),
322-
)
323-
.addOption(
324-
new Option(
325-
'-E, --edge-inspect-brk [address]',
326-
'enable the V8 Inspector Protocol for Edge Functions and pause execution on the first line of code, with an optional address in the host:port format',
327-
)
328-
.conflicts('edgeInspect')
329-
.argParser(validateShortFlagArgs),
330-
)
331-
.addExamples([
332-
'netlify dev',
333-
'netlify dev -d public',
334-
'netlify dev -c "hugo server -w" --target-port 1313',
335-
'netlify dev --context production',
336-
'netlify dev --edge-inspect',
337-
'netlify dev --edge-inspect=127.0.0.1:9229',
338-
'netlify dev --edge-inspect-brk',
339-
'netlify dev --edge-inspect-brk=127.0.0.1:9229',
340-
'BROWSER=none netlify dev # disable browser auto opening',
341-
])
342-
.action(dev)
343-
}

src/commands/dev/index.ts

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,110 @@
1-
export { createDevCommand } from './dev.js'
1+
import { Option, type OptionValues } from 'commander'
2+
3+
import { BANG, chalk } from '../../utils/command-helpers.js'
4+
import { normalizeContext } from '../../utils/env/index.js'
5+
import { getGeoCountryArgParser } from '../../utils/validation.js'
6+
import type BaseCommand from '../base-command.js'
7+
8+
const validateShortFlagArgs = (args: string) => {
9+
if (args.startsWith('=')) {
10+
throw new Error(
11+
`Short flag options like -e or -E don't support the '=' sign
12+
${chalk.red(BANG)} Supported formats:
13+
netlify dev -e
14+
netlify dev -e 127.0.0.1:9229
15+
netlify dev -e127.0.0.1:9229
16+
netlify dev -E
17+
netlify dev -E 127.0.0.1:9229
18+
netlify dev -E127.0.0.1:9229`,
19+
)
20+
}
21+
return args
22+
}
23+
24+
export const createDevCommand = (program: BaseCommand) => {
25+
return program
26+
.command('dev')
27+
.alias('develop')
28+
.description(
29+
`Local dev server\nThe dev command will run a local dev server with Netlify's proxy and redirect rules`,
30+
)
31+
.option('-c ,--command <command>', 'command to run')
32+
.option(
33+
'--context <context>',
34+
'Specify a deploy context or branch for environment variables (contexts: "production", "deploy-preview", "branch-deploy", "dev")',
35+
normalizeContext,
36+
)
37+
.option('-p ,--port <port>', 'port of netlify dev', (value) => Number.parseInt(value))
38+
.addOption(new Option('--skip-wait-port', 'disables waiting for target port to become available').hideHelp(true))
39+
.addOption(new Option('--no-open', 'disables the automatic opening of a browser window'))
40+
.option('--target-port <port>', 'port of target app server', (value) => Number.parseInt(value))
41+
.option('--framework <name>', 'framework to use. Defaults to #auto which automatically detects a framework')
42+
.option('-d ,--dir <path>', 'dir with static files')
43+
.option('-f ,--functions <folder>', 'specify a functions folder to serve')
44+
.option('-o, --offline', 'Disables any features that require network access')
45+
.addOption(
46+
new Option('--offline-env', 'disables fetching environment variables from the Netlify API').hideHelp(true),
47+
)
48+
.addOption(
49+
new Option(
50+
'--internal-disable-edge-functions',
51+
"disables edge functions. use this if your environment doesn't support Deno. This option is internal and should not be used by end users.",
52+
).hideHelp(true),
53+
)
54+
.option(
55+
'-l, --live [subdomain]',
56+
'start a public live session; optionally, supply a subdomain to generate a custom URL',
57+
false,
58+
)
59+
.option('--functions-port <port>', 'port of functions server', (value) => Number.parseInt(value))
60+
.addOption(
61+
new Option(
62+
'--geo <mode>',
63+
'force geolocation data to be updated, use cached data from the last 24h if found, or use a mock location',
64+
)
65+
.choices(['cache', 'mock', 'update'])
66+
.default('cache'),
67+
)
68+
.addOption(
69+
new Option(
70+
'--country <geoCountry>',
71+
'Two-letter country code (https://ntl.fyi/country-codes) to use as mock geolocation (enables --geo=mock automatically)',
72+
).argParser(getGeoCountryArgParser('netlify dev --geo=mock --country=FR')),
73+
)
74+
.addOption(
75+
new Option('--staticServerPort <port>', 'port of the static app server used when no framework is detected')
76+
.argParser((value) => Number.parseInt(value))
77+
.hideHelp(),
78+
)
79+
.addOption(
80+
new Option(
81+
'-e, --edge-inspect [address]',
82+
'enable the V8 Inspector Protocol for Edge Functions, with an optional address in the host:port format',
83+
)
84+
.conflicts('edgeInspectBrk')
85+
.argParser(validateShortFlagArgs),
86+
)
87+
.addOption(
88+
new Option(
89+
'-E, --edge-inspect-brk [address]',
90+
'enable the V8 Inspector Protocol for Edge Functions and pause execution on the first line of code, with an optional address in the host:port format',
91+
)
92+
.conflicts('edgeInspect')
93+
.argParser(validateShortFlagArgs),
94+
)
95+
.addExamples([
96+
'netlify dev',
97+
'netlify dev -d public',
98+
'netlify dev -c "hugo server -w" --target-port 1313',
99+
'netlify dev --context production',
100+
'netlify dev --edge-inspect',
101+
'netlify dev --edge-inspect=127.0.0.1:9229',
102+
'netlify dev --edge-inspect-brk',
103+
'netlify dev --edge-inspect-brk=127.0.0.1:9229',
104+
'BROWSER=none netlify dev # disable browser auto opening',
105+
])
106+
.action(async (options: OptionValues, command: BaseCommand) => {
107+
const { dev } = await import('./dev.js')
108+
await dev(options, command)
109+
})
110+
}

0 commit comments

Comments
 (0)