UNPKG

netlify-cli

Version:

Netlify command line tool

249 lines (248 loc) • 11.8 kB
import process from 'process'; import { applyMutations } from '@netlify/config'; import { Option } from 'commander'; import { BLOBS_CONTEXT_VARIABLE, encodeBlobsContext, getBlobsContextWithEdgeAccess } from '../../lib/blobs/blobs.js'; import { promptEditorHelper } from '../../lib/edge-functions/editor-helper.js'; import { startFunctionsServer } from '../../lib/functions/server.js'; import { printBanner } from '../../utils/banner.js'; import { BANG, NETLIFYDEV, NETLIFYDEVERR, NETLIFYDEVLOG, NETLIFYDEVWARN, chalk, log, normalizeConfig, } from '../../utils/command-helpers.js'; import detectServerSettings, { getConfigWithPlugins } from '../../utils/detect-server-settings.js'; import { UNLINKED_SITE_MOCK_ID, getDotEnvVariables, getSiteInformation, injectEnvVariables } from '../../utils/dev.js'; import { getEnvelopeEnv, normalizeContext } from '../../utils/env/index.js'; import { ensureNetlifyIgnore } from '../../utils/gitignore.js'; import { getLiveTunnelSlug, startLiveTunnel } from '../../utils/live-tunnel.js'; import openBrowser from '../../utils/open-browser.js'; import { generateInspectSettings, startProxyServer } from '../../utils/proxy-server.js'; import { getProxyUrl } from '../../utils/proxy.js'; import { runDevTimeline } from '../../utils/run-build.js'; import { getGeoCountryArgParser } from '../../utils/validation.js'; import { createDevExecCommand } from './dev-exec.js'; /** * * @param {object} config * @param {*} config.api * @param {import('commander').OptionValues} config.options * @param {import('../../utils/types.js').ServerSettings} config.settings * @param {*} config.site * @param {*} config.state * @returns */ // @ts-expect-error TS(7031) FIXME: Binding element 'api' implicitly has an 'any' type... Remove this comment to see the full error message const handleLiveTunnel = async ({ api, options, settings, site, state }) => { const { live } = options; if (live) { const customSlug = typeof live === 'string' && live.length !== 0 ? live : undefined; const slug = getLiveTunnelSlug(state, customSlug); let message = `${NETLIFYDEVWARN} Creating live URL with ID ${chalk.yellow(slug)}`; if (!customSlug) { message += ` (to generate a custom URL, use ${chalk.magenta('--live=<subdomain>')})`; } log(message); const sessionUrl = await startLiveTunnel({ siteId: site.id, netlifyApiToken: api.accessToken, localPort: settings.port, slug, }); process.env.BASE_URL = sessionUrl; return sessionUrl; } }; const validateShortFlagArgs = (args) => { if (args.startsWith('=')) { throw new Error(`Short flag options like -e or -E don't support the '=' sign ${chalk.red(BANG)} Supported formats: netlify dev -e netlify dev -e 127.0.0.1:9229 netlify dev -e127.0.0.1:9229 netlify dev -E netlify dev -E 127.0.0.1:9229 netlify dev -E127.0.0.1:9229`); } return args; }; export const dev = async (options, command) => { log(`${NETLIFYDEV}`); const { api, cachedConfig, config, repositoryRoot, site, siteInfo, state } = command.netlify; config.dev = { ...config.dev }; config.build = { ...config.build }; const devConfig = { framework: '#auto', autoLaunch: Boolean(options.open), ...(cachedConfig.siteInfo?.dev_server_settings && { command: cachedConfig.siteInfo.dev_server_settings.cmd, targetPort: cachedConfig.siteInfo.dev_server_settings.target_port, }), ...(config.functionsDirectory && { functions: config.functionsDirectory }), ...(config.build.publish && { publish: config.build.publish }), ...(config.build.base && { base: config.build.base }), ...config.dev, ...options, }; let { env } = cachedConfig; env.NETLIFY_DEV = { sources: ['internal'], value: 'true' }; const blobsContext = await getBlobsContextWithEdgeAccess({ debug: options.debug, projectRoot: command.workingDir, siteID: site.id ?? UNLINKED_SITE_MOCK_ID, }); env[BLOBS_CONTEXT_VARIABLE] = { sources: ['internal'], value: encodeBlobsContext(blobsContext) }; if (!options.offline && !options.offlineEnv) { env = await getEnvelopeEnv({ api, context: options.context, env, siteInfo }); log(`${NETLIFYDEVLOG} Injecting environment variable values for ${chalk.yellow('all scopes')}`); } env = await getDotEnvVariables({ devConfig, env, site }); injectEnvVariables(env); await promptEditorHelper({ chalk, config, log, NETLIFYDEVLOG, repositoryRoot, state }); const { accountId, addonsUrls, capabilities, siteUrl, timeouts } = await getSiteInformation({ // inherited from base command --offline offline: options.offline, api, site, siteInfo, }); /** @type {import('../../utils/types.js').ServerSettings} */ let settings; try { settings = await detectServerSettings(devConfig, options, command); const { NETLIFY_INCLUDE_DEV_SERVER_PLUGIN } = process.env; if (NETLIFY_INCLUDE_DEV_SERVER_PLUGIN) { const plugins = NETLIFY_INCLUDE_DEV_SERVER_PLUGIN.split(','); if (options.debug) { log(`${NETLIFYDEVLOG} Including dev server plugins: ${NETLIFY_INCLUDE_DEV_SERVER_PLUGIN}`); } settings.plugins = [...(settings.plugins || []), ...plugins]; } cachedConfig.config = getConfigWithPlugins(cachedConfig.config, settings); } catch (error_) { if (error_ && typeof error_ === 'object' && 'message' in error_) { log(NETLIFYDEVERR, error_.message); } process.exit(1); } command.setAnalyticsPayload({ live: options.live }); const liveTunnelUrl = await handleLiveTunnel({ options, site, api, settings, state }); const url = liveTunnelUrl || getProxyUrl(settings); process.env.URL = url; process.env.DEPLOY_URL = url; log(`${NETLIFYDEVWARN} Setting up local development server`); const { configMutations, configPath: configPathOverride } = await runDevTimeline({ command, options, settings, env: { URL: url, DEPLOY_URL: url, }, }); const mutatedConfig = applyMutations(config, configMutations); const functionsRegistry = await startFunctionsServer({ blobsContext, command, config: mutatedConfig, debug: options.debug, settings, site, siteInfo, siteUrl, capabilities, timeouts, geolocationMode: options.geo, geoCountry: options.country, offline: options.offline, state, accountId, }); // Try to add `.netlify` to `.gitignore`. try { await ensureNetlifyIgnore(repositoryRoot); } catch { // no-op } // TODO: We should consolidate this with the existing config watcher. const getUpdatedConfig = async () => { const { config: newConfig } = await command.getConfig({ cwd: command.workingDir, offline: true, }); const normalizedNewConfig = normalizeConfig(newConfig); return normalizedNewConfig; }; const inspectSettings = generateInspectSettings(options.edgeInspect, options.edgeInspectBrk); await startProxyServer({ addonsUrls, api, blobsContext, command, config: mutatedConfig, configPath: configPathOverride, debug: options.debug, disableEdgeFunctions: options.internalDisableEdgeFunctions, projectDir: command.workingDir, env, getUpdatedConfig, inspectSettings, offline: options.offline, settings, site, siteInfo, state, geolocationMode: options.geo, geoCountry: options.country, accountId, functionsRegistry, repositoryRoot, }); if (devConfig.autoLaunch !== false) { await openBrowser({ url, silentBrowserNoneError: true }); } printBanner({ url }); }; export const createDevCommand = (program) => { createDevExecCommand(program); return program .command('dev') .alias('develop') .description(`Local dev server\nThe dev command will run a local dev server with Netlify's proxy and redirect rules`) .option('-c ,--command <command>', 'command to run') .option('--context <context>', 'Specify a deploy context or branch for environment variables (contexts: "production", "deploy-preview", "branch-deploy", "dev")', normalizeContext) .option('-p ,--port <port>', 'port of netlify dev', (value) => Number.parseInt(value)) .addOption(new Option('--skip-wait-port', 'disables waiting for target port to become available').hideHelp(true)) .addOption(new Option('--no-open', 'disables the automatic opening of a browser window')) .option('--target-port <port>', 'port of target app server', (value) => Number.parseInt(value)) .option('--framework <name>', 'framework to use. Defaults to #auto which automatically detects a framework') .option('-d ,--dir <path>', 'dir with static files') .option('-f ,--functions <folder>', 'specify a functions folder to serve') .option('-o, --offline', 'Disables any features that require network access') .addOption(new Option('--offline-env', 'disables fetching environment variables from the Netlify API').hideHelp(true)) .addOption(new Option('--internal-disable-edge-functions', "disables edge functions. use this if your environment doesn't support Deno. This option is internal and should not be used by end users.").hideHelp(true)) .option('-l, --live [subdomain]', 'start a public live session; optionally, supply a subdomain to generate a custom URL', false) .option('--functions-port <port>', 'port of functions server', (value) => Number.parseInt(value)) .addOption(new Option('--geo <mode>', 'force geolocation data to be updated, use cached data from the last 24h if found, or use a mock location') .choices(['cache', 'mock', 'update']) .default('cache')) .addOption(new Option('--country <geoCountry>', 'Two-letter country code (https://ntl.fyi/country-codes) to use as mock geolocation (enables --geo=mock automatically)').argParser(getGeoCountryArgParser('netlify dev --geo=mock --country=FR'))) .addOption(new Option('--staticServerPort <port>', 'port of the static app server used when no framework is detected') .argParser((value) => Number.parseInt(value)) .hideHelp()) .addOption(new Option('-e, --edge-inspect [address]', 'enable the V8 Inspector Protocol for Edge Functions, with an optional address in the host:port format') .conflicts('edgeInspectBrk') .argParser(validateShortFlagArgs)) .addOption(new Option('-E, --edge-inspect-brk [address]', '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') .conflicts('edgeInspect') .argParser(validateShortFlagArgs)) .addExamples([ 'netlify dev', 'netlify dev -d public', 'netlify dev -c "hugo server -w" --target-port 1313', 'netlify dev --context production', 'netlify dev --edge-inspect', 'netlify dev --edge-inspect=127.0.0.1:9229', 'netlify dev --edge-inspect-brk', 'netlify dev --edge-inspect-brk=127.0.0.1:9229', 'BROWSER=none netlify dev # disable browser auto opening', ]) .action(dev); };