netlify-cli
Version:
Netlify command line tool
272 lines (241 loc) • 10.2 kB
JavaScript
const fs = require('fs')
const path = require('path')
const process = require('process')
const chalk = require('chalk')
const fuzzy = require('fuzzy')
const getPort = require('get-port')
const inquirer = require('inquirer')
const inquirerAutocompletePrompt = require('inquirer-autocomplete-prompt')
const { NETLIFYDEVLOG, NETLIFYDEVWARN } = require('./logo')
const serverSettings = async (devConfig, flags, projectDir, log) => {
let settings = {}
const detectorsFiles = fs
.readdirSync(path.join(__dirname, '..', 'detectors'))
// only accept .js detector files
.filter((filename) => filename.endsWith('.js'))
if (typeof devConfig.framework !== 'string') throw new Error('Invalid "framework" option provided in config')
if (flags.dir) {
settings = await getStaticServerSettings(settings, flags, projectDir, log)
;['command', 'targetPort'].forEach((property) => {
if (flags[property]) {
throw new Error(
`"${property}" option cannot be used in conjunction with "dir" flag which is used to run a static server`,
)
}
})
} else if (devConfig.framework === '#auto' && !(devConfig.command && devConfig.targetPort)) {
const settingsArr = []
const detectors = detectorsFiles.map((det) => {
try {
return loadDetector(det)
} catch (error) {
console.error(error)
return null
}
})
for (const detector of detectors) {
const detectorResult = detector(projectDir)
if (detectorResult) settingsArr.push(detectorResult)
}
if (settingsArr.length === 1) {
const [firstSettings] = settingsArr
settings = firstSettings
settings.args = chooseDefaultArgs(settings.possibleArgsArrs)
} else if (settingsArr.length > 1) {
/** multiple matching detectors, make the user choose */
inquirer.registerPrompt('autocomplete', inquirerAutocompletePrompt)
const scriptInquirerOptions = formatSettingsArrForInquirer(settingsArr)
const { chosenSetting } = await inquirer.prompt({
name: 'chosenSetting',
message: `Multiple possible start commands found`,
type: 'autocomplete',
source(_, input) {
if (!input || input === '') {
return scriptInquirerOptions
}
// only show filtered results
return filterSettings(scriptInquirerOptions, input)
},
})
// finally! we have a selected option
settings = chosenSetting
log(
`Add \`framework = "${chosenSetting.framework}"\` to [dev] section of your netlify.toml to avoid this selection prompt next time`,
)
}
} else if (devConfig.framework === '#custom' || (devConfig.command && devConfig.targetPort)) {
settings.framework = '#custom'
if (
devConfig.framework &&
!['command', 'targetPort'].every((property) => Object.prototype.hasOwnProperty.call(devConfig, property))
) {
throw new Error('"command" and "targetPort" properties are required when "framework" is set to "#custom"')
}
if (devConfig.framework !== '#custom' && devConfig.command && devConfig.targetPort) {
throw new Error(
'"framework" option must be set to "#custom" when specifying both "command" and "targetPort" options',
)
}
} else if (devConfig.framework === '#static') {
// Do nothing
} else {
const detectorName = detectorsFiles.find((dt) => dt === `${devConfig.framework}.js`)
if (!detectorName)
throw new Error(
'Unsupported value provided for "framework" option in config. Please use "#custom"' +
` if you're using a framework not intrinsically supported by Netlify Dev. E.g. with "command" and "targetPort" options.` +
` Or use one of following values: ${detectorsFiles
.map((detectorFile) => `"${path.parse(detectorFile).name}"`)
.join(', ')}`,
)
const detector = loadDetector(detectorName)
const detectorResult = detector(projectDir)
if (!detectorResult)
throw new Error(
`Specified "framework" detector "${devConfig.framework}" did not pass requirements for your project`,
)
settings = detectorResult
settings.args = chooseDefaultArgs(detectorResult.possibleArgsArrs)
}
if (settings.command === 'npm' && !['start', 'run'].includes(settings.args[0])) {
settings.args.unshift('run')
}
if (!settings.noCmd && devConfig.command) {
console.log(
`${NETLIFYDEVLOG} Overriding ${chalk.yellow('command')} with setting derived from netlify.toml [dev] block: ${
devConfig.command
}`,
)
const [devConfigCommand, ...devConfigArgs] = devConfig.command.split(/\s+/)
settings.command = devConfigCommand
settings.args = devConfigArgs
}
settings.dist = flags.dir || devConfig.publish || settings.dist
if (devConfig.targetPort) {
if (devConfig.targetPort && typeof devConfig.targetPort !== 'number') {
throw new Error('Invalid "targetPort" option specified. The value of "targetPort" option must be an integer')
}
if (devConfig.targetPort === devConfig.port) {
throw new Error(
'"port" and "targetPort" options cannot have same values. Please consult the documentation for more details: https://cli.netlify.com/netlify-dev#netlifytoml-dev-block',
)
}
if (!settings.command)
throw new Error(
'No "command" specified or detected. The "command" option is required to use "targetPort" option.',
)
if (flags.dir)
throw new Error(
'"targetPort" option cannot be used in conjunction with "dir" flag which is used to run a static server.',
)
settings.frameworkPort = devConfig.targetPort
}
if (devConfig.port && devConfig.port === settings.frameworkPort) {
throw new Error(
'The "port" option you specified conflicts with the port of your application. Please use a different value for "port"',
)
}
if (!settings.command && !settings.framework && !settings.noCmd) {
settings = await getStaticServerSettings(settings, flags, projectDir, log)
}
if (!settings.frameworkPort) throw new Error('No "targetPort" option specified or detected.')
if (devConfig.port && typeof devConfig.port !== 'number') {
throw new Error('Invalid "port" option specified. The value of "port" option must be an integer')
}
if (devConfig.port && devConfig.port === settings.frameworkPort) {
throw new Error(
'The "port" option you specified conflicts with the port of your application. Please use a different value for "port"',
)
}
const triedPort = devConfig.port || DEFAULT_PORT
settings.port = await getPort({ port: triedPort })
if (triedPort !== settings.port && devConfig.port) {
throw new Error(`Could not acquire required "port": ${triedPort}`)
}
settings.jwtSecret = devConfig.jwtSecret || 'secret'
settings.jwtRolePath = devConfig.jwtRolePath || 'app_metadata.authorization.roles'
settings.functions = devConfig.functions || settings.functions
if (settings.functions) {
settings.functionsPort = await getPort({ port: settings.functionsPort || 0 })
}
return settings
}
const DEFAULT_PORT = 8888
const getStaticServerSettings = async function (settings, flags, projectDir, log) {
let { dist } = settings
if (flags.dir) {
log(`${NETLIFYDEVWARN} Using simple static server because --dir flag was specified`)
dist = flags.dir
} else {
log(`${NETLIFYDEVWARN} No app server detected and no "command" specified`)
}
if (!dist) {
log(`${NETLIFYDEVLOG} Using current working directory`)
log(`${NETLIFYDEVWARN} Unable to determine public folder to serve files from`)
log(`${NETLIFYDEVWARN} Setup a netlify.toml file with a [dev] section to specify your dev server settings.`)
log(`${NETLIFYDEVWARN} See docs at: https://cli.netlify.com/netlify-dev#project-detection`)
dist = process.cwd()
}
log(`${NETLIFYDEVWARN} Running static server from "${path.relative(path.dirname(projectDir), dist)}"`)
return {
noCmd: true,
frameworkPort: await getPort({ port: flags.staticServerPort || DEFAULT_STATIC_PORT }),
dist,
}
}
const DEFAULT_STATIC_PORT = 3999
const loadDetector = function (detectorName) {
try {
// eslint-disable-next-line node/global-require, import/no-dynamic-require
return require(path.join(__dirname, '..', 'detectors', detectorName))
} catch (error) {
throw new Error(
`Failed to load detector: ${chalk.yellow(
detectorName,
)}, this is likely a bug in the detector, please file an issue in netlify-cli\n ${error}`,
)
}
}
const chooseDefaultArgs = function (possibleArgsArrs) {
// vast majority of projects will only have one matching detector
// just pick the first one
const [args] = possibleArgsArrs
if (!args) {
const { scripts } = JSON.parse(fs.readFileSync('package.json', { encoding: 'utf8' }))
const err = new Error(
'Empty args assigned, this is an internal Netlify Dev bug, please report your settings and scripts so we can improve',
)
err.scripts = scripts
err.possibleArgsArrs = possibleArgsArrs
throw err
}
return args
}
/** utilities for the inquirer section above */
const filterSettings = function (scriptInquirerOptions, input) {
const filteredSettings = fuzzy.filter(
input,
scriptInquirerOptions.map((scriptInquirerOption) => scriptInquirerOption.name),
)
const filteredSettingNames = new Set(
filteredSettings.map((filteredSetting) => (input ? filteredSetting.string : filteredSetting)),
)
return scriptInquirerOptions.filter((t) => filteredSettingNames.has(t.name))
}
/** utiltities for the inquirer section above */
const formatSettingsArrForInquirer = function (settingsArr) {
return [].concat(
...settingsArr.map((setting) =>
setting.possibleArgsArrs.map((args) => ({
name: `[${chalk.yellow(setting.framework)}] ${setting.command} ${args.join(' ')}`,
value: { ...setting, args },
short: `${setting.framework}-${args.join(' ')}`,
})),
),
)
}
module.exports = {
serverSettings,
loadDetector,
chooseDefaultArgs,
}