Skip to content

Commit fd3d2f7

Browse files
authored
fix(command-dev): use execa to launch framework server (#2857)
1 parent d451cb3 commit fd3d2f7

File tree

7 files changed

+86
-42
lines changed

7 files changed

+86
-42
lines changed

npm-shrinkwrap.json

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,6 @@
183183
"update-notifier": "^5.0.0",
184184
"uuid": "^8.0.0",
185185
"wait-port": "^0.2.2",
186-
"which": "^2.0.2",
187186
"winston": "^3.2.1",
188187
"write-file-atomic": "^3.0.0"
189188
},

src/commands/dev/index.js

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
const childProcess = require('child_process')
21
const path = require('path')
32
const process = require('process')
43
const { promisify } = require('util')
54

65
const { flags: flagsLib } = require('@oclif/command')
76
const boxen = require('boxen')
87
const chalk = require('chalk')
8+
const execa = require('execa')
99
const StaticServer = require('static-server')
1010
const stripAnsiCc = require('strip-ansi-control-characters')
1111
const waitPort = require('wait-port')
12-
const which = require('which')
1312

1413
const { startFunctionsServer } = require('../../lib/functions/server')
1514
const Command = require('../../utils/command')
@@ -35,47 +34,61 @@ const startStaticServer = async ({ settings, log }) => {
3534
log(`\n${NETLIFYDEVLOG} Server listening to`, settings.frameworkPort)
3635
}
3736

37+
const isNonExistingCommandError = ({ command, error }) => {
38+
// `ENOENT` is only returned for non Windows systems
39+
// See https://github.com/sindresorhus/execa/pull/447
40+
if (error.code === 'ENOENT') {
41+
return true
42+
}
43+
44+
// if the command is a package manager we let it report the error
45+
if (['yarn', 'npm'].includes(command)) {
46+
return false
47+
}
48+
49+
// this only works on English versions of Windows
50+
return (
51+
typeof error.message === 'string' && error.message.includes('is not recognized as an internal or external command')
52+
)
53+
}
54+
3855
const startFrameworkServer = async function ({ settings, log, exit }) {
3956
if (settings.noCmd) {
4057
return await startStaticServer({ settings, log })
4158
}
4259

4360
log(`${NETLIFYDEVLOG} Starting Netlify Dev with ${settings.framework || 'custom config'}`)
44-
const commandBin = await which(settings.command).catch((error) => {
45-
if (error.code === 'ENOENT') {
46-
throw new Error(
47-
`"${settings.command}" could not be found in your PATH. Please make sure that "${settings.command}" is installed and available in your PATH`,
48-
)
49-
}
50-
throw error
51-
})
52-
const ps = childProcess.spawn(commandBin, settings.args, {
53-
env: { ...process.env, ...settings.env, FORCE_COLOR: 'true' },
54-
stdio: 'pipe',
55-
})
5661

57-
ps.stdout.pipe(stripAnsiCc.stream()).pipe(process.stdout)
58-
ps.stderr.pipe(stripAnsiCc.stream()).pipe(process.stderr)
59-
60-
process.stdin.pipe(process.stdin)
62+
// we use reject=false to avoid rejecting synchronously when the command doesn't exist
63+
const frameworkProcess = execa(settings.command, settings.args, { preferLocal: true, reject: false })
64+
frameworkProcess.stdout.pipe(stripAnsiCc.stream()).pipe(process.stdout)
65+
frameworkProcess.stderr.pipe(stripAnsiCc.stream()).pipe(process.stderr)
66+
process.stdin.pipe(frameworkProcess.stdin)
67+
68+
// we can't try->await->catch since we don't want to block on the framework server which
69+
// is a long running process
70+
// eslint-disable-next-line promise/catch-or-return,promise/prefer-await-to-then
71+
frameworkProcess.then(async () => {
72+
const result = await frameworkProcess
73+
// eslint-disable-next-line promise/always-return
74+
if (result.failed && isNonExistingCommandError({ command: settings.command, error: result })) {
75+
log(
76+
NETLIFYDEVERR,
77+
`Failed launching framework server. Please verify ${chalk.magenta(`'${settings.command}'`)} exists`,
78+
)
79+
} else {
80+
const commandWithArgs = `${settings.command} ${settings.args.join(' ')}`
81+
const errorMessage = result.failed
82+
? `${NETLIFYDEVERR} ${result.shortMessage}`
83+
: `${NETLIFYDEVWARN} "${commandWithArgs}" exited with code ${result.exitCode}`
6184

62-
const handleProcessExit = function (code) {
63-
log(
64-
code > 0 ? NETLIFYDEVERR : NETLIFYDEVWARN,
65-
`"${[settings.command, ...settings.args].join(' ')}" exited with code ${code}. Shutting down Netlify Dev server`,
66-
)
85+
log(`${errorMessage}. Shutting down Netlify Dev server`)
86+
}
6787
process.exit(1)
68-
}
69-
ps.on('close', handleProcessExit)
70-
ps.on('SIGINT', handleProcessExit)
71-
ps.on('SIGTERM', handleProcessExit)
88+
})
7289
;['SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGHUP', 'exit'].forEach((signal) => {
7390
process.on(signal, () => {
74-
try {
75-
process.kill(-ps.pid)
76-
} catch (error) {
77-
// Ignore
78-
}
91+
frameworkProcess.kill('SIGTERM', { forceKillAfterTimeout: 500 })
7992
process.exit()
8093
})
8194
})
@@ -96,8 +109,6 @@ const startFrameworkServer = async function ({ settings, log, exit }) {
96109
log(NETLIFYDEVERR, `Please make sure your framework server is running on port ${settings.frameworkPort}`)
97110
exit(1)
98111
}
99-
100-
return ps
101112
}
102113

