UNPKG

netlify-cli

Version:

Netlify command line tool

252 lines • 12 kB
import { readFile } from 'fs/promises'; import { EOL } from 'os'; import { dirname, relative, resolve } from 'path'; import { getFramework, getSettings } from '@netlify/build-info'; import getPort from 'get-port'; import { detectFrameworkSettings } from './build-info.js'; import { NETLIFYDEVWARN, chalk, log } from './command-helpers.js'; import { acquirePort } from './dev.js'; import { getPluginsToAutoInstall } from './init/utils.js'; const formatProperty = (str) => chalk.magenta(`'${str}'`); const formatValue = (str) => chalk.green(`'${str}'`); const readHttpsSettings = async (options) => { if (typeof options !== 'object' || !options.keyFile || !options.certFile) { throw new TypeError(`https options should be an object with ${formatProperty('keyFile')} and ${formatProperty('certFile')} string properties`); } const { certFile, keyFile } = options; if (typeof keyFile !== 'string') { throw new TypeError(`Private key file configuration should be a string`); } if (typeof certFile !== 'string') { throw new TypeError(`Certificate file configuration should be a string`); } const [key, cert] = await Promise.allSettled([readFile(keyFile, 'utf-8'), readFile(certFile, 'utf-8')]); if (key.status === 'rejected') { throw new Error(`Error reading private key file: ${key.reason}`); } if (cert.status === 'rejected') { throw new Error(`Error reading certificate file: ${cert.reason}`); } return { key: key.value, cert: cert.value, keyFilePath: resolve(keyFile), certFilePath: resolve(certFile) }; }; /** * Validates a property inside the devConfig to be of a given type */ function validateProperty(devConfig, property, type) { if (devConfig[property] && typeof devConfig[property] !== type) { const formattedProperty = formatProperty(property); throw new TypeError(`Invalid ${formattedProperty} option provided in config. The value of ${formattedProperty} option must be of type ${type}`); } } const validateFrameworkConfig = ({ devConfig }) => { validateProperty(devConfig, 'command', 'string'); validateProperty(devConfig, 'port', 'number'); validateProperty(devConfig, 'targetPort', 'number'); if (devConfig.targetPort && devConfig.targetPort === devConfig.port) { throw new Error(`${formatProperty('port')} and ${formatProperty('targetPort')} options cannot have same values. Please consult the documentation for more details: https://ntl.fyi/ports-and-netlify-dev`); } }; const validateConfiguredPort = ({ detectedPort, devConfig }) => { if (devConfig.port && devConfig.port === detectedPort) { const formattedPort = formatProperty('port'); throw new Error(`The ${formattedPort} option you specified conflicts with the port of your application. Please use a different value for ${formattedPort}`); } }; const DEFAULT_PORT = 8888; const DEFAULT_STATIC_PORT = 3999; /** * Logs a message that it was unable to determine the dist directory and falls back to the workingDir */ const getDefaultDist = (workingDir) => { log(`${NETLIFYDEVWARN} Unable to determine public folder to serve files from. Using current working directory`); log(`${NETLIFYDEVWARN} Setup a netlify.toml file with a [dev] section to specify your dev server settings.`); log(`${NETLIFYDEVWARN} See docs at: https://docs.netlify.com/cli/local-development/#project-detection`); return workingDir; }; const getStaticServerPort = async ({ devConfig }) => { const port = await acquirePort({ configuredPort: devConfig.staticServerPort, defaultPort: DEFAULT_STATIC_PORT, errorMessage: 'Could not acquire configured static server port', }); return port; }; const handleStaticServer = async ({ devConfig, flags, workingDir, }) => { validateProperty(devConfig, 'staticServerPort', 'number'); if (flags.dir) { log(`${NETLIFYDEVWARN} Using simple static server because ${formatProperty('--dir')} flag was specified`); } else if (devConfig.framework === '#static') { log(`${NETLIFYDEVWARN} Using simple static server because ${formatProperty('[dev.framework]')} was set to ${formatValue('#static')}`); } if (devConfig.targetPort) { log(`${NETLIFYDEVWARN} Ignoring ${formatProperty('targetPort')} setting since using a simple static server.${EOL}${NETLIFYDEVWARN} Use --staticServerPort or [dev.staticServerPort] to configure the static server port`); } const dist = flags.dir || devConfig.publish || getDefaultDist(workingDir); log(`${NETLIFYDEVWARN} Running static server from "${relative(dirname(workingDir), dist)}"`); const frameworkPort = await getStaticServerPort({ devConfig }); return { ...(devConfig.command && { command: devConfig.command }), useStaticServer: true, frameworkPort, dist, }; }; /** * Retrieves the settings from a framework */ const getSettingsFromDetectedSettings = (command, settings) => { if (!settings) { return; } return { baseDirectory: settings.baseDirectory, command: settings.devCommand, frameworkPort: settings.frameworkPort, dist: settings.dist, framework: settings.framework.name, env: settings.env, pollingStrategies: settings.pollingStrategies, plugins: getPluginsToAutoInstall(command, settings.plugins_from_config_file, settings.plugins_recommended), clearPublishDirectory: settings.clearPublishDirectory, }; }; const hasCommandAndTargetPort = (devConfig) => devConfig.command && devConfig.targetPort; /** * Creates settings for the custom framework */ const handleCustomFramework = ({ devConfig, workingDir, }) => { if (!hasCommandAndTargetPort(devConfig)) { throw new Error(`${formatProperty('command')} and ${formatProperty('targetPort')} properties are required when ${formatProperty('framework')} is set to ${formatValue('#custom')}`); } return { command: devConfig.command, frameworkPort: devConfig.targetPort, dist: devConfig.publish || getDefaultDist(workingDir), framework: '#custom', pollingStrategies: devConfig.pollingStrategies ?? [], }; }; /** * Merges the framework settings with the devConfig */ const mergeSettings = async ({ devConfig, frameworkSettings, workingDir, }) => { const command = devConfig.command || frameworkSettings?.command; const frameworkPort = devConfig.targetPort || frameworkSettings?.frameworkPort; const useStaticServer = !(command && frameworkPort); return { baseDirectory: devConfig.base || frameworkSettings?.baseDirectory, command, frameworkPort: useStaticServer ? await getStaticServerPort({ devConfig }) : frameworkPort, dist: devConfig.publish || frameworkSettings?.dist || getDefaultDist(workingDir), framework: frameworkSettings?.framework, env: frameworkSettings?.env, pollingStrategies: frameworkSettings?.pollingStrategies ?? [], useStaticServer, clearPublishDirectory: frameworkSettings?.clearPublishDirectory, }; }; /** * Handles a forced framework and retrieves the settings for it */ const handleForcedFramework = async (options) => { // this throws if `devConfig.framework` is not a supported framework const framework = await getFramework(options.devConfig.framework, options.project); const settings = await getSettings(framework, options.project, options.workspacePackage || ''); const frameworkSettings = getSettingsFromDetectedSettings(options.command, settings); // TODO(serhalp): Remove and update `getSettingsFromDetectedSettings` type to return non-nullable // when given non-nullable second arg if (frameworkSettings == null) { throw new Error(`Could not get settings for framework ${options.devConfig.framework}`); } return mergeSettings({ devConfig: options.devConfig, workingDir: options.workingDir, frameworkSettings }); }; /** * Get the server settings based on the flags and the devConfig */ const detectServerSettings = async (devConfig, flags, command) => { validateProperty(devConfig, 'framework', 'string'); // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO(serhalp): Set to `BaseServerSettings`. Good luck! let settings = {}; if (flags.dir || devConfig.framework === '#static') { // serving files statically without a framework server settings = await handleStaticServer({ flags, devConfig, workingDir: command.workingDir }); } else if (devConfig.framework === '#auto') { // this is the default CLI behavior const runDetection = !hasCommandAndTargetPort(devConfig); const frameworkSettings = runDetection ? getSettingsFromDetectedSettings(command, await detectFrameworkSettings(command, 'dev')) : undefined; if (frameworkSettings === undefined && runDetection) { log(`${NETLIFYDEVWARN} No app server detected. Using simple static server`); settings = await handleStaticServer({ flags, devConfig, workingDir: command.workingDir }); } else { validateFrameworkConfig({ devConfig }); settings = await mergeSettings({ devConfig, frameworkSettings, workingDir: command.workingDir }); } settings.plugins = frameworkSettings?.plugins; } else if (devConfig.framework === '#custom') { validateFrameworkConfig({ devConfig }); // when the users wants to configure `command` and `targetPort` settings = handleCustomFramework({ devConfig, workingDir: command.workingDir }); } else if (devConfig.framework) { validateFrameworkConfig({ devConfig }); // this is when the user explicitly configures a framework, e.g. `framework = "gatsby"` settings = await handleForcedFramework({ command, devConfig, project: command.project, workingDir: command.workingDir, workspacePackage: command.workspacePackage, }); } validateConfiguredPort({ devConfig, detectedPort: settings.frameworkPort }); const acquiredPort = await acquirePort({ configuredPort: devConfig.port, defaultPort: DEFAULT_PORT, errorMessage: `Could not acquire required ${formatProperty('port')}`, }); const functionsDir = devConfig.functions || settings.functions; return { ...settings, port: acquiredPort, jwtSecret: devConfig.jwtSecret || 'secret', jwtRolePath: devConfig.jwtRolePath || 'app_metadata.authorization.roles', functions: functionsDir, functionsPort: await getPort({ port: devConfig.functionsPort || 0 }), ...(devConfig.https && { https: await readHttpsSettings(devConfig.https) }), }; }; /** * Returns a copy of the provided config with any plugins provided by the * server settings */ export const getConfigWithPlugins = (config, settings) => { if (!settings.plugins) { return config; } // If there are plugins that we should be running for this site, add them // to the config as if they were declared in netlify.toml. We must check // whether the plugin has already been added by another source (like the // TOML file or the UI), as we don't want to run the same plugin twice. const { plugins: existingPlugins = [] } = config; const existingPluginNames = new Set(existingPlugins.map((plugin) => plugin.package)); const newPlugins = settings.plugins .map((pluginName) => { if (existingPluginNames.has(pluginName)) { return; } return { package: pluginName, origin: 'config', inputs: {} }; }) .filter((plugin) => plugin != null); return { ...config, plugins: [...newPlugins, ...(config.plugins ?? [])], }; }; export default detectServerSettings; //# sourceMappingURL=detect-server-settings.js.map