Skip to content

Commit ba154f6

Browse files
Lukas Holzerkodiakhq[bot]
andauthored
feat(dev): enable the usage of the command property without port (#3662)
* feat(dev): enable the usage of the command property without port * chore: fix tests Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 51bde9a commit ba154f6

File tree

7 files changed

+146
-33
lines changed

7 files changed

+146
-33
lines changed

src/commands/dev/index.js

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// @ts-check
12
const path = require('path')
23
const process = require('process')
34
const { promisify } = require('util')
@@ -53,53 +54,75 @@ const isNonExistingCommandError = ({ command, error }) => {
5354
)
5455
}
5556

56-
const startFrameworkServer = async function ({ settings }) {
57-
if (settings.useStaticServer) {
58-
return await startStaticServer({ settings })
59-
}
60-
61-
log(`${NETLIFYDEVLOG} Starting Netlify Dev with ${settings.framework || 'custom config'}`)
62-
63-
// we use reject=false to avoid rejecting synchronously when the command doesn't exist
64-
const frameworkProcess = execa.command(settings.command, {
57+
/**
58+
* Run a command and pipe stdout, stderr and stdin
59+
* @param {string} command
60+
* @param {NodeJS.ProcessEnv} env
61+
* @returns {execa.ExecaChildProcess<string>}
62+
*/
63+
const runCommand = (command, env = {}) => {
64+
const commandProcess = execa.command(command, {
6565
preferLocal: true,
66+
// we use reject=false to avoid rejecting synchronously when the command doesn't exist
6667
reject: false,
67-
env: settings.env,
68+
env,
6869
// windowsHide needs to be false for child process to terminate properly on Windows
6970
windowsHide: false,
7071
})
71-
frameworkProcess.stdout.pipe(stripAnsiCc.stream()).pipe(process.stdout)
72-
frameworkProcess.stderr.pipe(stripAnsiCc.stream()).pipe(process.stderr)
73-
process.stdin.pipe(frameworkProcess.stdin)
72+
73+
commandProcess.stdout.pipe(stripAnsiCc.stream()).pipe(process.stdout)
74+
commandProcess.stderr.pipe(stripAnsiCc.stream()).pipe(process.stderr)
75+
process.stdin.pipe(commandProcess.stdin)
7476

7577
// we can't try->await->catch since we don't want to block on the framework server which
7678
// is a long running process
7779
// eslint-disable-next-line promise/catch-or-return,promise/prefer-await-to-then
78-
frameworkProcess.then(async () => {
79-
const result = await frameworkProcess
80-
const [commandWithoutArgs] = settings.command.split(' ')
80+
commandProcess.then(async () => {
81+
const result = await commandProcess
82+
const [commandWithoutArgs] = command.split(' ')
8183
// eslint-disable-next-line promise/always-return
8284
if (result.failed && isNonExistingCommandError({ command: commandWithoutArgs, error: result })) {
8385
log(
8486
NETLIFYDEVERR,
85-
`Failed launching framework server. Please verify ${chalk.magenta(`'${commandWithoutArgs}'`)} exists`,
87+
`Failed running command: ${command}. Please verify ${chalk.magenta(`'${commandWithoutArgs}'`)} exists`,
8688
)
8789
} else {
8890
const errorMessage = result.failed
8991
? `${NETLIFYDEVERR} ${result.shortMessage}`
90-
: `${NETLIFYDEVWARN} "${settings.command}" exited with code ${result.exitCode}`
92+
: `${NETLIFYDEVWARN} "${command}" exited with code ${result.exitCode}`
9193

9294
log(`${errorMessage}. Shutting down Netlify Dev server`)
9395
}
9496
process.exit(1)
9597
})
9698
;['SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGHUP', 'exit'].forEach((signal) => {
9799
process.on(signal, () => {
98-
frameworkProcess.kill('SIGTERM', { forceKillAfterTimeout: 500 })
100+
commandProcess.kill('SIGTERM', { forceKillAfterTimeout: 500 })
99101
process.exit()
100102
})
101103
})
102104

105+
return commandProcess
106+
}
107+
108+
/**
109+
* Start a static server if the `useStaticServer` is provided or a framework specific server
110+
* @param {object} config
111+
* @param {import('../../utils/types').ServerSettings} config.settings
112+
* @returns {Promise<void>}
113+
*/
114+
const startFrameworkServer = async function ({ settings }) {
115+
if (settings.useStaticServer) {
116+
if (settings.command) {
117+
runCommand(settings.command, settings.env)
118+
}
119+
return await startStaticServer({ settings })
120+
}
121+
122+
log(`${NETLIFYDEVLOG} Starting Netlify Dev with ${settings.framework || 'custom config'}`)
123+
124+
runCommand(settings.command, settings.env)
125+
103126
try {
104127
const open = await waitPort({
105128
port: settings.frameworkPort,
@@ -185,6 +208,7 @@ class DevCommand extends Command {
185208
const { api, config, site, siteInfo } = this.netlify
186209
config.dev = { ...config.dev }
187210
config.build = { ...config.build }
211+
/** @type {import('./types').DevConfig} */
188212
const devConfig = {
189213
framework: '#auto',
190214
...(config.functionsDirectory && { functions: config.functionsDirectory }),
@@ -207,6 +231,7 @@ class DevCommand extends Command {
207231
siteInfo,
208232
})
209233

234+
/** @type {Partial<import('../../utils/types').ServerSettings>} */
210235
let settings = {}
211236
try {
212237
settings = await detectServerSettings(devConfig, flags, site.root)

src/commands/dev/types.d.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { FrameworkNames } from '../../utils/types';
2+
3+
export type DevConfig = {
4+
framework: FrameworkNames
5+
/** Directory of the functions */
6+
functions: string
7+
publish: string
8+
/** Port to serve the functions */
9+
port: number
10+
live: boolean
11+
staticServerPort?: number
12+
targetPort?: number
13+
/** Command to run */
14+
command?: string
15+
functionsPort?: number
16+
autoLaunch?: boolean
17+
https?: {
18+
keyFile: string
19+
certFile: string
20+
}
21+
}

src/utils/detect-server-settings.js

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// @ts-check
12
const { EOL } = require('os')
23
const path = require('path')
34
const process = require('process')
@@ -101,6 +102,14 @@ const getDefaultDist = () => {
101102
return process.cwd()
102103
}
103104

105+
/**
106+
*
107+
* @param {object} param0
108+
* @param {import('../commands/dev/types').DevConfig} param0.devConfig
109+
* @param {Record<string, string>} param0.flags
110+
* @param {string} param0.projectDir
111+
* @returns {Promise<import('./types').BaseServerSettings>}
112+
*/
104113
const handleStaticServer = async ({ devConfig, flags, projectDir }) => {
105114
validateNumberProperty({ devConfig, property: 'staticServerPort' })
106115

@@ -114,14 +123,6 @@ const handleStaticServer = async ({ devConfig, flags, projectDir }) => {
114123
)
115124
}
116125

117-
if (devConfig.command) {
118-
log(
119-
`${NETLIFYDEVWARN} Ignoring command setting since using a simple static server. Configure ${formatProperty(
120-
'command',
121-
)} ${chalk.bold('and')} ${formatProperty('targetPort')} for a custom setup`,
122-
)
123-
}
124-
125126
if (devConfig.targetPort) {
126127
log(
127128
`${NETLIFYDEVWARN} Ignoring ${formatProperty(
@@ -139,12 +140,18 @@ const handleStaticServer = async ({ devConfig, flags, projectDir }) => {
139140
errorMessage: 'Could not acquire configured static server port',
140141
})
141142
return {
143+
...(devConfig.command && { command: devConfig.command }),
142144
useStaticServer: true,
143145
frameworkPort,
144146
dist,
145147
}
146148
}
147149

150+
/**
151+
* Retrieves the settings from a framework
152+
* @param {import('./types').FrameworkInfo} framework
153+
* @returns {import('./types').BaseServerSettings}
154+
*/
148155
const getSettingsFromFramework = (framework) => {
149156
const {
150157
build: { directory: dist },
@@ -211,6 +218,11 @@ const detectFrameworkSettings = async ({ projectDir }) => {
211218

212219
const hasCommandAndTargetPort = ({ devConfig }) => devConfig.command && devConfig.targetPort
213220

221+
/**
222+
* Creates settings for the custom framework
223+
* @param {*} param0
224+
* @returns {import('./types').BaseServerSettings}
225+
*/
214226
const handleCustomFramework = ({ devConfig }) => {
215227
if (!hasCommandAndTargetPort({ devConfig })) {
216228
throw new Error(
@@ -228,6 +240,11 @@ const handleCustomFramework = ({ devConfig }) => {
228240
}
229241
}
230242

243+
/**
244+
* Handles a forced framework and retrieves the settings for it
245+
* @param {*} param0
246+
* @returns {Promise<import('./types').BaseServerSettings>}
247+
*/
231248
const handleForcedFramework = async ({ devConfig, projectDir }) => {
232249
// this throws if `devConfig.framework` is not a supported framework
233250
const { command, dist, env, framework, frameworkPort, pollingStrategies } = getSettingsFromFramework(
@@ -243,9 +260,17 @@ const handleForcedFramework = async ({ devConfig, projectDir }) => {
243260
}
244261
}
245262

263+
/**
264+
* Get the server settings based on the flags and the devConfig
265+
* @param {import('../commands/dev/types').DevConfig} devConfig
266+
* @param {Record<string, string>} flags
267+
* @param {string} projectDir
268+
* @returns {Promise<import('./types').ServerSettings>}
269+
*/
246270
const detectServerSettings = async (devConfig, flags, projectDir) => {
247271
validateStringProperty({ devConfig, property: 'framework' })
248272

273+
/** @type {Partial<import('./types').BaseServerSettings>} */
249274
let settings = {}
250275

251276
if (flags.dir || devConfig.framework === '#static') {
@@ -254,7 +279,6 @@ const detectServerSettings = async (devConfig, flags, projectDir) => {
254279
} else if (devConfig.framework === '#auto') {
255280
// this is the default CLI behavior
256281

257-
// we don't need to run the detection if both command and targetPort are configured
258282
const runDetection = !hasCommandAndTargetPort({ devConfig })
259283
const frameworkSettings = runDetection ? await detectFrameworkSettings({ projectDir }) : undefined
260284

src/utils/types.d.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
export type FrameworkNames = '#static' | '#auto' | '#custom' | string
2+
3+
export type FrameworkInfo = {
4+
build: {
5+
directory: string
6+
}
7+
dev: {
8+
commands: string[]
9+
port: number
10+
pollingStrategies: { name: string }[]
11+
}
12+
name: FrameworkNames
13+
staticAssetsDirectory: string
14+
env: NodeJS.ProcessEnv
15+
}
16+
17+
export type BaseServerSettings = {
18+
dist: string
19+
20+
// static serving
21+
useStaticServer?: true
22+
23+
// Framework specific part
24+
/** A port where a proxy can listen to it */
25+
frameworkPort?: number
26+
functions?: string
27+
/** The command that was provided for the dev config */
28+
command?: string
29+
/** The framework name ('Create React App') */
30+
framework?: string
31+
env?: NodeJS.ProcessEnv
32+
pollingStrategies?: string[]
33+
}
34+
35+
export type ServerSettings = BaseServerSettings & {
36+
/** default 'secret' */
37+
jwtSecret: string
38+
/** default 'app_metadata.authorization.roles' */
39+
jwtRolePath: string
40+
/** The port where the functions are running on */
41+
port: number
42+
/** The directory of the functions */
43+
functions: number
44+
}

tests/framework-detection.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ test('should use static server when framework is set to #static', async (t) => {
6565
})
6666
})
6767

68-
test('should warn if using static server and `command` is configured', async (t) => {
68+
test('should log the command if using static server and `command` is configured', async (t) => {
6969
await withSiteBuilder('site-with-index-file', async (builder) => {
7070
await builder
7171
.withContentFile({

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,12 @@ Generated by [AVA](https://avajs.dev).
6464
6565
`
6666

67-
## should warn if using static server and `command` is configured
67+
## should log the command if using static server and `command` is configured
6868

6969
> Snapshot 1
7070
7171
`◈ Netlify Dev ◈␊
7272
◈ Using simple static server because '--dir' flag was specified␊
73-
◈ Ignoring command setting since using a simple static server. Configure 'command' and 'targetPort' for a custom setup␊
7473
◈ Running static server from "site-with-index-file/public"␊
7574
7675
◈ Static server listening to 88888␊
@@ -118,7 +117,7 @@ Generated by [AVA](https://avajs.dev).
118117
119118
`◈ Netlify Dev ◈␊
120119
◈ Starting Netlify Dev with Create React App␊
121-
◈ Failed launching framework server. Please verify 'react-scripts' exists`
120+
◈ Failed running command: react-scripts start. Please verify 'react-scripts' exists`
122121

123122
## should throw when forcing a non supported framework
124123

@@ -171,7 +170,7 @@ Generated by [AVA](https://avajs.dev).
171170
◈ Setup a netlify.toml file with a [dev] section to specify your dev server settings.␊
172171
◈ See docs at: https://cli.netlify.com/netlify-dev#project-detection␊
173172
◈ Starting Netlify Dev with #custom␊
174-
◈ Failed launching framework server. Please verify 'oops-i-did-it-again' exists`
173+
◈ Failed running command: oops-i-did-it-again forgot-to-use-a-valid-command. Please verify 'oops-i-did-it-again' exists`
175174

176175
## should prompt when multiple frameworks are detected
177176

0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)