103114
// 10 minutes

tests/framework-detection.test.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,32 @@ test('should start custom command if framework=#custom, command and targetPort a
177177

178178
const error = await t.throwsAsync(() =>
179179
withDevServer(
180-
{ cwd: builder.directory, args: ['--command', 'cat non-existing', '--targetPort', '3000'] },
180+
{ cwd: builder.directory, args: ['--command', 'echo hello', '--targetPort', '3000'] },
181+
() => {},
182+
true,
183+
),
184+
)
185+
t.snapshot(normalize(error.stdout))
186+
})
187+
})
188+
189+
test(`should print specific error when command doesn't exist`, async (t) => {
190+
await withSiteBuilder('site-with-custom-framework', async (builder) => {
191+
await builder.buildAsync()
192+
193+
const error = await t.throwsAsync(() =>
194+
withDevServer(
195+
{
196+
cwd: builder.directory,
197+
args: [
198+
'--command',
199+
'oops-i-did-it-again forgot-to-use-a-valid-command',
200+
'--targetPort',
201+
'3000',
202+
'--framework',
203+
'#custom',
204+
],
205+
},
181206
() => {},
182207
true,
183208
),

tests/snapshots/framework-detection.test.js.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ Generated by [AVA](https://avajs.dev).
115115
> start␊
116116
> react-scripts start␊
117117
118-
"npm start" exited with code *. Shutting down Netlify Dev server`
118+
Command failed with exit code *: npm start. Shutting down Netlify Dev server`
119119

120120
## should throw if framework=#custom but command is missing
121121

@@ -136,9 +136,19 @@ Generated by [AVA](https://avajs.dev).
136136
> Snapshot 1
137137
138138
`◈ Netlify Dev ◈␊
139-
◈ Overriding command with setting derived from netlify.toml [dev] block: cat non-existing
139+
◈ Overriding command with setting derived from netlify.toml [dev] block: echo hello
140140
◈ Starting Netlify Dev with #custom␊
141-
◈ "cat non-existing" exited with code *. Shutting down Netlify Dev server`
141+
hello␊
142+
◈ "echo hello" exited with code *. Shutting down Netlify Dev server`
143+
144+
## should print specific error when command doesn't exist
145+
146+
> Snapshot 1
147+
148+
`◈ Netlify Dev ◈␊
149+
◈ Overriding command with setting derived from netlify.toml [dev] block: oops-i-did-it-again forgot-to-use-a-valid-command␊
150+
◈ Starting Netlify Dev with #custom␊
151+
◈ Failed launching framework server. Please verify 'oops-i-did-it-again' exists`
142152

143153
## should prompt when multiple frameworks are detected
144154

@@ -158,4 +168,4 @@ Generated by [AVA](https://avajs.dev).
158168
> start␊
159169
> react-scripts start␊
160170
161-
"npm start" exited with code *. Shutting down Netlify Dev server`
171+
Command failed with exit code *: npm start. Shutting down Netlify Dev server`
105 Bytes
Binary file not shown.

tests/utils/snapshots.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const normalizers = [
77
{ pattern: /\r\n/gu, value: '\n' },
88
{ pattern: //gu, value: '>' },
99
// normalize exit code from different OSes
10-
{ pattern: /exited with code \d+/, value: 'exited with code *' },
10+
{ pattern: /code \d+/, value: 'code *' },
1111
// this is specific to npm v6
1212
{ pattern: /@ start.+\/.+netlify-cli-tests-v10.+/, value: 'start' },
1313
]

0 commit comments

Comments
 (0)