From 087f7dd47e1158e8955c365baa2d8a51fac9056e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Fri, 23 Dec 2022 13:02:25 +0000 Subject: [PATCH 01/43] feat: add `--serve` flag to `dev` command --- npm-shrinkwrap.json | 11 +++ package.json | 1 + src/commands/dev/dev.mjs | 166 ++++++++++++++++++++++++++++++++++----- 3 files changed, 157 insertions(+), 21 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 13f74f2d9bb..5fac066527e 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -99,6 +99,7 @@ "static-server": "^2.2.1", "string-similarity": "^4.0.4", "strip-ansi-control-characters": "^2.0.0", + "symlink-or-copy": "^1.3.1", "tabtab": "^3.0.2", "tempy": "^1.0.0", "terminal-link": "^2.1.1", @@ -21984,6 +21985,11 @@ "node": ">=0.10.0" } }, + "node_modules/symlink-or-copy": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz", + "integrity": "sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==" + }, "node_modules/synckit": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.1.tgz", @@ -40319,6 +40325,11 @@ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" }, + "symlink-or-copy": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz", + "integrity": "sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==" + }, "synckit": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.1.tgz", diff --git a/package.json b/package.json index 2d0ef4c7723..ee8e1740519 100644 --- a/package.json +++ b/package.json @@ -319,6 +319,7 @@ "static-server": "^2.2.1", "string-similarity": "^4.0.4", "strip-ansi-control-characters": "^2.0.0", + "symlink-or-copy": "^1.3.1", "tabtab": "^3.0.2", "tempy": "^1.0.0", "terminal-link": "^2.1.1", diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index eecf81e56e3..0e2ab6cd927 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -1,5 +1,6 @@ // @ts-check import events from 'events' +import { promises as fs } from 'fs' import path from 'path' import process from 'process' import { promisify } from 'util' @@ -9,8 +10,10 @@ import { Option } from 'commander' import execa from 'execa' import StaticServer from 'static-server' import stripAnsiCc from 'strip-ansi-control-characters' +import { sync as symlinkOrCopy } from 'symlink-or-copy' import waitPort from 'wait-port' +import { INTERNAL_EDGE_FUNCTIONS_FOLDER } from '../../lib/edge-functions/consts.mjs' import { promptEditorHelper } from '../../lib/edge-functions/editor-helper.mjs' import { startFunctionsServer } from '../../lib/functions/server.mjs' import { @@ -26,6 +29,7 @@ import { getNetlifyGraphConfig, readGraphQLOperationsSourceFile, } from '../../lib/one-graph/cli-netlify-graph.mjs' +import { getPathInProject } from '../../lib/settings.cjs' import { startSpinner, stopSpinner } from '../../lib/spinner.cjs' import { BANG, @@ -45,6 +49,7 @@ import { import detectServerSettings from '../../utils/detect-server-settings.mjs' import { generateNetlifyGraphJWT, getSiteInformation, injectEnvVariables, processOnExit } from '../../utils/dev.mjs' import { getEnvelopeEnv, normalizeContext } from '../../utils/env/index.mjs' +import { getInternalFunctionsDir } from '../../utils/functions/index.mjs' import { ensureNetlifyIgnore } from '../../utils/gitignore.mjs' import { startLiveTunnel } from '../../utils/live-tunnel.mjs' import openBrowser from '../../utils/open-browser.mjs' @@ -244,6 +249,7 @@ const FRAMEWORK_PORT_TIMEOUT = 6e5 * @param {object} params * @param {*} params.addonsUrls * @param {import('../base-command.mjs').NetlifyOptions["config"]} params.config + * @param {string} [params.configPath] An override for the Netlify config path * @param {import('../base-command.mjs').NetlifyOptions["cachedConfig"]['env']} params.env * @param {InspectSettings} params.inspectSettings * @param {() => Promise} params.getUpdatedConfig @@ -259,6 +265,7 @@ const FRAMEWORK_PORT_TIMEOUT = 6e5 const startProxyServer = async ({ addonsUrls, config, + configPath, env, geoCountry, geolocationMode, @@ -273,7 +280,7 @@ const startProxyServer = async ({ const url = await startProxy({ addonsUrls, config, - configPath: site.configPath, + configPath: configPath || site.configPath, env, geolocationMode, geoCountry, @@ -428,7 +435,6 @@ const validateGeoCountryCode = (arg) => { const dev = async (options, command) => { log(`${NETLIFYDEV}`) const { api, cachedConfig, config, repositoryRoot, site, siteInfo, state } = command.netlify - const netlifyBuild = await netlifyBuildPromise config.dev = { ...config.dev } config.build = { ...config.build } /** @type {import('./types').DevConfig} */ @@ -442,6 +448,12 @@ const dev = async (options, command) => { let { env } = cachedConfig + if (options.serve) { + ensureNodeModulesForPlugins({ siteRoot: site.root }) + + devConfig.framework = '#static' + } + if (!options.offline && siteInfo.use_envelope) { env = await getEnvelopeEnv({ api, context: options.context, env, siteInfo }) log(`${NETLIFYDEVLOG} Injecting environment variable values for ${chalk.yellow('all scopes')}`) @@ -494,6 +506,10 @@ const dev = async (options, command) => { startPollingForAPIAuthentication({ api, command, config, site, siteInfo }) } + log(`${NETLIFYDEVWARN} Setting up local development server`) + + const { configPath: configPathOverride } = await runBuild({ cachedConfig, options, settings, site }) + await startFunctionsServer({ api, command, @@ -506,23 +522,6 @@ const dev = async (options, command) => { timeouts, }) - log(`${NETLIFYDEVWARN} Setting up local development server`) - - const devCommand = async () => { - const { ipVersion } = await startFrameworkServer({ settings }) - - settings.frameworkHost = ipVersion === 6 ? '::1' : '127.0.0.1' - } - const startDevOptions = getBuildOptions({ - cachedConfig, - options, - }) - const { error: startDevError, success } = await netlifyBuild.startDev(devCommand, startDevOptions) - - if (!success) { - error(`Could not start local development server\n\n${startDevError.message}\n\n${startDevError.stack}`) - } - // Try to add `.netlify` to `.gitignore`. try { await ensureNetlifyIgnore(repositoryRoot) @@ -544,6 +543,7 @@ const dev = async (options, command) => { let url = await startProxyServer({ addonsUrls, config, + configPath: configPathOverride, env: command.netlify.cachedConfig.env, geolocationMode: options.geo, geoCountry: options.country, @@ -668,9 +668,90 @@ const dev = async (options, command) => { printBanner({ url }) } -const getBuildOptions = ({ cachedConfig, options: { context, cwd = process.cwd(), debug, dry, offline }, token }) => ({ +// Copies `netlify.toml`, if one is defined, into the `.netlify` internal +// directory and returns the path to its new location. +const copyConfig = async ({ configPath, siteRoot }) => { + const newConfigPath = path.resolve(siteRoot, getPathInProject(['netlify.toml'])) + + try { + await fs.copyFile(configPath, newConfigPath) + } catch { + // no-op + } + + return newConfigPath +} + +// Loads and runs Netlify Build. Chooses the right flags and entry point based +// on the options supplied. +const runBuild = async ({ cachedConfig, options, settings, site }) => { + const { default: buildSite, startDev } = await netlifyBuildPromise + const sharedOptions = getBuildOptions({ + cachedConfig, + options, + }) + const devCommand = async (settingsOverrides = {}) => { + const { ipVersion } = await startFrameworkServer({ + settings: { + ...settings, + ...settingsOverrides, + }, + }) + + settings.frameworkHost = ipVersion === 6 ? '::1' : '127.0.0.1' + } + + if (options.serve) { + // Start by cleaning the internal directory, as it may have artifacts left + // by previous builds. + await cleanInternalDirectory(site.root) + + // Copy `netlify.toml` into the internal directory. This will be the new + // location of the config file for the duration of the command. + const tempConfigPath = await copyConfig({ configPath: cachedConfig.configPath, siteRoot: site.root }) + const buildSiteOptions = { + ...sharedOptions, + outputConfigPath: tempConfigPath, + saveConfig: true, + } + + // Run Netlify Build using the main entry point. + await buildSite(buildSiteOptions) + + // Start the dev server, forcing the usage of a static server as opposed to + // the framework server. + await devCommand({ + command: undefined, + useStaticServer: true, + }) + + return { configPath: tempConfigPath } + } + + // Enable `quiet` to suppress non-essential output from Netlify Build. + const startDevOptions = { + ...sharedOptions, + quiet: true, + } + + // Run Netlify Build using the `startDev` entry point. + const { error: startDevError, success } = await startDev(devCommand, startDevOptions) + + if (!success) { + error(`Could not start local development server\n\n${startDevError.message}\n\n${startDevError.stack}`) + } + + return {} +} + +const getBuildOptions = ({ cachedConfig, - token, + options: { configPath, context, cwd = process.cwd(), debug, dry, offline, quiet, saveConfig }, +}) => ({ + cachedConfig, + configPath, + siteId: cachedConfig.siteInfo.id, + token: cachedConfig.token, dry, debug, context, @@ -679,8 +760,50 @@ const getBuildOptions = ({ cachedConfig, options: { context, cwd = process.cwd() buffer: false, offline, cwd, + quiet, + saveConfig, }) +// Any Node modules used by internal serverless functions will be installed at +// `.netlify/plugins/node_modules`, which isn't part of the resolution paths +// used by code in `.netlify/functions`. To make sure modules can resolve, we +// create a symlink for the plugin's node_modules at `.netlify/node_modules`. +const ensureNodeModulesForPlugins = ({ siteRoot }) => { + const targetPath = path.resolve(siteRoot, getPathInProject(['plugins', 'node_modules'])) + const linkPath = path.resolve(siteRoot, getPathInProject(['node_modules'])) + + try { + symlinkOrCopy(targetPath, linkPath) + } catch (symlinkError) { + if (symlinkError.code === 'EEXIST') { + return + } + + warn( + `There was an error setting up the node_modules directory at ${linkPath}, which may cause some serverless functions to not work as expected. Please ensure that you have write permissions on this directory.`, + ) + } +} + +const cleanInternalDirectory = async (basePath) => { + const internalFunctionsDirectory = await getInternalFunctionsDir({ base: basePath }) + + // Remove any internal serverless functions. + if (internalFunctionsDirectory) { + await fs.rm(internalFunctionsDirectory, { force: true, recursive: true }) + } + + const internalEdgeFunctionsDirectory = path.resolve(basePath, getPathInProject([INTERNAL_EDGE_FUNCTIONS_FOLDER])) + + // Remove any internal edge functions. + await fs.rm(internalEdgeFunctionsDirectory, { force: true, recursive: true }) + + const configPath = path.join(basePath, 'netlify.toml') + + // Remove any config file. + await fs.rm(configPath, { force: true }) +} + /** * Creates the `netlify dev` command * @param {import('../base-command.mjs').default} program @@ -710,6 +833,7 @@ export const createDevCommand = (program) => { .option('-o ,--offline', 'disables any features that require network access') .option('-l, --live', 'start a public live session', false) .option('--functionsPort ', 'port of functions server', (value) => Number.parseInt(value)) + .option('-s, --serve', 'run in "serve" mode', false) .addOption( new Option( '--geo ', From b3f582d7a8558cfef4b3eba1532f72c8d7aa130b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Fri, 23 Dec 2022 15:22:07 +0000 Subject: [PATCH 02/43] refactor: simplify internal directory clean up --- src/commands/dev/dev.mjs | 21 ++++++--------------- src/utils/functions/functions.mjs | 4 +++- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index 0e2ab6cd927..2fdd3d654c4 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -49,7 +49,7 @@ import { import detectServerSettings from '../../utils/detect-server-settings.mjs' import { generateNetlifyGraphJWT, getSiteInformation, injectEnvVariables, processOnExit } from '../../utils/dev.mjs' import { getEnvelopeEnv, normalizeContext } from '../../utils/env/index.mjs' -import { getInternalFunctionsDir } from '../../utils/functions/index.mjs' +import { INTERNAL_FUNCTIONS_FOLDER } from '../../utils/functions/index.mjs' import { ensureNetlifyIgnore } from '../../utils/gitignore.mjs' import { startLiveTunnel } from '../../utils/live-tunnel.mjs' import openBrowser from '../../utils/open-browser.mjs' @@ -786,22 +786,13 @@ const ensureNodeModulesForPlugins = ({ siteRoot }) => { } const cleanInternalDirectory = async (basePath) => { - const internalFunctionsDirectory = await getInternalFunctionsDir({ base: basePath }) + const ops = [INTERNAL_FUNCTIONS_FOLDER, INTERNAL_EDGE_FUNCTIONS_FOLDER, 'netlify.toml'].map((name) => { + const fullPath = path.resolve(basePath, getPathInProject([name])) - // Remove any internal serverless functions. - if (internalFunctionsDirectory) { - await fs.rm(internalFunctionsDirectory, { force: true, recursive: true }) - } - - const internalEdgeFunctionsDirectory = path.resolve(basePath, getPathInProject([INTERNAL_EDGE_FUNCTIONS_FOLDER])) - - // Remove any internal edge functions. - await fs.rm(internalEdgeFunctionsDirectory, { force: true, recursive: true }) - - const configPath = path.join(basePath, 'netlify.toml') + return fs.rm(fullPath, { force: true, recursive: true }) + }) - // Remove any config file. - await fs.rm(configPath, { force: true }) + await Promise.all(ops) } /** diff --git a/src/utils/functions/functions.mjs b/src/utils/functions/functions.mjs index e9f2f85a474..963aaa64a22 100644 --- a/src/utils/functions/functions.mjs +++ b/src/utils/functions/functions.mjs @@ -4,6 +4,8 @@ import { resolve } from 'path' import { isDirectoryAsync, isFileAsync } from '../../lib/fs.cjs' import { getPathInProject } from '../../lib/settings.cjs' +export const INTERNAL_FUNCTIONS_FOLDER = 'functions-internal' + /** * retrieves the function directory out of the flags or config * @param {object} param @@ -27,7 +29,7 @@ export const getFunctionsManifestPath = async ({ base }) => { } export const getInternalFunctionsDir = async ({ base }) => { - const path = resolve(base, getPathInProject(['functions-internal'])) + const path = resolve(base, getPathInProject([INTERNAL_FUNCTIONS_FOLDER])) const isDirectory = await isDirectoryAsync(path) return isDirectory ? path : null From 90402fdc759596b7c7442b79676ce4adea386983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 2 Jan 2023 12:13:10 +0000 Subject: [PATCH 03/43] refactor: load built functions --- npm-shrinkwrap.json | 84 +++++++++++++++++++++++++++---- package.json | 2 +- src/commands/dev/dev.mjs | 31 +++--------- src/lib/functions/registry.mjs | 75 +++++++++++++++------------ src/lib/functions/server.mjs | 47 +++++++++++------ src/utils/functions/functions.mjs | 7 +++ 6 files changed, 162 insertions(+), 84 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 5fac066527e..5143380d871 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -45,6 +45,7 @@ "execa": "^5.0.0", "express": "^4.17.1", "express-logging": "^1.1.1", + "extract-zip": "^2.0.1", "find-up": "^5.0.0", "flush-write-stream": "^2.0.0", "folder-walker": "^3.2.0", @@ -99,7 +100,6 @@ "static-server": "^2.2.1", "string-similarity": "^4.0.4", "strip-ansi-control-characters": "^2.0.0", - "symlink-or-copy": "^1.3.1", "tabtab": "^3.0.2", "tempy": "^1.0.0", "terminal-link": "^2.1.1", @@ -5665,6 +5665,15 @@ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.1.tgz", "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==" }, + "node_modules/@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.18.0.tgz", @@ -12369,6 +12378,39 @@ "node": ">=0.10.0" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -21985,11 +22027,6 @@ "node": ">=0.10.0" } }, - "node_modules/symlink-or-copy": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz", - "integrity": "sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==" - }, "node_modules/synckit": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.1.tgz", @@ -28025,6 +28062,15 @@ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.1.tgz", "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==" }, + "@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "optional": true, + "requires": { + "@types/node": "*" + } + }, "@typescript-eslint/eslint-plugin": { "version": "5.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.18.0.tgz", @@ -33094,6 +33140,27 @@ } } }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + } + } + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -40325,11 +40392,6 @@ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" }, - "symlink-or-copy": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz", - "integrity": "sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==" - }, "synckit": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.1.tgz", diff --git a/package.json b/package.json index ee8e1740519..220f4d9859b 100644 --- a/package.json +++ b/package.json @@ -265,6 +265,7 @@ "execa": "^5.0.0", "express": "^4.17.1", "express-logging": "^1.1.1", + "extract-zip": "^2.0.1", "find-up": "^5.0.0", "flush-write-stream": "^2.0.0", "folder-walker": "^3.2.0", @@ -319,7 +320,6 @@ "static-server": "^2.2.1", "string-similarity": "^4.0.4", "strip-ansi-control-characters": "^2.0.0", - "symlink-or-copy": "^1.3.1", "tabtab": "^3.0.2", "tempy": "^1.0.0", "terminal-link": "^2.1.1", diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index 2fdd3d654c4..813f9d5ca94 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -10,7 +10,6 @@ import { Option } from 'commander' import execa from 'execa' import StaticServer from 'static-server' import stripAnsiCc from 'strip-ansi-control-characters' -import { sync as symlinkOrCopy } from 'symlink-or-copy' import waitPort from 'wait-port' import { INTERNAL_EDGE_FUNCTIONS_FOLDER } from '../../lib/edge-functions/consts.mjs' @@ -448,9 +447,9 @@ const dev = async (options, command) => { let { env } = cachedConfig + // If the `serve` flag is present, we override the `framework` value so that + // we start a static server and not the framework's development server. if (options.serve) { - ensureNodeModulesForPlugins({ siteRoot: site.root }) - devConfig.framework = '#static' } @@ -510,10 +509,15 @@ const dev = async (options, command) => { const { configPath: configPathOverride } = await runBuild({ cachedConfig, options, settings, site }) + // When using the `serve` flag, we want to use the production functions built + // by Netlify Build rather than building them from source. + const dist = Boolean(options.serve) + await startFunctionsServer({ api, command, config, + dist, settings, site, siteInfo, @@ -764,27 +768,6 @@ const getBuildOptions = ({ saveConfig, }) -// Any Node modules used by internal serverless functions will be installed at -// `.netlify/plugins/node_modules`, which isn't part of the resolution paths -// used by code in `.netlify/functions`. To make sure modules can resolve, we -// create a symlink for the plugin's node_modules at `.netlify/node_modules`. -const ensureNodeModulesForPlugins = ({ siteRoot }) => { - const targetPath = path.resolve(siteRoot, getPathInProject(['plugins', 'node_modules'])) - const linkPath = path.resolve(siteRoot, getPathInProject(['node_modules'])) - - try { - symlinkOrCopy(targetPath, linkPath) - } catch (symlinkError) { - if (symlinkError.code === 'EEXIST') { - return - } - - warn( - `There was an error setting up the node_modules directory at ${linkPath}, which may cause some serverless functions to not work as expected. Please ensure that you have write permissions on this directory.`, - ) - } -} - const cleanInternalDirectory = async (basePath) => { const ops = [INTERNAL_FUNCTIONS_FOLDER, INTERNAL_EDGE_FUNCTIONS_FOLDER, 'netlify.toml'].map((name) => { const fullPath = path.resolve(basePath, getPathInProject([name])) diff --git a/src/lib/functions/registry.mjs b/src/lib/functions/registry.mjs index c985a6cec15..b24792233dc 100644 --- a/src/lib/functions/registry.mjs +++ b/src/lib/functions/registry.mjs @@ -1,19 +1,21 @@ // @ts-check import { mkdir } from 'fs/promises' -import { extname, isAbsolute, join } from 'path' +import { extname, isAbsolute, join, resolve } from 'path' import { env } from 'process' +import extractZip from 'extract-zip' + import { chalk, getTerminalLink, log, NETLIFYDEVERR, NETLIFYDEVLOG, - NETLIFYDEVWARN, warn, watchDebounced, } from '../../utils/command-helpers.mjs' import { getLogMessage } from '../log.mjs' +import { getPathInProject } from '../settings.cjs' import NetlifyFunction from './netlify-function.mjs' import runtimes from './runtimes/index.mjs' @@ -123,7 +125,7 @@ export class FunctionsRegistry { return this.functions.get(name) } - registerFunction(name, funcBeforeHook) { + async registerFunction(name, funcBeforeHook) { const { runtime } = funcBeforeHook // The `onRegister` hook allows runtimes to modify the function before it's @@ -148,8 +150,7 @@ export class FunctionsRegistry { // This fixes the bug described here https://github.com/netlify/zip-it-and-ship-it/issues/637 // If the current function's file is a zip bundle, we ignore it and log a helpful message. if (extname(func.mainFile) === ZIP_EXTENSION) { - log(`${NETLIFYDEVWARN} Skipped bundled function ${chalk.yellow(name)}. Unzip the archive to load it from source.`) - return + await this.unzipFunction(func) } this.functions.set(name, func) @@ -197,34 +198,36 @@ export class FunctionsRegistry { await Promise.all(deletedFunctions.map((func) => this.unregisterFunction(func.name))) - functions.forEach(({ mainFile, name, runtime: runtimeName }) => { - const runtime = runtimes[runtimeName] - - // If there is no matching runtime, it means this function is not yet - // supported in Netlify Dev. - if (runtime === undefined) { - return - } - - // If this function has already been registered, we skip it. - if (this.functions.has(name)) { - return - } - - const func = new NetlifyFunction({ - config: this.config, - directory: directories.find((directory) => mainFile.startsWith(directory)), - mainFile, - name, - projectRoot: this.projectRoot, - runtime, - timeoutBackground: this.timeouts.backgroundFunctions, - timeoutSynchronous: this.timeouts.syncFunctions, - settings: this.settings, - }) + await Promise.all( + functions.map(async ({ mainFile, name, runtime: runtimeName }) => { + const runtime = runtimes[runtimeName] - this.registerFunction(name, func) - }) + // If there is no matching runtime, it means this function is not yet + // supported in Netlify Dev. + if (runtime === undefined) { + return + } + + // If this function has already been registered, we skip it. + if (this.functions.has(name)) { + return + } + + const func = new NetlifyFunction({ + config: this.config, + directory: directories.find((directory) => mainFile.startsWith(directory)), + mainFile, + name, + projectRoot: this.projectRoot, + runtime, + timeoutBackground: this.timeouts.backgroundFunctions, + timeoutSynchronous: this.timeouts.syncFunctions, + settings: this.settings, + }) + + await this.registerFunction(name, func) + }), + ) await Promise.all(directories.map((path) => this.setupDirectoryWatcher(path))) } @@ -261,4 +264,12 @@ export class FunctionsRegistry { await watcher.close() } } + + async unzipFunction(func) { + const targetDirectory = resolve(this.projectRoot, getPathInProject(['functions-serve', '.unzipped', func.name])) + + await extractZip(func.mainFile, { dir: targetDirectory }) + + func.mainFile = join(targetDirectory, `${func.name}.js`) + } } diff --git a/src/lib/functions/server.mjs b/src/lib/functions/server.mjs index f53d6c164d8..5659d6c592f 100644 --- a/src/lib/functions/server.mjs +++ b/src/lib/functions/server.mjs @@ -4,7 +4,7 @@ import jwtDecode from 'jwt-decode' import { NETLIFYDEVERR, NETLIFYDEVLOG, error as errorExit, log } from '../../utils/command-helpers.mjs' import { generateNetlifyGraphJWT } from '../../utils/dev.mjs' -import { CLOCKWORK_USERAGENT, getInternalFunctionsDir } from '../../utils/functions/index.mjs' +import { CLOCKWORK_USERAGENT, getFunctionsDistPath, getInternalFunctionsDir } from '../../utils/functions/index.mjs' import { handleBackgroundFunction, handleBackgroundFunctionResult } from './background.mjs' import { createFormSubmissionHandler } from './form-submissions-handler.mjs' @@ -208,29 +208,44 @@ const getFunctionsServer = async function (options) { } export const startFunctionsServer = async (options) => { - const { capabilities, config, settings, site, siteUrl, timeouts } = options + const { capabilities, config, dist, settings, site, siteUrl, timeouts } = options const internalFunctionsDir = await getInternalFunctionsDir({ base: site.root }) // The order of the function directories matters. Leftmost directories take // precedence. - const functionsDirectories = [settings.functions, internalFunctionsDir].filter(Boolean) - - if (functionsDirectories.length !== 0) { - const functionsRegistry = new FunctionsRegistry({ - capabilities, - config, - isConnected: Boolean(siteUrl), - projectRoot: site.root, - settings, - timeouts, - }) + const functionsDirectories = [] - await functionsRegistry.scan(functionsDirectories) + // If the `dist` parameter is sent, the functions server will use the built + // functions created by zip-it-and-ship-it rather than building them from + // source. + if (dist) { + const distPath = await getFunctionsDistPath({ base: site.root }) - const server = await getFunctionsServer(Object.assign(options, { functionsRegistry })) + if (distPath) { + functionsDirectories.push(distPath) + } + } else { + functionsDirectories.push(...[settings.functions, internalFunctionsDir].filter(Boolean)) + } - await startWebServer({ server, settings }) + if (functionsDirectories.length === 0) { + return } + + const functionsRegistry = new FunctionsRegistry({ + capabilities, + config, + isConnected: Boolean(siteUrl), + projectRoot: site.root, + settings, + timeouts, + }) + + await functionsRegistry.scan(functionsDirectories) + + const server = await getFunctionsServer(Object.assign(options, { functionsRegistry })) + + await startWebServer({ server, settings }) } const startWebServer = async ({ server, settings }) => { diff --git a/src/utils/functions/functions.mjs b/src/utils/functions/functions.mjs index 963aaa64a22..bb6d1942f03 100644 --- a/src/utils/functions/functions.mjs +++ b/src/utils/functions/functions.mjs @@ -28,6 +28,13 @@ export const getFunctionsManifestPath = async ({ base }) => { return isFile ? path : null } +export const getFunctionsDistPath = async ({ base }) => { + const path = resolve(base, getPathInProject(['functions'])) + const isDirectory = await isDirectoryAsync(path) + + return isDirectory ? path : null +} + export const getInternalFunctionsDir = async ({ base }) => { const path = resolve(base, getPathInProject([INTERNAL_FUNCTIONS_FOLDER])) const isDirectory = await isDirectoryAsync(path) From d5652e75f69be02fa26b7333c4ce1a82b53cacc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 2 Jan 2023 12:24:14 +0000 Subject: [PATCH 04/43] refactor: make things nicer --- src/lib/functions/registry.mjs | 10 ++++++---- src/lib/functions/server.mjs | 5 ++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/lib/functions/registry.mjs b/src/lib/functions/registry.mjs index b24792233dc..e41969b8863 100644 --- a/src/lib/functions/registry.mjs +++ b/src/lib/functions/registry.mjs @@ -147,10 +147,12 @@ export class FunctionsRegistry { ) } - // This fixes the bug described here https://github.com/netlify/zip-it-and-ship-it/issues/637 - // If the current function's file is a zip bundle, we ignore it and log a helpful message. + // If the function file is a ZIP, we extract it and rewire its main file to + // the new location. if (extname(func.mainFile) === ZIP_EXTENSION) { - await this.unzipFunction(func) + const unzippedDirectory = await this.unzipFunction(func) + + func.mainFile = join(unzippedDirectory, `${func.name}.js`) } this.functions.set(name, func) @@ -270,6 +272,6 @@ export class FunctionsRegistry { await extractZip(func.mainFile, { dir: targetDirectory }) - func.mainFile = join(targetDirectory, `${func.name}.js`) + return targetDirectory } } diff --git a/src/lib/functions/server.mjs b/src/lib/functions/server.mjs index 5659d6c592f..155640a3a0f 100644 --- a/src/lib/functions/server.mjs +++ b/src/lib/functions/server.mjs @@ -210,9 +210,6 @@ const getFunctionsServer = async function (options) { export const startFunctionsServer = async (options) => { const { capabilities, config, dist, settings, site, siteUrl, timeouts } = options const internalFunctionsDir = await getInternalFunctionsDir({ base: site.root }) - - // The order of the function directories matters. Leftmost directories take - // precedence. const functionsDirectories = [] // If the `dist` parameter is sent, the functions server will use the built @@ -225,6 +222,8 @@ export const startFunctionsServer = async (options) => { functionsDirectories.push(distPath) } } else { + // The order of the function directories matters. Leftmost directories take + // precedence. functionsDirectories.push(...[settings.functions, internalFunctionsDir].filter(Boolean)) } From ac6a8458b752c65f57131ce12b3da9b0c22b3bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 2 Jan 2023 12:29:07 +0000 Subject: [PATCH 05/43] refactor: add const --- src/lib/functions/registry.mjs | 6 +++++- src/lib/functions/runtimes/js/builders/zisi.mjs | 3 ++- src/lib/functions/runtimes/rust/index.mjs | 3 ++- src/utils/functions/functions.mjs | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/lib/functions/registry.mjs b/src/lib/functions/registry.mjs index e41969b8863..e4b75d13ba7 100644 --- a/src/lib/functions/registry.mjs +++ b/src/lib/functions/registry.mjs @@ -14,6 +14,7 @@ import { warn, watchDebounced, } from '../../utils/command-helpers.mjs' +import { SERVE_FUNCTIONS_FOLDER } from '../../utils/functions/functions.mjs' import { getLogMessage } from '../log.mjs' import { getPathInProject } from '../settings.cjs' @@ -268,7 +269,10 @@ export class FunctionsRegistry { } async unzipFunction(func) { - const targetDirectory = resolve(this.projectRoot, getPathInProject(['functions-serve', '.unzipped', func.name])) + const targetDirectory = resolve( + this.projectRoot, + getPathInProject([SERVE_FUNCTIONS_FOLDER, '.unzipped', func.name]), + ) await extractZip(func.mainFile, { dir: targetDirectory }) diff --git a/src/lib/functions/runtimes/js/builders/zisi.mjs b/src/lib/functions/runtimes/js/builders/zisi.mjs index 39b46307672..5c70f1fdbdd 100644 --- a/src/lib/functions/runtimes/js/builders/zisi.mjs +++ b/src/lib/functions/runtimes/js/builders/zisi.mjs @@ -7,6 +7,7 @@ import readPkgUp from 'read-pkg-up' import sourceMapSupport from 'source-map-support' import { NETLIFYDEVERR } from '../../../../../utils/command-helpers.mjs' +import { SERVE_FUNCTIONS_FOLDER } from '../../../../../utils/functions/functions.mjs' import { getPathInProject } from '../../../../settings.cjs' import { normalizeFunctionsConfig } from '../../../config.mjs' import { memoizedBuild } from '../../../memoized-build.mjs' @@ -94,7 +95,7 @@ const clearFunctionsCache = (functionsPath) => { } const getTargetDirectory = async ({ errorExit }) => { - const targetDirectory = path.resolve(getPathInProject(['functions-serve'])) + const targetDirectory = path.resolve(getPathInProject([SERVE_FUNCTIONS_FOLDER])) try { await mkdir(targetDirectory, { recursive: true }) diff --git a/src/lib/functions/runtimes/rust/index.mjs b/src/lib/functions/runtimes/rust/index.mjs index cc3c5cb40d0..e5ae6feca96 100644 --- a/src/lib/functions/runtimes/rust/index.mjs +++ b/src/lib/functions/runtimes/rust/index.mjs @@ -7,6 +7,7 @@ import findUp from 'find-up' import toml from 'toml' import execa from '../../../../utils/execa.mjs' +import { SERVE_FUNCTIONS_FOLDER } from '../../../../utils/functions/functions.mjs' import { getPathInProject } from '../../../settings.cjs' import { runFunctionsProxy } from '../../local-proxy.mjs' @@ -16,7 +17,7 @@ export const name = 'rs' const build = async ({ func }) => { const functionDirectory = dirname(func.mainFile) - const cacheDirectory = resolve(getPathInProject(['functions-serve'])) + const cacheDirectory = resolve(getPathInProject([SERVE_FUNCTIONS_FOLDER])) const targetDirectory = join(cacheDirectory, func.name) const crateName = await getCrateName(functionDirectory) const binaryName = `${crateName}${isWindows ? '.exe' : ''}` diff --git a/src/utils/functions/functions.mjs b/src/utils/functions/functions.mjs index bb6d1942f03..7b09e79b51e 100644 --- a/src/utils/functions/functions.mjs +++ b/src/utils/functions/functions.mjs @@ -5,6 +5,7 @@ import { isDirectoryAsync, isFileAsync } from '../../lib/fs.cjs' import { getPathInProject } from '../../lib/settings.cjs' export const INTERNAL_FUNCTIONS_FOLDER = 'functions-internal' +export const SERVE_FUNCTIONS_FOLDER = 'functions-serve' /** * retrieves the function directory out of the flags or config From 134bf3f175fcaaf1b9fe16e572c890f46be2b717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 3 Jan 2023 10:15:37 +0000 Subject: [PATCH 06/43] feat: show debug message --- src/commands/dev/dev.mjs | 1 + src/commands/functions/functions-serve.mjs | 1 + src/lib/functions/registry.mjs | 7 ++++++- src/lib/functions/server.mjs | 3 ++- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index 813f9d5ca94..1d2664c66e8 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -517,6 +517,7 @@ const dev = async (options, command) => { api, command, config, + debug: options.debug, dist, settings, site, diff --git a/src/commands/functions/functions-serve.mjs b/src/commands/functions/functions-serve.mjs index f5a0abde131..a349d5f4b84 100644 --- a/src/commands/functions/functions-serve.mjs +++ b/src/commands/functions/functions-serve.mjs @@ -34,6 +34,7 @@ const functionsServe = async (options, command) => { await startFunctionsServer({ config, + debug: options.debug, api, settings: { functions: functionsDir, functionsPort }, site, diff --git a/src/lib/functions/registry.mjs b/src/lib/functions/registry.mjs index e4b75d13ba7..cdc26afca4c 100644 --- a/src/lib/functions/registry.mjs +++ b/src/lib/functions/registry.mjs @@ -24,9 +24,10 @@ import runtimes from './runtimes/index.mjs' const ZIP_EXTENSION = '.zip' export class FunctionsRegistry { - constructor({ capabilities, config, isConnected = false, projectRoot, settings, timeouts }) { + constructor({ capabilities, config, debug = false, isConnected = false, projectRoot, settings, timeouts }) { this.capabilities = capabilities this.config = config + this.debug = debug this.isConnected = isConnected this.projectRoot = projectRoot this.timeouts = timeouts @@ -153,6 +154,10 @@ export class FunctionsRegistry { if (extname(func.mainFile) === ZIP_EXTENSION) { const unzippedDirectory = await this.unzipFunction(func) + if (this.debug) { + log(`${NETLIFYDEVLOG} ${chalk.green('Extracted')} function ${chalk.yellow(name)} from ${func.mainFile}.`) + } + func.mainFile = join(unzippedDirectory, `${func.name}.js`) } diff --git a/src/lib/functions/server.mjs b/src/lib/functions/server.mjs index 155640a3a0f..ef605daf8fe 100644 --- a/src/lib/functions/server.mjs +++ b/src/lib/functions/server.mjs @@ -208,7 +208,7 @@ const getFunctionsServer = async function (options) { } export const startFunctionsServer = async (options) => { - const { capabilities, config, dist, settings, site, siteUrl, timeouts } = options + const { capabilities, config, debug, dist, settings, site, siteUrl, timeouts } = options const internalFunctionsDir = await getInternalFunctionsDir({ base: site.root }) const functionsDirectories = [] @@ -234,6 +234,7 @@ export const startFunctionsServer = async (options) => { const functionsRegistry = new FunctionsRegistry({ capabilities, config, + debug, isConnected: Boolean(siteUrl), projectRoot: site.root, settings, From d782283cd00af0cb6b0a5cec751ff1c4a901d349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 3 Jan 2023 13:35:27 +0000 Subject: [PATCH 07/43] refactor: rename variable --- src/commands/dev/dev.mjs | 4 ++-- src/lib/functions/server.mjs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index 1d2664c66e8..908329c41a3 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -511,14 +511,14 @@ const dev = async (options, command) => { // When using the `serve` flag, we want to use the production functions built // by Netlify Build rather than building them from source. - const dist = Boolean(options.serve) + const loadDistFunctions = Boolean(options.serve) await startFunctionsServer({ api, command, config, debug: options.debug, - dist, + loadDistFunctions, settings, site, siteInfo, diff --git a/src/lib/functions/server.mjs b/src/lib/functions/server.mjs index ef605daf8fe..dfffe37e989 100644 --- a/src/lib/functions/server.mjs +++ b/src/lib/functions/server.mjs @@ -208,14 +208,14 @@ const getFunctionsServer = async function (options) { } export const startFunctionsServer = async (options) => { - const { capabilities, config, debug, dist, settings, site, siteUrl, timeouts } = options + const { capabilities, config, debug, loadDistFunctions, settings, site, siteUrl, timeouts } = options const internalFunctionsDir = await getInternalFunctionsDir({ base: site.root }) const functionsDirectories = [] - // If the `dist` parameter is sent, the functions server will use the built - // functions created by zip-it-and-ship-it rather than building them from - // source. - if (dist) { + // If the `loadDistFunctions` parameter is sent, the functions server will + // use the built functions created by zip-it-and-ship-it rather than building + // them from source. + if (loadDistFunctions) { const distPath = await getFunctionsDistPath({ base: site.root }) if (distPath) { From 5411c3c2d39bfddc63cd2f32540d69de40480e81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 3 Jan 2023 14:17:15 +0000 Subject: [PATCH 08/43] chore: add `extract-zip` dependency --- npm-shrinkwrap.json | 73 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 74 insertions(+) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index bb7528af171..8fae414e033 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -46,6 +46,7 @@ "execa": "^5.0.0", "express": "^4.17.1", "express-logging": "^1.1.1", + "extract-zip": "^2.0.1", "fastify": "^4.10.2", "find-up": "^5.0.0", "flush-write-stream": "^2.0.0", @@ -5613,6 +5614,15 @@ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.1.tgz", "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==" }, + "node_modules/@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.18.0.tgz", @@ -12390,6 +12400,39 @@ "node": ">=0.10.0" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -28152,6 +28195,15 @@ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.1.tgz", "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==" }, + "@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "optional": true, + "requires": { + "@types/node": "*" + } + }, "@typescript-eslint/eslint-plugin": { "version": "5.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.18.0.tgz", @@ -33278,6 +33330,27 @@ } } }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + } + } + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", diff --git a/package.json b/package.json index c91c9be3da1..f35fe990d00 100644 --- a/package.json +++ b/package.json @@ -266,6 +266,7 @@ "execa": "^5.0.0", "express": "^4.17.1", "express-logging": "^1.1.1", + "extract-zip": "^2.0.1", "fastify": "^4.10.2", "find-up": "^5.0.0", "flush-write-stream": "^2.0.0", From 05e9dc393a3dcef4edf3f9291d5f362d08bb234a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 3 Jan 2023 14:48:16 +0000 Subject: [PATCH 09/43] chore: update snapshots --- .../600.framework-detection.test.cjs.md | 320 ------------------ .../600.framework-detection.test.cjs.snap | Bin 1975 -> 1655 bytes 2 files changed, 320 deletions(-) diff --git a/tests/integration/snapshots/600.framework-detection.test.cjs.md b/tests/integration/snapshots/600.framework-detection.test.cjs.md index 89ceb9a4e71..674ecf6d3c6 100644 --- a/tests/integration/snapshots/600.framework-detection.test.cjs.md +++ b/tests/integration/snapshots/600.framework-detection.test.cjs.md @@ -15,32 +15,8 @@ Generated by [AVA](https://avajs.dev). ◈ See docs at: https://cli.netlify.com/netlify-dev#project-detection␊ ◈ Running static server from "site-with-index-file"␊ ◈ Setting up local development server␊ - ​␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - > Version␊ - @netlify/build 0.0.0␊ - ​␊ - > Flags␊ - offline: true␊ - ​␊ - > Current directory␊ - /file/path␊ - ​␊ - > Config file␊ - No config file was defined: using default values.␊ - ​␊ - > Context␊ - dev␊ - ​␊ - 1. Run command for local development ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ ␊ ◈ Static server listening to 88888␊ - ​␊ - (dev.command completed in Xms)␊ ␊ ┌──────────────────────────────────────────────────┐␊ │ │␊ @@ -56,32 +32,8 @@ Generated by [AVA](https://avajs.dev). ◈ Using simple static server because '--dir' flag was specified␊ ◈ Running static server from "site-with-index-file/public"␊ ◈ Setting up local development server␊ - ​␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - > Version␊ - @netlify/build 0.0.0␊ - ​␊ - > Flags␊ - offline: true␊ - ​␊ - > Current directory␊ - /file/path␊ - ​␊ - > Config file␊ - No config file was defined: using default values.␊ - ​␊ - > Context␊ - dev␊ - ​␊ - 1. Run command for local development ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ ␊ ◈ Static server listening to 88888␊ - ​␊ - (dev.command completed in Xms)␊ ␊ ┌──────────────────────────────────────────────────┐␊ │ │␊ @@ -100,32 +52,8 @@ Generated by [AVA](https://avajs.dev). ◈ See docs at: https://cli.netlify.com/netlify-dev#project-detection␊ ◈ Running static server from "site-with-index-file"␊ ◈ Setting up local development server␊ - ​␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - > Version␊ - @netlify/build 0.0.0␊ - ​␊ - > Flags␊ - offline: true␊ - ​␊ - > Current directory␊ - /file/path␊ - ​␊ - > Config file␊ - /file/path␊ - ​␊ - > Context␊ - dev␊ - ​␊ - 1. Run command for local development ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ ␊ ◈ Static server listening to 88888␊ - ​␊ - (dev.command completed in Xms)␊ ␊ ┌──────────────────────────────────────────────────┐␊ │ │␊ @@ -141,32 +69,8 @@ Generated by [AVA](https://avajs.dev). ◈ Using simple static server because '--dir' flag was specified␊ ◈ Running static server from "site-with-index-file/public"␊ ◈ Setting up local development server␊ - ​␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - > Version␊ - @netlify/build 0.0.0␊ - ​␊ - > Flags␊ - offline: true␊ - ​␊ - > Current directory␊ - /file/path␊ - ​␊ - > Config file␊ - No config file was defined: using default values.␊ - ​␊ - > Context␊ - dev␊ - ​␊ - 1. Run command for local development ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ ␊ ◈ Static server listening to 88888␊ - ​␊ - (dev.command completed in Xms)␊ ␊ ┌──────────────────────────────────────────────────┐␊ │ │␊ @@ -184,32 +88,8 @@ Generated by [AVA](https://avajs.dev). ◈ Use --staticServerPort or [dev.staticServerPort] to configure the static server port␊ ◈ Running static server from "site-with-index-file/public"␊ ◈ Setting up local development server␊ - ​␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - > Version␊ - @netlify/build 0.0.0␊ - ​␊ - > Flags␊ - offline: true␊ - ​␊ - > Current directory␊ - /file/path␊ - ​␊ - > Config file␊ - No config file was defined: using default values.␊ - ​␊ - > Context␊ - dev␊ - ​␊ - 1. Run command for local development ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ ␊ ◈ Static server listening to 88888␊ - ​␊ - (dev.command completed in Xms)␊ ␊ ┌──────────────────────────────────────────────────┐␊ │ │␊ @@ -223,28 +103,6 @@ Generated by [AVA](https://avajs.dev). `◈ Netlify Dev ◈␊ ◈ Setting up local development server␊ - ​␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - > Version␊ - @netlify/build 0.0.0␊ - ​␊ - > Flags␊ - offline: true␊ - ​␊ - > Current directory␊ - /file/path␊ - ​␊ - > Config file␊ - /file/path␊ - ​␊ - > Context␊ - dev␊ - ​␊ - 1. Run command for local development ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ ◈ Starting Netlify Dev with custom config␊ hello␊ ◈ "echo hello" exited with code *. Shutting down Netlify Dev server` @@ -255,28 +113,6 @@ Generated by [AVA](https://avajs.dev). `◈ Netlify Dev ◈␊ ◈ Setting up local development server␊ - ​␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - > Version␊ - @netlify/build 0.0.0␊ - ​␊ - > Flags␊ - offline: true␊ - ​␊ - > Current directory␊ - /file/path␊ - ​␊ - > Config file␊ - /file/path␊ - ​␊ - > Context␊ - dev␊ - ​␊ - 1. Run command for local development ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ ◈ Starting Netlify Dev with Create React App␊ ◈ Failed running command: react-scripts start. Please verify 'react-scripts' exists` @@ -293,28 +129,6 @@ Generated by [AVA](https://avajs.dev). `◈ Netlify Dev ◈␊ ◈ Setting up local development server␊ - ​␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - > Version␊ - @netlify/build 0.0.0␊ - ​␊ - > Flags␊ - offline: true␊ - ​␊ - > Current directory␊ - /file/path␊ - ​␊ - > Config file␊ - No config file was defined: using default values.␊ - ​␊ - > Context␊ - dev␊ - ​␊ - 1. Run command for local development ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ ◈ Starting Netlify Dev with Create React App␊ ␊ > start␊ @@ -342,28 +156,6 @@ Generated by [AVA](https://avajs.dev). `◈ Netlify Dev ◈␊ ◈ Setting up local development server␊ - ​␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - > Version␊ - @netlify/build 0.0.0␊ - ​␊ - > Flags␊ - offline: true␊ - ​␊ - > Current directory␊ - /file/path␊ - ​␊ - > Config file␊ - /file/path␊ - ​␊ - > Context␊ - dev␊ - ​␊ - 1. Run command for local development ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ ◈ Starting Netlify Dev with #custom␊ hello␊ ◈ "echo hello" exited with code *. Shutting down Netlify Dev server` @@ -377,28 +169,6 @@ Generated by [AVA](https://avajs.dev). ◈ Setup a netlify.toml file with a [dev] section to specify your dev server settings.␊ ◈ See docs at: https://cli.netlify.com/netlify-dev#project-detection␊ ◈ Setting up local development server␊ - ​␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - > Version␊ - @netlify/build 0.0.0␊ - ​␊ - > Flags␊ - offline: true␊ - ​␊ - > Current directory␊ - /file/path␊ - ​␊ - > Config file␊ - No config file was defined: using default values.␊ - ​␊ - > Context␊ - dev␊ - ​␊ - 1. Run command for local development ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ ◈ Starting Netlify Dev with #custom␊ ◈ Failed running command: oops-i-did-it-again forgot-to-use-a-valid-command. Please verify 'oops-i-did-it-again' exists` @@ -414,28 +184,6 @@ Generated by [AVA](https://avajs.dev). > [Create React App] 'npm run start' ? Multiple possible start commands found Create React App-npm run start␊ Add 'framework = "create-react-app"' to the [dev] section of your netlify.toml to avoid this selection prompt next time␊ ◈ Setting up local development server␊ - ​␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - > Version␊ - @netlify/build 0.0.0␊ - ​␊ - > Flags␊ - offline: true␊ - ​␊ - > Current directory␊ - /file/path␊ - ​␊ - > Config file␊ - No config file was defined: using default values.␊ - ​␊ - > Context␊ - dev␊ - ​␊ - 1. Run command for local development ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ ◈ Starting Netlify Dev with Create React App␊ ␊ > start␊ @@ -452,28 +200,6 @@ Generated by [AVA](https://avajs.dev). ◈ Setup a netlify.toml file with a [dev] section to specify your dev server settings.␊ ◈ See docs at: https://cli.netlify.com/netlify-dev#project-detection␊ ◈ Setting up local development server␊ - ​␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - > Version␊ - @netlify/build 0.0.0␊ - ​␊ - > Flags␊ - offline: true␊ - ​␊ - > Current directory␊ - /file/path␊ - ​␊ - > Config file␊ - No config file was defined: using default values.␊ - ​␊ - > Context␊ - dev␊ - ​␊ - 1. Run command for local development ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ ◈ Starting Netlify Dev with custom config␊ hello␊ ◈ "echo hello" exited with code *. Shutting down Netlify Dev server` @@ -489,32 +215,8 @@ Generated by [AVA](https://avajs.dev). ◈ See docs at: https://cli.netlify.com/netlify-dev#project-detection␊ ◈ Running static server from "site-with-gulp"␊ ◈ Setting up local development server␊ - ​␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - > Version␊ - @netlify/build 0.0.0␊ - ​␊ - > Flags␊ - offline: true␊ - ​␊ - > Current directory␊ - /file/path␊ - ​␊ - > Config file␊ - No config file was defined: using default values.␊ - ​␊ - > Context␊ - dev␊ - ​␊ - 1. Run command for local development ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ ␊ ◈ Static server listening to 88888␊ - ​␊ - (dev.command completed in Xms)␊ ␊ ┌──────────────────────────────────────────────────┐␊ │ │␊ @@ -528,28 +230,6 @@ Generated by [AVA](https://avajs.dev). `◈ Netlify Dev ◈␊ ◈ Setting up local development server␊ - ​␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - > Version␊ - @netlify/build 0.0.0␊ - ​␊ - > Flags␊ - offline: true␊ - ​␊ - > Current directory␊ - /file/path␊ - ​␊ - > Config file␊ - No config file was defined: using default values.␊ - ​␊ - > Context␊ - dev␊ - ​␊ - 1. Run command for local development ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ ◈ Starting Netlify Dev with Nuxt 3␊ ␊ > dev␊ diff --git a/tests/integration/snapshots/600.framework-detection.test.cjs.snap b/tests/integration/snapshots/600.framework-detection.test.cjs.snap index 8a3083ebf9d0373b8e9f4e3e0a73e5f67570ef54..15d926fb618c28a6194d76c9c4124a281499efca 100644 GIT binary patch literal 1655 zcmV--28j7VRzV(4PU?6AO*2zui*X8BUQcmUBGMQ|;VnBJ z5|&a0i4?6uYLrV!xrUCA+vYlE61581wW4oOoo7IRJ8NrNv{EYsI!rh5#%4_0TPQrz zOmM3wqmfxxSL9|1?P4{GYK`($&C8__i%0>{E6~(BQ!7hLQOatCqDZus{Qn_JIf_od zgGxf@6x3zKT+cbTt{L=+T^>{w(=;?zhK$E_Cp1=9i$xhz09S^aO;TefO-0r+e(-*E z15094rZlzcaGB@Kqf!BY-Mb&}-hJ<}e178W`v4yJfs^`kozJWfxafeSB<=$4w%Iq_ zHtWNtPOWD+?{CIP6^h!`URt1@spJ4AUfG8*p-ElZ9>W}umY=HNi{;&jj?lC-GFMFumN zGfdbVb>qe));~(v`DJ2a-3xY1Cad+-Zv_rO&Vs<35X|!JEZ@$k;}KEEx~Gm*U`RA6 zV9d5m`OOk!lhLp=w{H6}w+)>{f+wtz%UK$pVd+W7(yGT&he$pZK$^$~)z^g7_r=h{ zhb0WP|5ZAkj-mAi7t#m``t}7y&O^yOqJBwate%>;gJPBLZnz*ZazIF%W^2PYwhVZB z4zLtdHa^2aGl*C~lKtY(=V{whHVvHNV{@3(cuAOS0Q z6-kz9Ww=P{8eB^$LE1A1l_)qUjR}k=l~#psPJ7?zrbJ-5{tVsZXq6+xRGt~mL*+eh zje2K$&^z;6lbo~8+azTJlAx*ua%93d)7=nP6xL~1;M*Nlt-(5a%vo2bh<1D>D{!K) z{JGw-t!ZD-+HG#aV2YK0mGIDYW166+OEL5J*n@V)YF==dGCR)|ITM!gUz;8kTSgURh=bM3XR%ZZIw$Tk}H+YsS!rz|e=@gPkmTFqUe2PSWFAD~=26Bp)JUDEEg_5PUuReZ|B5 zzNe9=+3e_Qc64=cM^|TyQj93orvq~sAb|3=36p*vQF*=iP1a~bGgQGJVnef$V-`Pc zaxG59zub^unDu@xhIZFBu3o(K_8XV3-&nu8QK>ASaWpzmAMKAv{{dbCNWr!y003mN B9$Wwb literal 1975 zcmV;o2T1rqRzV2YAgPA=J z@|NTt(jSWm00000000B+T+NRZMHB}YFxi{uJ@~R58VEh>0t&7HU6v0wL?9ptZZ=ZW zT{BZ`Pgh%2y|a^Of}Tw9pb3c{#dtH(7;h#fo=p4$Og!_K`0As3rguJe_*__OsLe3b z)vsQ?dhhppud8`NDP(y)!CnoX#3%|c znf}mV36;PrBJ@CIk^;K9!Vf-0a{9Ba$#;RN?5HJDB|ro_x}*3 z>_?;D#z8`B6dcQlxss=;F-^Zs%=BPZGDSnJWysQ)ZiHIv*Ig)+@dD(=Z;OCj zCrM7VR~1qe-B2h2*VHZId`q9kEbQ4>8FwI}q;^gMu8 zsTH=ucZ3|oAYdunXv>4sXis=>@9S-!)rj_u)$hxPj#YhA?_P@%#!M;ifS@Gq0^*xS zDg-LyJ}oZQg0b7?yB~Grd)sZjQSJMJ4UafB%;hE?f=-L35QZ4x&_E;rCLtqI(s+;# z_x@L*!+-XUqQlX&2Xz_|twdmk4D6U1B`p0g8b1<0%vsv>H1SZ))5K0$L{AeFgSDrL zfmYJQ+v91XAxN9*dPB1W)&<(L_S=3#il<*IZ1Ka$;%O4L(A7$<9DX&hlwmapoC<-j zseDcK^xC$O<*=)s1cxk+C&7uS$dlkeD@kzKkzm5nYg)97Db{eQX%%4grCYAgElrFz zvCV}qp|*bQ4B+u4)b=mLV=tm-kBKqdv&TRy*<;bMM~8@XEM+Jnnp9opLT$+!`#KfY zXgyXbHJ&w=n<*DsNYK+SDC!lIoJQ15i3}a5=JcQthW3JW5-kT1R!kwc`NFD>uhIa9 zbgRl|*vXUHGi#pIcFH1pQk$4sJgE(|lGNUJq$V()dbN&uCRpdH)%V>1wGlzba@!QE zaNEaIqqxnZ_k_(qsCtf@v4R6-Vf?w@*UfX}D3=QJ85=ayEt(|U+%Zd|76w%u&s7=irEwXG#21cq513O!A!k*h1#yA8m>lb zd2{N|3fBDMvF3%v8eiaTFD9WS4cOrO5D2p6;!fh-HkE*btx404 z6BWo}jb6x~-0yOpdaiogEfU&HYL^(qtm;A91+EU7%v$^_3ST8&Cmp$PhE zjy@b)Bn?W6`18g`DE92S#GJ}BP&VBhjM_cFSEBZ( zJtMawUfOe59hf|&D6hAfO(95NM>CSj(aZE_W2shLX}lF$^Pp}S?E zTpl7}RMCua+s>55y*92g)7zM3W@EFb;+Ob_oZ$_Y&a50ger9In_?t`k=`{Xam|2-$tiGS0SZ_V<7_Ck{q*lk~pNLkQ#|$eY z;^gPG1?HEg7vs1uC5DqQ8<|@}TNaTln+^0D=)LGFo$!XGkqY}@PqNOLt_4hTow&}< z(^3QtP^?WoL3o1j1o2!yrnulpBIU~5-UEp;&C`YOBUZh8?pDa-GoL^E{Mo~!hsSN* z5I$UN2(v_)<8);vY{aG9#JpRfA;%N{s9@U9{?BAQ+h6Hw=1JyHIC|)ODWTJ!aNO$U zPdJWl Date: Tue, 3 Jan 2023 14:49:44 +0000 Subject: [PATCH 10/43] refactor: conditionally set `quiet` flag --- src/commands/dev/dev.mjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index 6f6844dd54c..59da5fc105a 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -746,10 +746,12 @@ const runBuild = async ({ cachedConfig, options, settings, site }) => { return { configPath: tempConfigPath } } - // Enable `quiet` to suppress non-essential output from Netlify Build. const startDevOptions = { ...sharedOptions, - quiet: true, + + // Set `quiet` to suppress non-essential output from Netlify Build unless + // the `debug` flag is set. + quiet: !options.debug, } // Run Netlify Build using the `startDev` entry point. From 1a890502d74a7510e8c737ac4d5a92d98f3eda1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 3 Jan 2023 14:55:22 +0000 Subject: [PATCH 11/43] refactor: simplify code --- src/lib/functions/server.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/functions/server.mjs b/src/lib/functions/server.mjs index dfffe37e989..49d497f9717 100644 --- a/src/lib/functions/server.mjs +++ b/src/lib/functions/server.mjs @@ -224,7 +224,9 @@ export const startFunctionsServer = async (options) => { } else { // The order of the function directories matters. Leftmost directories take // precedence. - functionsDirectories.push(...[settings.functions, internalFunctionsDir].filter(Boolean)) + const sourceDirectories = [settings.functions, internalFunctionsDir].filter(Boolean) + + functionsDirectories.push(...sourceDirectories) } if (functionsDirectories.length === 0) { From 2d8d2d0c0e39a12b64d64aae62c67f355ebfeba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 3 Jan 2023 14:55:27 +0000 Subject: [PATCH 12/43] chore: update docs --- docs/commands/dev.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/commands/dev.md b/docs/commands/dev.md index 637361953c4..e62d6190420 100644 --- a/docs/commands/dev.md +++ b/docs/commands/dev.md @@ -30,6 +30,7 @@ netlify dev - `live` (*boolean*) - start a public live session - `offline` (*boolean*) - disables any features that require network access - `port` (*string*) - port of netlify dev +- `serve` (*boolean*) - run in "serve" mode - `sessionId` (*string*) - (Graph) connect to cloud session with ID [sessionId] - `targetPort` (*string*) - port of target app server - `debug` (*boolean*) - Print debugging information From 78c35b68bf54712bc6230c0c7d874f5a9ac5c780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 3 Jan 2023 15:59:30 +0000 Subject: [PATCH 13/43] refactor: rename flag to `prod` --- docs/commands/dev.md | 2 +- src/commands/dev/dev.mjs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/commands/dev.md b/docs/commands/dev.md index e62d6190420..6f8782c19fc 100644 --- a/docs/commands/dev.md +++ b/docs/commands/dev.md @@ -30,7 +30,7 @@ netlify dev - `live` (*boolean*) - start a public live session - `offline` (*boolean*) - disables any features that require network access - `port` (*string*) - port of netlify dev -- `serve` (*boolean*) - run in "serve" mode +- `prod` (*boolean*) - build the site for production - `sessionId` (*string*) - (Graph) connect to cloud session with ID [sessionId] - `targetPort` (*string*) - port of target app server - `debug` (*boolean*) - Print debugging information diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index 59da5fc105a..a6422dcc75a 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -460,9 +460,9 @@ const dev = async (options, command) => { let { env } = cachedConfig - // If the `serve` flag is present, we override the `framework` value so that + // If the `prod` flag is present, we override the `framework` value so that // we start a static server and not the framework's development server. - if (options.serve) { + if (options.prod) { devConfig.framework = '#static' } @@ -524,7 +524,7 @@ const dev = async (options, command) => { // When using the `serve` flag, we want to use the production functions built // by Netlify Build rather than building them from source. - const loadDistFunctions = Boolean(options.serve) + const loadDistFunctions = Boolean(options.prod) await startFunctionsServer({ api, @@ -719,7 +719,7 @@ const runBuild = async ({ cachedConfig, options, settings, site }) => { settings.frameworkHost = ipVersion === 6 ? '::1' : '127.0.0.1' } - if (options.serve) { + if (options.prod) { // Start by cleaning the internal directory, as it may have artifacts left // by previous builds. await cleanInternalDirectory(site.root) @@ -823,7 +823,7 @@ export const createDevCommand = (program) => { .option('-o ,--offline', 'disables any features that require network access') .option('-l, --live', 'start a public live session', false) .option('--functionsPort ', 'port of functions server', (value) => Number.parseInt(value)) - .option('-s, --serve', 'run in "serve" mode', false) + .option('-P, --prod', 'build the site for production', false) .addOption( new Option( '--geo ', From 0bcc9a26965ca1c84525f17ad429c5607e0063dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 3 Jan 2023 16:02:41 +0000 Subject: [PATCH 14/43] refactor: update message --- src/commands/dev/dev.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index a6422dcc75a..a1511544679 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -518,7 +518,11 @@ const dev = async (options, command) => { startPollingForAPIAuthentication({ api, command, config, site, siteInfo }) } - log(`${NETLIFYDEVWARN} Setting up local development server`) + if (options.prod) { + log(`${NETLIFYDEVWARN} Building site for production`) + } else { + log(`${NETLIFYDEVWARN} Setting up local development server`) + } const { configPath: configPathOverride } = await runBuild({ cachedConfig, options, settings, site }) From 40900139baf949b843d0184b2a47cd12863cfcd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 3 Jan 2023 17:07:07 +0000 Subject: [PATCH 15/43] feat: do not set `NETLIFY_DEV` when `prod` is set --- .eslintrc.cjs | 6 ++ src/commands/dev/dev.mjs | 2 +- src/utils/dev.mjs | 6 +- .../600.framework-detection.test.cjs | 44 ++++++++++++ .../600.framework-detection.test.cjs.md | 68 ++++++++++++++++++ .../600.framework-detection.test.cjs.snap | Bin 1655 -> 2075 bytes tests/integration/utils/dev-server.cjs | 14 +++- 7 files changed, 136 insertions(+), 4 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index f6c67316b8e..b7c1e863718 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -98,5 +98,11 @@ module.exports = { ], }, }, + { + files: ['tests/integration/**/*'], + rules: { + 'require-await': 'off', + }, + }, ], } diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index a1511544679..4f2b4ed8dd7 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -471,7 +471,7 @@ const dev = async (options, command) => { log(`${NETLIFYDEVLOG} Injecting environment variable values for ${chalk.yellow('all scopes')}`) } - await injectEnvVariables({ devConfig, env, site }) + await injectEnvVariables({ devConfig, env, prod: options.prod, site }) await promptEditorHelper({ chalk, config, log, NETLIFYDEVLOG, repositoryRoot, state }) const { addonsUrls, capabilities, siteUrl, timeouts } = await getSiteInformation({ diff --git a/src/utils/dev.mjs b/src/utils/dev.mjs index 294c2347e2b..23a5b369556 100644 --- a/src/utils/dev.mjs +++ b/src/utils/dev.mjs @@ -139,7 +139,7 @@ const getEnvSourceName = (source) => { // Takes a set of environment variables in the format provided by @netlify/config, augments it with variables from both // dot-env files and the process itself, and injects into `process.env`. -export const injectEnvVariables = async ({ devConfig, env, site }) => { +export const injectEnvVariables = async ({ devConfig, env, prod, site }) => { const environment = new Map(Object.entries(env)) const dotEnvFiles = await loadDotEnvFiles({ envFiles: devConfig.envFiles, projectDir: site.root }) @@ -183,7 +183,9 @@ export const injectEnvVariables = async ({ devConfig, env, site }) => { } } - process.env.NETLIFY_DEV = 'true' + if (prod !== true) { + process.env.NETLIFY_DEV = 'true' + } } export const acquirePort = async ({ configuredPort, defaultPort, errorMessage }) => { diff --git a/tests/integration/600.framework-detection.test.cjs b/tests/integration/600.framework-detection.test.cjs index a365abd91f5..7b48707f583 100644 --- a/tests/integration/600.framework-detection.test.cjs +++ b/tests/integration/600.framework-detection.test.cjs @@ -332,3 +332,47 @@ test('should start static service for frameworks without port, detected framewor t.true(error.stdout.includes(`Failed running command: remix watch. Please verify 'remix' exists`)) }) }) + +test('should run and serve a production build when the `--prod` flag is set', async (t) => { + await withSiteBuilder('site-with-framework', async (builder) => { + await builder + .withNetlifyToml({ + config: { + build: { publish: 'public' }, + functions: { directory: 'functions' }, + plugins: [{ package: './plugins/frameworker' }], + }, + }) + .withBuildPlugin({ + name: 'frameworker', + plugin: { + onPreBuild: async ({ netlifyConfig }) => { + // eslint-disable-next-line n/global-require + const { mkdir, writeFile } = require('fs').promises + + const generatedFunctionsDir = 'new_functions' + netlifyConfig.functions.directory = generatedFunctionsDir + + netlifyConfig.redirects.push({ + from: '/hello', + to: '/.netlify/functions/hello', + }) + + await mkdir(generatedFunctionsDir) + await writeFile( + `${generatedFunctionsDir}/hello.js`, + `exports.handler = async () => ({ statusCode: 200, body: 'Hello! NETLIFY_DEV is ' + process.env.NETLIFY_DEV })`, + ) + }, + }, + }) + .buildAsync() + + await withDevServer({ cwd: builder.directory, debug: true, prod: true }, async ({ output, url }) => { + const response = await got(`${url}/hello`).text() + t.is(response, 'Hello! NETLIFY_DEV is undefined') + + t.snapshot(normalize(output, { duration: true, filePath: true })) + }) + }) +}) diff --git a/tests/integration/snapshots/600.framework-detection.test.cjs.md b/tests/integration/snapshots/600.framework-detection.test.cjs.md index 674ecf6d3c6..550d5cc7747 100644 --- a/tests/integration/snapshots/600.framework-detection.test.cjs.md +++ b/tests/integration/snapshots/600.framework-detection.test.cjs.md @@ -237,3 +237,71 @@ Generated by [AVA](https://avajs.dev). ␊ 14␊ ◈ "npm run dev" exited with code *. Shutting down Netlify Dev server` + +## should run and serve a production build when the `--prod` flag is set + +> Snapshot 1 + + `◈ Netlify Dev ◈␊ + ◈ Using simple static server because '[dev.framework]' was set to '#static'␊ + ◈ Running static server from "site-with-framework/public"␊ + ◈ Building site for production␊ + ​␊ + Netlify Build ␊ + ────────────────────────────────────────────────────────────────␊ + ​␊ + > Version␊ + @netlify/build 0.0.0␊ + ​␊ + > Flags␊ + offline: true␊ + outputConfigPath:/file/path␊ + ​␊ + > Current directory␊ + /file/path␊ + ​␊ + > Config file␊ + /file/path␊ + ​␊ + > Context␊ + dev␊ + ​␊ + > Loading plugins␊ + -/file/path from netlify.toml␊ + ​␊ + 1./file/path (onPreBuild event) ␊ + ────────────────────────────────────────────────────────────────␊ + ​␊ + Netlify configuration property "redirects" value changed to [ { from: /file/path', to: /file/path' } ].␊ + ​␊ + /file/path onPreBuild completed in Xms)␊ + ​␊ + 2. Functions bundling ␊ + ────────────────────────────────────────────────────────────────␊ + ​␊ + Packaging Functions from new_functions directory:␊ + - hello.js␊ + ​␊ + ​␊ + (Functions bundling completed in Xms)␊ + ​␊ + 3. Save deploy artifacts ␊ + ────────────────────────────────────────────────────────────────␊ + ​␊ + ​␊ + (Save deploy artifacts completed in Xms)␊ + ​␊ + Netlify Build Complete ␊ + ────────────────────────────────────────────────────────────────␊ + ​␊ + (Netlify Build completed in Xms)␊ + ␊ + ◈ Static server listening to 88888␊ + ◈ Loaded function hello http://localhost:88888/.netlify/functions/hello.␊ + ◈ Functions server is listening on 88888␊ + ␊ + ┌──────────────────────────────────────────────────┐␊ + │ │␊ + │ ◈ Server now ready on http://localhost:88888 │␊ + │ │␊ + └──────────────────────────────────────────────────┘` diff --git a/tests/integration/snapshots/600.framework-detection.test.cjs.snap b/tests/integration/snapshots/600.framework-detection.test.cjs.snap index 15d926fb618c28a6194d76c9c4124a281499efca..4613081fb5ec0c7e9e42706b6dff75c947c1a2f0 100644 GIT binary patch literal 2075 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 B9$Wwb diff --git a/tests/integration/utils/dev-server.cjs b/tests/integration/utils/dev-server.cjs index 743dda887d9..4802afe4e5c 100644 --- a/tests/integration/utils/dev-server.cjs +++ b/tests/integration/utils/dev-server.cjs @@ -29,6 +29,7 @@ const startServer = async ({ env = {}, args = [], expectFailure = false, + prod = false, prompt, }) => { const port = await getPort() @@ -40,7 +41,18 @@ const startServer = async ({ const ps = execa( cliPath, - ['dev', offline ? '--offline' : '', '-p', port, '--staticServerPort', staticPort, '--context', context, ...args], + [ + 'dev', + offline ? '--offline' : '', + '-p', + port, + '--staticServerPort', + staticPort, + '--context', + context, + prod ? '--prod' : '', + ...args, + ], getExecaOptions({ cwd, env }), ) From 9be736bb94d78f17957c0470951d1af3addb956b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 3 Jan 2023 17:12:12 +0000 Subject: [PATCH 16/43] chore: remove lint exceptions --- .eslintrc.cjs | 2 +- tests/integration/100.command.dev.test.cjs | 3 --- tests/integration/20.command.functions.test.cjs | 3 --- tests/integration/200.command.dev.test.cjs | 2 -- tests/integration/210.command.deploy.test.cjs | 2 -- tests/integration/300.command.dev.test.cjs | 2 -- tests/integration/330.serving-functions.test.cjs | 2 -- tests/integration/400.command.dev.test.cjs | 2 -- tests/integration/500.command.dev.test.cjs | 2 -- tests/integration/eleventy-site/functions/echo.cjs | 1 - tests/unit/utils/deploy/hash-fns.test.mjs | 2 -- 11 files changed, 1 insertion(+), 22 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b7c1e863718..3c72e3fff64 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -99,7 +99,7 @@ module.exports = { }, }, { - files: ['tests/integration/**/*'], + files: ['tests/**/*'], rules: { 'require-await': 'off', }, diff --git a/tests/integration/100.command.dev.test.cjs b/tests/integration/100.command.dev.test.cjs index f1ce6a11d76..bdc83820d58 100644 --- a/tests/integration/100.command.dev.test.cjs +++ b/tests/integration/100.command.dev.test.cjs @@ -1,5 +1,4 @@ // Handlers are meant to be async outside tests -/* eslint-disable require-await */ const path = require('path') // eslint-disable-next-line ava/use-test @@ -933,5 +932,3 @@ test('should have only allowed environment variables set', async (t) => { }) }) }) - -/* eslint-enable require-await */ diff --git a/tests/integration/20.command.functions.test.cjs b/tests/integration/20.command.functions.test.cjs index 9b033939e1f..7ceeb15dc43 100644 --- a/tests/integration/20.command.functions.test.cjs +++ b/tests/integration/20.command.functions.test.cjs @@ -1,5 +1,4 @@ // Handlers are meant to be async outside tests -/* eslint-disable require-await */ // eslint-disable-next-line ava/use-test const avaTest = require('ava') const { isCI } = require('ci-info') @@ -937,5 +936,3 @@ test('should handle content-types with charset', async (t) => { }) }) }) - -/* eslint-enable require-await */ diff --git a/tests/integration/200.command.dev.test.cjs b/tests/integration/200.command.dev.test.cjs index 964c89b6f72..ee773b90a12 100644 --- a/tests/integration/200.command.dev.test.cjs +++ b/tests/integration/200.command.dev.test.cjs @@ -1,5 +1,4 @@ // Handlers are meant to be async outside tests -/* eslint-disable require-await */ const { copyFile } = require('fs').promises const os = require('os') const path = require('path') @@ -439,4 +438,3 @@ export const handler = async function () { }) }) }) -/* eslint-enable require-await */ diff --git a/tests/integration/210.command.deploy.test.cjs b/tests/integration/210.command.deploy.test.cjs index 723347acc0e..c3dccc425b6 100644 --- a/tests/integration/210.command.deploy.test.cjs +++ b/tests/integration/210.command.deploy.test.cjs @@ -1,4 +1,3 @@ -/* eslint-disable require-await */ const { join } = require('path') const process = require('process') @@ -795,4 +794,3 @@ if (process.env.NETLIFY_TEST_DISABLE_LIVE !== 'true') { test('always pass, used for forked PRs since ava fails when no tests are present', (t) => { t.pass() }) -/* eslint-enable require-await */ diff --git a/tests/integration/300.command.dev.test.cjs b/tests/integration/300.command.dev.test.cjs index 9a9ba7ce2cf..85887a9063d 100644 --- a/tests/integration/300.command.dev.test.cjs +++ b/tests/integration/300.command.dev.test.cjs @@ -1,5 +1,4 @@ // Handlers are meant to be async outside tests -/* eslint-disable require-await */ const path = require('path') const process = require('process') @@ -281,4 +280,3 @@ test('should inject env vars based on [dev].envFiles file order', async (t) => { }) }) }) -/* eslint-enable require-await */ diff --git a/tests/integration/330.serving-functions.test.cjs b/tests/integration/330.serving-functions.test.cjs index abc756702f2..29d3139eda6 100644 --- a/tests/integration/330.serving-functions.test.cjs +++ b/tests/integration/330.serving-functions.test.cjs @@ -1,4 +1,3 @@ -/* eslint-disable require-await */ const { join } = require('path') // eslint-disable-next-line ava/use-test @@ -962,4 +961,3 @@ test('Ensures watcher watches included files', async (t) => { }) }) }) -/* eslint-enable require-await */ diff --git a/tests/integration/400.command.dev.test.cjs b/tests/integration/400.command.dev.test.cjs index 1ba6b103a58..aeb01aa62c5 100644 --- a/tests/integration/400.command.dev.test.cjs +++ b/tests/integration/400.command.dev.test.cjs @@ -1,5 +1,4 @@ // Handlers are meant to be async outside tests -/* eslint-disable require-await */ const process = require('process') // eslint-disable-next-line ava/use-test @@ -413,4 +412,3 @@ test('should handle multipart form data when redirecting', async (t) => { }) }) }) -/* eslint-enable require-await */ diff --git a/tests/integration/500.command.dev.test.cjs b/tests/integration/500.command.dev.test.cjs index 9cbe348a7c3..a509df87394 100644 --- a/tests/integration/500.command.dev.test.cjs +++ b/tests/integration/500.command.dev.test.cjs @@ -1,5 +1,4 @@ // Handlers are meant to be async outside tests -/* eslint-disable require-await */ const { promises: fs } = require('fs') const path = require('path') @@ -472,4 +471,3 @@ test('Handles errors from the `onPreDev` event', async (t) => { ) }) }) -/* eslint-enable require-await */ diff --git a/tests/integration/eleventy-site/functions/echo.cjs b/tests/integration/eleventy-site/functions/echo.cjs index 357343c7ace..a46ecb8192b 100644 --- a/tests/integration/eleventy-site/functions/echo.cjs +++ b/tests/integration/eleventy-site/functions/echo.cjs @@ -1,4 +1,3 @@ -// eslint-disable-next-line require-await const handler = async (event) => ({ statusCode: 200, body: JSON.stringify(event), diff --git a/tests/unit/utils/deploy/hash-fns.test.mjs b/tests/unit/utils/deploy/hash-fns.test.mjs index eef29ec6aff..5054ced118a 100644 --- a/tests/unit/utils/deploy/hash-fns.test.mjs +++ b/tests/unit/utils/deploy/hash-fns.test.mjs @@ -11,12 +11,10 @@ test('Hashes files in a folder', async () => { .withNetlifyToml({ config: { functions: { directory: 'functions' } } }) .withFunction({ path: 'hello.js', - // eslint-disable-next-line require-await handler: async () => ({ statusCode: 200, body: 'Hello' }), }) .withFunction({ path: 'goodbye.js', - // eslint-disable-next-line require-await handler: async () => ({ statusCode: 200, body: 'Goodbye' }), }) .buildAsync() From e3eea4a645576a0dbe58f0378badfb8b8a485b33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 3 Jan 2023 17:36:41 +0000 Subject: [PATCH 17/43] Apply suggestions from code review Co-authored-by: Matt Kane --- docs/commands/dev.md | 2 +- src/commands/dev/dev.mjs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/commands/dev.md b/docs/commands/dev.md index 6f8782c19fc..80501e90a69 100644 --- a/docs/commands/dev.md +++ b/docs/commands/dev.md @@ -30,7 +30,7 @@ netlify dev - `live` (*boolean*) - start a public live session - `offline` (*boolean*) - disables any features that require network access - `port` (*string*) - port of netlify dev -- `prod` (*boolean*) - build the site for production +- `prod` (*boolean*) - build the site for production and serve locally. Changes to source files will not be visible until you restart the server. - `sessionId` (*string*) - (Graph) connect to cloud session with ID [sessionId] - `targetPort` (*string*) - port of target app server - `debug` (*boolean*) - Print debugging information diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index 4f2b4ed8dd7..c5dbe35b83b 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -526,7 +526,7 @@ const dev = async (options, command) => { const { configPath: configPathOverride } = await runBuild({ cachedConfig, options, settings, site }) - // When using the `serve` flag, we want to use the production functions built + // When using the `prod` flag, we want to use the production functions built // by Netlify Build rather than building them from source. const loadDistFunctions = Boolean(options.prod) @@ -827,7 +827,7 @@ export const createDevCommand = (program) => { .option('-o ,--offline', 'disables any features that require network access') .option('-l, --live', 'start a public live session', false) .option('--functionsPort ', 'port of functions server', (value) => Number.parseInt(value)) - .option('-P, --prod', 'build the site for production', false) + .option('-P, --prod', 'build the site for production and serve locally', false) .addOption( new Option( '--geo ', From 29342333651a437275e85bfb28d991e190e7d92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 3 Jan 2023 18:00:18 +0000 Subject: [PATCH 18/43] refactor: treat `NETLIFY_DEV` consistently across function types --- src/commands/dev/dev.mjs | 7 ++++++- src/lib/edge-functions/registry.mjs | 7 ++----- src/utils/dev.mjs | 6 +----- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index c5dbe35b83b..0572624af7a 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -460,6 +460,11 @@ const dev = async (options, command) => { let { env } = cachedConfig + // Add `NETLIFY_DEV` to the environment variables unless `prod` is set. + if (!options.prod) { + env.NETLIFY_DEV = { sources: ['general'], value: 'true' } + } + // If the `prod` flag is present, we override the `framework` value so that // we start a static server and not the framework's development server. if (options.prod) { @@ -566,7 +571,7 @@ const dev = async (options, command) => { addonsUrls, config, configPath: configPathOverride, - env: command.netlify.cachedConfig.env, + env, geolocationMode: options.geo, geoCountry: options.country, getUpdatedConfig, diff --git a/src/lib/edge-functions/registry.mjs b/src/lib/edge-functions/registry.mjs index f5aeb7d5077..92689b7c3be 100644 --- a/src/lib/edge-functions/registry.mjs +++ b/src/lib/edge-functions/registry.mjs @@ -213,17 +213,14 @@ export class EdgeFunctionsRegistry { if ( variable.sources.includes('ui') || variable.sources.includes('account') || - variable.sources.includes('addons') + variable.sources.includes('addons') || + key === 'NETLIFY_DEV' ) { env[key] = variable.value } }) env.DENO_REGION = 'local' - env.NETLIFY_DEV = 'true' - // We use it in the bootstrap layer to detect whether we're running in production or not - // (see https://github.com/netlify/edge-functions-bootstrap/blob/main/src/bootstrap/environment.ts#L2) - // env.DENO_DEPLOYMENT_ID = 'xxx=' return env } diff --git a/src/utils/dev.mjs b/src/utils/dev.mjs index 23a5b369556..22f3ea3a401 100644 --- a/src/utils/dev.mjs +++ b/src/utils/dev.mjs @@ -139,7 +139,7 @@ const getEnvSourceName = (source) => { // Takes a set of environment variables in the format provided by @netlify/config, augments it with variables from both // dot-env files and the process itself, and injects into `process.env`. -export const injectEnvVariables = async ({ devConfig, env, prod, site }) => { +export const injectEnvVariables = async ({ devConfig, env, site }) => { const environment = new Map(Object.entries(env)) const dotEnvFiles = await loadDotEnvFiles({ envFiles: devConfig.envFiles, projectDir: site.root }) @@ -182,10 +182,6 @@ export const injectEnvVariables = async ({ devConfig, env, prod, site }) => { process.env[key] = variable.value } } - - if (prod !== true) { - process.env.NETLIFY_DEV = 'true' - } } export const acquirePort = async ({ configuredPort, defaultPort, errorMessage }) => { From ebb233467d788abbc071a82adab463c7680519cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 3 Jan 2023 18:23:03 +0000 Subject: [PATCH 19/43] refactor: add `internal` source for env vars --- src/commands/dev/dev.mjs | 2 +- src/commands/functions/functions-serve.mjs | 5 ++++- src/lib/edge-functions/registry.mjs | 2 +- src/utils/dev.mjs | 8 ++++++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index 0572624af7a..aec82494778 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -462,7 +462,7 @@ const dev = async (options, command) => { // Add `NETLIFY_DEV` to the environment variables unless `prod` is set. if (!options.prod) { - env.NETLIFY_DEV = { sources: ['general'], value: 'true' } + env.NETLIFY_DEV = { sources: ['internal'], value: 'true' } } // If the `prod` flag is present, we override the `framework` value so that diff --git a/src/commands/functions/functions-serve.mjs b/src/commands/functions/functions-serve.mjs index a349d5f4b84..9b077fc2157 100644 --- a/src/commands/functions/functions-serve.mjs +++ b/src/commands/functions/functions-serve.mjs @@ -16,8 +16,11 @@ const functionsServe = async (options, command) => { const { api, config, site, siteInfo } = command.netlify const functionsDir = getFunctionsDir({ options, config }, join('netlify', 'functions')) + const { env } = command.netlify.cachedConfig - await injectEnvVariables({ devConfig: { ...config.dev }, env: command.netlify.cachedConfig.env, site }) + env.NETLIFY_DEV = { sources: ['internal'], value: 'true' } + + await injectEnvVariables({ devConfig: { ...config.dev }, env, site }) const { capabilities, siteUrl, timeouts } = await getSiteInformation({ offline: options.offline, diff --git a/src/lib/edge-functions/registry.mjs b/src/lib/edge-functions/registry.mjs index 92689b7c3be..c658b9529e8 100644 --- a/src/lib/edge-functions/registry.mjs +++ b/src/lib/edge-functions/registry.mjs @@ -214,7 +214,7 @@ export class EdgeFunctionsRegistry { variable.sources.includes('ui') || variable.sources.includes('account') || variable.sources.includes('addons') || - key === 'NETLIFY_DEV' + variable.sources.includes('internal') ) { env[key] = variable.value } diff --git a/src/utils/dev.mjs b/src/utils/dev.mjs index 22f3ea3a401..54f0db3c272 100644 --- a/src/utils/dev.mjs +++ b/src/utils/dev.mjs @@ -148,6 +148,10 @@ export const injectEnvVariables = async ({ devConfig, env, site }) => { const newSourceName = `${file} file` const sources = environment.has(key) ? [newSourceName, ...environment.get(key).sources] : [newSourceName] + if (sources.includes('internal')) { + return + } + environment.set(key, { sources, value: fileEnv[key], @@ -174,8 +178,8 @@ export const injectEnvVariables = async ({ devConfig, env, site }) => { }) if (!existsInProcess) { - // Omitting `general` env vars to reduce noise in the logs. - if (usedSource !== 'general') { + // Omitting `general` and `internal` env vars to reduce noise in the logs. + if (usedSource !== 'general' && usedSource !== 'internal') { log(`${NETLIFYDEVLOG} Injected ${usedSourceName} env var: ${chalk.yellow(key)}`) } From 01df8a357254bb894d27a64e430ea251a2663269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 3 Jan 2023 18:39:09 +0000 Subject: [PATCH 20/43] fix: handle internal vars correctly --- src/utils/dev.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/utils/dev.mjs b/src/utils/dev.mjs index 54f0db3c272..9bb7e7b3ee6 100644 --- a/src/utils/dev.mjs +++ b/src/utils/dev.mjs @@ -164,6 +164,7 @@ export const injectEnvVariables = async ({ devConfig, env, site }) => { const existsInProcess = process.env[key] !== undefined const [usedSource, ...overriddenSources] = existsInProcess ? ['process', ...variable.sources] : variable.sources const usedSourceName = getEnvSourceName(usedSource) + const isInternal = variable.sources.includes('internal') overriddenSources.forEach((source) => { const sourceName = getEnvSourceName(source) @@ -177,9 +178,9 @@ export const injectEnvVariables = async ({ devConfig, env, site }) => { ) }) - if (!existsInProcess) { + if (!existsInProcess || isInternal) { // Omitting `general` and `internal` env vars to reduce noise in the logs. - if (usedSource !== 'general' && usedSource !== 'internal') { + if (usedSource !== 'general' && !isInternal) { log(`${NETLIFYDEVLOG} Injected ${usedSourceName} env var: ${chalk.yellow(key)}`) } From 59297c19eb9a1c2c60beeaaad6a7f7a5f704d755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 3 Jan 2023 19:22:59 +0000 Subject: [PATCH 21/43] chore: update docs --- docs/commands/dev.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/commands/dev.md b/docs/commands/dev.md index 80501e90a69..b96bb77cc86 100644 --- a/docs/commands/dev.md +++ b/docs/commands/dev.md @@ -30,7 +30,7 @@ netlify dev - `live` (*boolean*) - start a public live session - `offline` (*boolean*) - disables any features that require network access - `port` (*string*) - port of netlify dev -- `prod` (*boolean*) - build the site for production and serve locally. Changes to source files will not be visible until you restart the server. +- `prod` (*boolean*) - build the site for production and serve locally - `sessionId` (*string*) - (Graph) connect to cloud session with ID [sessionId] - `targetPort` (*string*) - port of target app server - `debug` (*boolean*) - Print debugging information From 917fc13cd311307ae163a1cf7e3cfa45e8ebe237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Tue, 3 Jan 2023 19:29:52 +0000 Subject: [PATCH 22/43] refactor: remove unused variable --- src/commands/dev/dev.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index aec82494778..fd104e9c254 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -476,7 +476,7 @@ const dev = async (options, command) => { log(`${NETLIFYDEVLOG} Injecting environment variable values for ${chalk.yellow('all scopes')}`) } - await injectEnvVariables({ devConfig, env, prod: options.prod, site }) + await injectEnvVariables({ devConfig, env, site }) await promptEditorHelper({ chalk, config, log, NETLIFYDEVLOG, repositoryRoot, state }) const { addonsUrls, capabilities, siteUrl, timeouts } = await getSiteInformation({ From ae74853c17f5113dae181fe57230fef02fd3c7ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Wed, 4 Jan 2023 11:18:11 +0000 Subject: [PATCH 23/43] feat: set `context=production` when `prod` is used --- src/commands/base-command.mjs | 24 ++++++++++--- src/commands/dev/dev.mjs | 1 - .../600.framework-detection.test.cjs | 12 ++++--- .../600.framework-detection.test.cjs.md | 3 +- .../600.framework-detection.test.cjs.snap | Bin 2075 -> 2108 bytes tests/integration/utils/dev-server.cjs | 33 +++++++++--------- 6 files changed, 46 insertions(+), 27 deletions(-) diff --git a/src/commands/base-command.mjs b/src/commands/base-command.mjs index 5c30ce90a62..93c12f4f848 100644 --- a/src/commands/base-command.mjs +++ b/src/commands/base-command.mjs @@ -469,11 +469,7 @@ export default class BaseCommand extends Command { return await resolveConfig({ config: options.config, cwd, - context: - options.context || - process.env.CONTEXT || - // Dev commands have a default context of `dev`, otherwise we let netlify/config handle default behavior - (['dev', 'dev:exec'].includes(this.name()) ? 'dev' : undefined), + context: options.context || process.env.CONTEXT || this.getDefaultContext(), debug: this.opts().debug, siteId: options.siteId || (typeof options.site === 'string' && options.site) || state.get('siteId'), token, @@ -505,4 +501,22 @@ export default class BaseCommand extends Command { exit(1) } } + + /** + * Returns the context that should be used in case one hasn't been explicitly + * set. The default context is `dev` most of the time, but some commands may + * wish to override that. + * + * @returns {string} + */ + getDefaultContext() { + const { prod } = this.opts() + const isDevCommand = ['dev', 'dev:exec'].includes(this.name()) + + if (isDevCommand && prod) { + return 'production' + } + + return 'dev' + } } diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index fd104e9c254..6a9ade431cc 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -822,7 +822,6 @@ export const createDevCommand = (program) => { '--context ', 'Specify a deploy context or branch for environment variables (contexts: "production", "deploy-preview", "branch-deploy", "dev")', normalizeContext, - 'dev', ) .option('-p ,--port ', 'port of netlify dev', (value) => Number.parseInt(value)) .option('--targetPort ', 'port of target app server', (value) => Number.parseInt(value)) diff --git a/tests/integration/600.framework-detection.test.cjs b/tests/integration/600.framework-detection.test.cjs index 7b48707f583..c220ddd6754 100644 --- a/tests/integration/600.framework-detection.test.cjs +++ b/tests/integration/600.framework-detection.test.cjs @@ -339,6 +339,10 @@ test('should run and serve a production build when the `--prod` flag is set', as .withNetlifyToml({ config: { build: { publish: 'public' }, + context: { + dev: { environment: { CONTEXT_CHECK: 'DEV' } }, + production: { environment: { CONTEXT_CHECK: 'PRODUCTION' } }, + }, functions: { directory: 'functions' }, plugins: [{ package: './plugins/frameworker' }], }, @@ -361,16 +365,16 @@ test('should run and serve a production build when the `--prod` flag is set', as await mkdir(generatedFunctionsDir) await writeFile( `${generatedFunctionsDir}/hello.js`, - `exports.handler = async () => ({ statusCode: 200, body: 'Hello! NETLIFY_DEV is ' + process.env.NETLIFY_DEV })`, + `const { CONTEXT_CHECK, NETLIFY_DEV } = process.env; exports.handler = async () => ({ statusCode: 200, body: JSON.stringify({ CONTEXT_CHECK, NETLIFY_DEV }) })`, ) }, }, }) .buildAsync() - await withDevServer({ cwd: builder.directory, debug: true, prod: true }, async ({ output, url }) => { - const response = await got(`${url}/hello`).text() - t.is(response, 'Hello! NETLIFY_DEV is undefined') + await withDevServer({ cwd: builder.directory, context: null, debug: true, prod: true }, async ({ output, url }) => { + const response = await got(`${url}/hello`).json() + t.deepEqual(response, { CONTEXT_CHECK: 'PRODUCTION' }) t.snapshot(normalize(output, { duration: true, filePath: true })) }) diff --git a/tests/integration/snapshots/600.framework-detection.test.cjs.md b/tests/integration/snapshots/600.framework-detection.test.cjs.md index 550d5cc7747..626a9b17c4d 100644 --- a/tests/integration/snapshots/600.framework-detection.test.cjs.md +++ b/tests/integration/snapshots/600.framework-detection.test.cjs.md @@ -243,6 +243,7 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 `◈ Netlify Dev ◈␊ + ◈ Injected netlify.toml file env var: CONTEXT_CHECK␊ ◈ Using simple static server because '[dev.framework]' was set to '#static'␊ ◈ Running static server from "site-with-framework/public"␊ ◈ Building site for production␊ @@ -264,7 +265,7 @@ Generated by [AVA](https://avajs.dev). /file/path␊ ​␊ > Context␊ - dev␊ + production␊ ​␊ > Loading plugins␊ -/file/path from netlify.toml␊ diff --git a/tests/integration/snapshots/600.framework-detection.test.cjs.snap b/tests/integration/snapshots/600.framework-detection.test.cjs.snap index 4613081fb5ec0c7e9e42706b6dff75c947c1a2f0..97490bb2fa7419083592d4c303bf264dc3aeaf13 100644 GIT binary patch literal 2108 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 literal 2075 zcmV+$2;}!cRzV;q5ZL$4$E3~L7*2F0*h+hG;rtZXGD zTCL)m?&_KL_Ea@h)iWNUNFh=rBsieR5?sTb6DOodi8yhB6L3VbfFoC~yjNA-(>*_% z_0QTNJL4J8_H@;&SMR;=d+*iECrLk((V_nMx7?&A`P`YWpP|Rb*Uo(7>}Tg5{`%)| zi<>M>rf{8)0k2#3GYKruy&PQ9@W}AE18?b63_z!&Jmby9?6kK8>*Ebnji#tkDmdgt zKf_Up%wiOUx9oUG(u`|JlpK{(qe3Yz3{0dtr0Y0UxT;iBEBX#MMGg#v)3#&eD6>Lf zlA0mj*pK=60EI^;mBOx*^N98}l?82KT<%75V^F^Cc)1*68EIgq2Sa0W-RpLvEbWvF zMRL^je}^dL0vi1uE)qJU;96G9?Lr7^nz~Kw_Moj(!$ZY&SusI zLQX(&7Eb}aO@2eSSsMu$PzjSE7Z8SsB}g(h zfK)@ywC3?5Ih_4>gB<>Tb`?3SCIRU4h>;S3T{3WMPMG#!OnLlRXz4lQhR@r@t#pLX>1)wv?g>W!;2lb zGc4TNv7Cs8T(|E00be2&at14TCaQHJ)?`PS<1YA+sb8PXmdRHERZF(x2OYNJcrT=BLsLxsR{{TZ6c z(W*d*=^`i2!^JCWEn4TWZk>&TRmRyBW0s{gNzgVDIWkE_YNjFXDD3m8#J798-GN=S znA5J$5$*U&dhlYY`8TU$TWViW+e1NNaDvw#un>b%v zK@|2r-g_YOkwv<9^Krxf<%Wx~5U5P_$_0C8D=4O?bnAjN-DV0Rq5$O|gXg4)?Y;I$0O4Snx;8=5Wqf3T+?`#o9T&M2|! zeipWPsk%$A+Frki*gtKc;CCm80!R?JX9x z8~<#W*#of^9Q%<{Y)kPoN@Y~oKQ4tEW>;z%}@DJ3P#@|hp(BTex8$ouJt ze-#Ui*FCC)G{k~^VWcm8&YJGI|^p38smZccedPWCy7$AX*F@?!2kLaRb{w8a& zp_!}TPq3l6RbUpsLb(>F;{Sq3FwCo;i=p0~TW?;!@!s1vZr|B`^H!_1^^&8}T7C3n zJbI&c;%9%CW;uzG3UT2Wq+g_&Z$7Dbxf_Ob^KSJs6{L|xf^E%Z+19i9p8a_2Q;BLE zqQ6S}bjn~UguOz|2(5=7`~s0sZom$GWHhy&H&~wZX{~S#-sVb^O#pu2^Fi0e$hSK9 z&kwqZ(V=mOOp*+INXw>q^!n^Np4?wpTU7v|zu*2`t2mz6Xt zyAL{lJdTlPT<0`9P!IZ+rbc&H@atf5u5^M|gX|lSt*>WqaGFvlY?D$cqD_A$1Mp!h} z@ydG|L?lk%9Qn9)A-$sX?Us#8w*o}hLBO3GR6D Date: Wed, 4 Jan 2023 11:18:52 +0000 Subject: [PATCH 24/43] refactor: add beta label --- docs/commands/dev.md | 2 +- src/commands/dev/dev.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/commands/dev.md b/docs/commands/dev.md index b96bb77cc86..b1b1f6f288f 100644 --- a/docs/commands/dev.md +++ b/docs/commands/dev.md @@ -30,7 +30,7 @@ netlify dev - `live` (*boolean*) - start a public live session - `offline` (*boolean*) - disables any features that require network access - `port` (*string*) - port of netlify dev -- `prod` (*boolean*) - build the site for production and serve locally +- `prod` (*boolean*) - (Beta) build the site for production and serve locally - `sessionId` (*string*) - (Graph) connect to cloud session with ID [sessionId] - `targetPort` (*string*) - port of target app server - `debug` (*boolean*) - Print debugging information diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index 6a9ade431cc..668d0229912 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -831,7 +831,7 @@ export const createDevCommand = (program) => { .option('-o ,--offline', 'disables any features that require network access') .option('-l, --live', 'start a public live session', false) .option('--functionsPort ', 'port of functions server', (value) => Number.parseInt(value)) - .option('-P, --prod', 'build the site for production and serve locally', false) + .option('-P, --prod', '(Beta) build the site for production and serve locally', false) .addOption( new Option( '--geo ', From 32cf4b562ac891f1c221ab6cf8b8e6c38007871a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Fri, 6 Jan 2023 15:47:04 +0000 Subject: [PATCH 25/43] feat: create separate `serve` command --- src/commands/base-command.mjs | 5 +- src/commands/dev/dev.mjs | 646 +----------------- src/commands/main.mjs | 2 + src/commands/serve/serve.mjs | 182 +++++ src/utils/banner.mjs | 17 + src/utils/detect-server-settings.mjs | 34 + src/utils/framework-server.mjs | 66 ++ src/utils/graph.mjs | 170 +++++ src/utils/proxy-server.mjs | 90 +++ src/utils/run-build.mjs | 123 ++++ src/utils/shell.mjs | 120 ++++ src/utils/static-server.mjs | 34 + src/utils/validation.mjs | 15 + .../600.framework-detection.test.cjs | 13 +- .../600.framework-detection.test.cjs.md | 3 +- .../600.framework-detection.test.cjs.snap | Bin 2108 -> 2103 bytes tests/integration/utils/dev-server.cjs | 15 +- 17 files changed, 887 insertions(+), 648 deletions(-) create mode 100644 src/commands/serve/serve.mjs create mode 100644 src/utils/banner.mjs create mode 100644 src/utils/framework-server.mjs create mode 100644 src/utils/graph.mjs create mode 100644 src/utils/proxy-server.mjs create mode 100644 src/utils/run-build.mjs create mode 100644 src/utils/shell.mjs create mode 100644 src/utils/static-server.mjs create mode 100644 src/utils/validation.mjs diff --git a/src/commands/base-command.mjs b/src/commands/base-command.mjs index 93c12f4f848..774a51b0947 100644 --- a/src/commands/base-command.mjs +++ b/src/commands/base-command.mjs @@ -510,10 +510,7 @@ export default class BaseCommand extends Command { * @returns {string} */ getDefaultContext() { - const { prod } = this.opts() - const isDevCommand = ['dev', 'dev:exec'].includes(this.name()) - - if (isDevCommand && prod) { + if (this.name() === 'serve') { return 'production' } diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index 668d0229912..d653eb667f8 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -1,317 +1,35 @@ // @ts-check -import events from 'events' -import { promises as fs } from 'fs' -import path from 'path' import process from 'process' -import fastifyStatic from '@fastify/static' -import boxen from 'boxen' import { Option } from 'commander' -import execa from 'execa' -import Fastify from 'fastify' -import stripAnsiCc from 'strip-ansi-control-characters' -import waitPort from 'wait-port' -import { INTERNAL_EDGE_FUNCTIONS_FOLDER } from '../../lib/edge-functions/consts.mjs' import { promptEditorHelper } from '../../lib/edge-functions/editor-helper.mjs' import { startFunctionsServer } from '../../lib/functions/server.mjs' -import { - OneGraphCliClient, - loadCLISession, - markCliSessionInactive, - persistNewOperationsDocForSession, - startOneGraphCLISession, -} from '../../lib/one-graph/cli-client.mjs' -import { - defaultExampleOperationsDoc, - getGraphEditUrlBySiteId, - getNetlifyGraphConfig, - readGraphQLOperationsSourceFile, -} from '../../lib/one-graph/cli-netlify-graph.mjs' -import { getPathInProject } from '../../lib/settings.cjs' -import { startSpinner, stopSpinner } from '../../lib/spinner.cjs' +import { printBanner } from '../../utils/banner.mjs' import { BANG, chalk, - error, exit, - getToken, log, NETLIFYDEV, NETLIFYDEVERR, NETLIFYDEVLOG, NETLIFYDEVWARN, normalizeConfig, - warn, - watchDebounced, } from '../../utils/command-helpers.mjs' -import detectServerSettings from '../../utils/detect-server-settings.mjs' -import { generateNetlifyGraphJWT, getSiteInformation, injectEnvVariables, processOnExit } from '../../utils/dev.mjs' +import detectServerSettings, { getConfigWithPlugins } from '../../utils/detect-server-settings.mjs' +import { getSiteInformation, injectEnvVariables } from '../../utils/dev.mjs' import { getEnvelopeEnv, normalizeContext } from '../../utils/env/index.mjs' -import { INTERNAL_FUNCTIONS_FOLDER } from '../../utils/functions/index.mjs' import { ensureNetlifyIgnore } from '../../utils/gitignore.mjs' +import { startNetlifyGraph, startPollingForAPIAuthentication } from '../../utils/graph.mjs' import { startLiveTunnel } from '../../utils/live-tunnel.mjs' import openBrowser from '../../utils/open-browser.mjs' -import { startProxy } from '../../utils/proxy.mjs' +import { generateInspectSettings, startProxyServer } from '../../utils/proxy-server.mjs' +import { runDevTimeline } from '../../utils/run-build.mjs' +import { getGeoCountryArgParser } from '../../utils/validation.mjs' import { createDevExecCommand } from './dev-exec.mjs' -const netlifyBuildPromise = import('@netlify/build') - -const startStaticServer = async ({ settings }) => { - const server = Fastify() - const rootPath = path.resolve(settings.dist) - server.register(fastifyStatic, { - root: rootPath, - etag: false, - acceptRanges: false, - lastModified: false, - }) - - server.setNotFoundHandler((_req, res) => { - res.code(404).sendFile('404.html', rootPath) - }) - - server.addHook('onRequest', (req, reply, done) => { - reply.header('X-Powered-by', 'netlify-dev') - const validMethods = ['GET', 'HEAD'] - if (!validMethods.includes(req.method)) { - reply.code(405).send('Method Not Allowed') - } - done() - }) - - await server.listen({ port: settings.frameworkPort }) - log(`\n${NETLIFYDEVLOG} Static server listening to`, settings.frameworkPort) -} - -const isNonExistingCommandError = ({ command, error: commandError }) => { - // `ENOENT` is only returned for non Windows systems - // See https://github.com/sindresorhus/execa/pull/447 - if (commandError.code === 'ENOENT') { - return true - } - - // if the command is a package manager we let it report the error - if (['yarn', 'npm'].includes(command)) { - return false - } - - // this only works on English versions of Windows - return ( - typeof commandError.message === 'string' && - commandError.message.includes('is not recognized as an internal or external command') - ) -} - -/** - * @type {(() => Promise)[]} - array of functions to run before the process exits - */ -const cleanupWork = [] - -let cleanupStarted = false - -/** - * @param {object} input - * @param {number=} input.exitCode The exit code to return when exiting the process after cleanup - */ -const cleanupBeforeExit = async ({ exitCode }) => { - // If cleanup has started, then wherever started it will be responsible for exiting - if (!cleanupStarted) { - cleanupStarted = true - try { - // eslint-disable-next-line no-unused-vars - const cleanupFinished = await Promise.all(cleanupWork.map((cleanup) => cleanup())) - } finally { - process.exit(exitCode) - } - } -} - -/** - * Run a command and pipe stdout, stderr and stdin - * @param {string} command - * @param {NodeJS.ProcessEnv} env - * @returns {execa.ExecaChildProcess} - */ -const runCommand = (command, env = {}, spinner = null) => { - const commandProcess = execa.command(command, { - preferLocal: true, - // we use reject=false to avoid rejecting synchronously when the command doesn't exist - reject: false, - env, - // windowsHide needs to be false for child process to terminate properly on Windows - windowsHide: false, - }) - - // This ensures that an active spinner stays at the bottom of the commandline - // even though the actual framework command might be outputting stuff - const pipeDataWithSpinner = (writeStream, chunk) => { - if (spinner && spinner.isSpinning) { - spinner.clear() - spinner.isSilent = true - } - writeStream.write(chunk, () => { - if (spinner && spinner.isSpinning) { - spinner.isSilent = false - spinner.render() - } - }) - } - - commandProcess.stdout.pipe(stripAnsiCc.stream()).on('data', pipeDataWithSpinner.bind(null, process.stdout)) - commandProcess.stderr.pipe(stripAnsiCc.stream()).on('data', pipeDataWithSpinner.bind(null, process.stderr)) - process.stdin.pipe(commandProcess.stdin) - - // we can't try->await->catch since we don't want to block on the framework server which - // is a long running process - // eslint-disable-next-line promise/catch-or-return - commandProcess - // eslint-disable-next-line promise/prefer-await-to-then - .then(async () => { - const result = await commandProcess - const [commandWithoutArgs] = command.split(' ') - if (result.failed && isNonExistingCommandError({ command: commandWithoutArgs, error: result })) { - log( - NETLIFYDEVERR, - `Failed running command: ${command}. Please verify ${chalk.magenta(`'${commandWithoutArgs}'`)} exists`, - ) - } else { - const errorMessage = result.failed - ? `${NETLIFYDEVERR} ${result.shortMessage}` - : `${NETLIFYDEVWARN} "${command}" exited with code ${result.exitCode}` - - log(`${errorMessage}. Shutting down Netlify Dev server`) - } - - return await cleanupBeforeExit({ exitCode: 1 }) - }) - processOnExit(async () => await cleanupBeforeExit({})) - - return commandProcess -} - -/** - * @typedef StartReturnObject - * @property {4 | 6 | undefined=} ipVersion The version the open port was found on - */ - -/** - * Start a static server if the `useStaticServer` is provided or a framework specific server - * @param {object} config - * @param {Partial} config.settings - * @returns {Promise} - */ -const startFrameworkServer = async function ({ settings }) { - if (settings.useStaticServer) { - if (settings.command) { - runCommand(settings.command, settings.env) - } - await startStaticServer({ settings }) - - return {} - } - - log(`${NETLIFYDEVLOG} Starting Netlify Dev with ${settings.framework || 'custom config'}`) - - const spinner = startSpinner({ - text: `Waiting for framework port ${settings.frameworkPort}. This can be configured using the 'targetPort' property in the netlify.toml`, - }) - - runCommand(settings.command, settings.env, spinner) - - let port - try { - port = await waitPort({ - port: settings.frameworkPort, - host: 'localhost', - output: 'silent', - timeout: FRAMEWORK_PORT_TIMEOUT, - ...(settings.pollingStrategies.includes('HTTP') && { protocol: 'http' }), - }) - - if (!port.open) { - throw new Error(`Timed out waiting for port '${settings.frameworkPort}' to be open`) - } - - stopSpinner({ error: false, spinner }) - } catch (error_) { - stopSpinner({ error: true, spinner }) - log(NETLIFYDEVERR, `Netlify Dev could not start or connect to localhost:${settings.frameworkPort}.`) - log(NETLIFYDEVERR, `Please make sure your framework server is running on port ${settings.frameworkPort}`) - error(error_) - exit(1) - } - - return { ipVersion: port?.ipVersion } -} - -// 10 minutes -const FRAMEWORK_PORT_TIMEOUT = 6e5 - -/** - * @typedef {Object} InspectSettings - * @property {boolean} enabled - Inspect enabled - * @property {boolean} pause - Pause on breakpoints - * @property {string|undefined} address - Host/port override (optional) - */ - -/** - * - * @param {object} params - * @param {*} params.addonsUrls - * @param {import('../base-command.mjs').NetlifyOptions["config"]} params.config - * @param {string} [params.configPath] An override for the Netlify config path - * @param {import('../base-command.mjs').NetlifyOptions["cachedConfig"]['env']} params.env - * @param {InspectSettings} params.inspectSettings - * @param {() => Promise} params.getUpdatedConfig - * @param {string} params.geolocationMode - * @param {string} params.geoCountry - * @param {*} params.settings - * @param {boolean} params.offline - * @param {*} params.site - * @param {*} params.siteInfo - * @param {import('../../utils/state-config.mjs').default} params.state - * @returns - */ -const startProxyServer = async ({ - addonsUrls, - config, - configPath, - env, - geoCountry, - geolocationMode, - getUpdatedConfig, - inspectSettings, - offline, - settings, - site, - siteInfo, - state, -}) => { - const url = await startProxy({ - addonsUrls, - config, - configPath: configPath || site.configPath, - env, - geolocationMode, - geoCountry, - getUpdatedConfig, - inspectSettings, - offline, - projectDir: site.root, - settings, - state, - siteInfo, - }) - if (!url) { - log(NETLIFYDEVERR, `Unable to start proxy server on port '${settings.port}'`) - exit(1) - } - - return url -} - /** * * @param {object} config @@ -333,83 +51,6 @@ const handleLiveTunnel = async ({ api, options, settings, site }) => { } } -const printBanner = ({ url }) => { - const banner = chalk.bold(`${NETLIFYDEVLOG} Server now ready on ${url}`) - - log( - boxen(banner, { - padding: 1, - margin: 1, - align: 'center', - borderColor: '#00c7b7', - }), - ) -} - -const startPollingForAPIAuthentication = async function (options) { - const { api, command, config, site, siteInfo } = options - const frequency = 5000 - - const helper = async (maybeSiteData) => { - const siteData = await (maybeSiteData || api.getSite({ siteId: site.id })) - const authlifyTokenId = siteData && siteData.authlify_token_id - - const existingAuthlifyTokenId = config && config.netlifyGraphConfig && config.netlifyGraphConfig.authlifyTokenId - if (authlifyTokenId && authlifyTokenId !== existingAuthlifyTokenId) { - const netlifyToken = await command.authenticate() - // Only inject the authlify config if a token ID exists. This prevents - // calling command.authenticate() (which opens a browser window) if the - // user hasn't enabled API Authentication - const netlifyGraphConfig = { - netlifyToken, - authlifyTokenId: siteData.authlify_token_id, - siteId: site.id, - } - config.netlifyGraphConfig = netlifyGraphConfig - - const netlifyGraphJWT = generateNetlifyGraphJWT(netlifyGraphConfig) - - if (netlifyGraphJWT != null) { - // XXX(anmonteiro): this name is deprecated. Delete after 3/31/2022 - process.env.ONEGRAPH_AUTHLIFY_TOKEN = netlifyGraphJWT - process.env.NETLIFY_GRAPH_TOKEN = netlifyGraphJWT - } - } else if (!authlifyTokenId) { - // If there's no `authlifyTokenId`, it's because the user disabled API - // Auth. Delete the config in this case. - delete config.netlifyGraphConfig - } - - setTimeout(helper, frequency) - } - - await helper(siteInfo) -} - -/** - * @param {boolean|string} edgeInspect - * @param {boolean|string} edgeInspectBrk - * @returns {InspectSettings} - */ -const generateInspectSettings = (edgeInspect, edgeInspectBrk) => { - const enabled = Boolean(edgeInspect) || Boolean(edgeInspectBrk) - const pause = Boolean(edgeInspectBrk) - const getAddress = () => { - if (edgeInspect) { - return typeof edgeInspect === 'string' ? edgeInspect : undefined - } - if (edgeInspectBrk) { - return typeof edgeInspectBrk === 'string' ? edgeInspectBrk : undefined - } - } - - return { - enabled, - pause, - address: getAddress(), - } -} - const validateShortFlagArgs = (args) => { if (args.startsWith('=')) { throw new Error( @@ -426,19 +67,6 @@ const validateShortFlagArgs = (args) => { return args } -const validateGeoCountryCode = (arg) => { - // Validate that the arg passed is two letters only for country - // See https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes - if (!/^[a-z]{2}$/i.test(arg)) { - throw new Error( - `The geo country code must use a two letter abbreviation. - ${chalk.red(BANG)} Example: - netlify dev --geo=mock --country=FR`, - ) - } - return arg.toUpperCase() -} - /** * The dev command * @param {import('commander').OptionValues} options @@ -460,16 +88,7 @@ const dev = async (options, command) => { let { env } = cachedConfig - // Add `NETLIFY_DEV` to the environment variables unless `prod` is set. - if (!options.prod) { - env.NETLIFY_DEV = { sources: ['internal'], value: 'true' } - } - - // If the `prod` flag is present, we override the `framework` value so that - // we start a static server and not the framework's development server. - if (options.prod) { - devConfig.framework = '#static' - } + env.NETLIFY_DEV = { sources: ['internal'], value: 'true' } if (!options.offline && siteInfo.use_envelope) { env = await getEnvelopeEnv({ api, context: options.context, env, siteInfo }) @@ -492,25 +111,7 @@ const dev = async (options, command) => { try { settings = await detectServerSettings(devConfig, options, site.root) - // 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. - if (settings.plugins) { - const { plugins: existingPlugins = [] } = cachedConfig.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(Boolean) - - cachedConfig.config.plugins = [...newPlugins, ...cachedConfig.config.plugins] - } + cachedConfig.config = getConfigWithPlugins(cachedConfig.config, settings) } catch (error_) { log(NETLIFYDEVERR, error_.message) exit(1) @@ -523,24 +124,15 @@ const dev = async (options, command) => { startPollingForAPIAuthentication({ api, command, config, site, siteInfo }) } - if (options.prod) { - log(`${NETLIFYDEVWARN} Building site for production`) - } else { - log(`${NETLIFYDEVWARN} Setting up local development server`) - } - - const { configPath: configPathOverride } = await runBuild({ cachedConfig, options, settings, site }) + log(`${NETLIFYDEVWARN} Setting up local development server`) - // When using the `prod` flag, we want to use the production functions built - // by Netlify Build rather than building them from source. - const loadDistFunctions = Boolean(options.prod) + const { configPath: configPathOverride } = await runDevTimeline({ cachedConfig, options, settings, site }) await startFunctionsServer({ api, command, config, debug: options.debug, - loadDistFunctions, settings, site, siteInfo, @@ -593,214 +185,17 @@ const dev = async (options, command) => { process.env.URL = url process.env.DEPLOY_URL = url - if (startNetlifyGraphWatcher && options.offline) { - warn(`Unable to start Netlify Graph in offline mode`) - } else if (startNetlifyGraphWatcher && !site.id) { - error( - `No siteId defined, unable to start Netlify Graph. To enable, run ${chalk.yellow( - 'netlify init', - )} or ${chalk.yellow('netlify link')}.`, - ) - } else if (startNetlifyGraphWatcher) { - const netlifyToken = await command.authenticate() - await OneGraphCliClient.ensureAppForSite(netlifyToken, site.id) - - let stopWatchingCLISessions - - let liveConfig = { ...config } - let isRestartingSession = false - - const createOrResumeSession = async function () { - const netlifyGraphConfig = await getNetlifyGraphConfig({ command, options, settings }) - - let graphqlDocument = readGraphQLOperationsSourceFile(netlifyGraphConfig) - - if (!graphqlDocument || graphqlDocument.trim().length === 0) { - graphqlDocument = defaultExampleOperationsDoc - } - - stopWatchingCLISessions = await startOneGraphCLISession({ - config: liveConfig, - netlifyGraphConfig, - netlifyToken, - site, - state, - oneGraphSessionId: options.sessionId, - }) - - // Should be created by startOneGraphCLISession - const oneGraphSessionId = loadCLISession(state) - - await persistNewOperationsDocForSession({ - config: liveConfig, - netlifyGraphConfig, - netlifyToken, - oneGraphSessionId, - operationsDoc: graphqlDocument, - siteId: site.id, - siteRoot: site.root, - }) - - return oneGraphSessionId - } - - const configWatcher = new events.EventEmitter() - - // Only set up a watcher if we know the config path. - const { configPath } = command.netlify.site - if (configPath) { - // chokidar handle - command.configWatcherHandle = await watchDebounced(configPath, { - depth: 1, - onChange: async () => { - const cwd = options.cwd || process.cwd() - const [token] = await getToken(options.auth) - const { config: newConfig } = await command.getConfig({ cwd, state, token, ...command.netlify.apiUrlOpts }) - - const normalizedNewConfig = normalizeConfig(newConfig) - configWatcher.emit('change', normalizedNewConfig) - }, - }) - - processOnExit(async () => { - await command.configWatcherHandle.close() - }) - } - - // Set up a handler for config changes. - configWatcher.on('change', async (newConfig) => { - command.netlify.config = newConfig - liveConfig = newConfig - if (isRestartingSession) { - return - } - stopWatchingCLISessions && stopWatchingCLISessions() - isRestartingSession = true - await createOrResumeSession() - isRestartingSession = false - }) - - const oneGraphSessionId = await createOrResumeSession() - const cleanupSession = () => markCliSessionInactive({ netlifyToken, sessionId: oneGraphSessionId, siteId: site.id }) - - cleanupWork.push(cleanupSession) - - const graphEditUrl = getGraphEditUrlBySiteId({ siteId: site.id, oneGraphSessionId }) - - log( - `Starting Netlify Graph session, to edit your library visit ${graphEditUrl} or run \`netlify graph:edit\` in another tab`, - ) - } - - printBanner({ url }) -} - -// Copies `netlify.toml`, if one is defined, into the `.netlify` internal -// directory and returns the path to its new location. -const copyConfig = async ({ configPath, siteRoot }) => { - const newConfigPath = path.resolve(siteRoot, getPathInProject(['netlify.toml'])) - - try { - await fs.copyFile(configPath, newConfigPath) - } catch { - // no-op - } - - return newConfigPath -} - -// Loads and runs Netlify Build. Chooses the right flags and entry point based -// on the options supplied. -const runBuild = async ({ cachedConfig, options, settings, site }) => { - const { default: buildSite, startDev } = await netlifyBuildPromise - const sharedOptions = getBuildOptions({ - cachedConfig, + await startNetlifyGraph({ + command, + config, options, - }) - const devCommand = async (settingsOverrides = {}) => { - const { ipVersion } = await startFrameworkServer({ - settings: { - ...settings, - ...settingsOverrides, - }, - }) - - settings.frameworkHost = ipVersion === 6 ? '::1' : '127.0.0.1' - } - - if (options.prod) { - // Start by cleaning the internal directory, as it may have artifacts left - // by previous builds. - await cleanInternalDirectory(site.root) - - // Copy `netlify.toml` into the internal directory. This will be the new - // location of the config file for the duration of the command. - const tempConfigPath = await copyConfig({ configPath: cachedConfig.configPath, siteRoot: site.root }) - const buildSiteOptions = { - ...sharedOptions, - outputConfigPath: tempConfigPath, - saveConfig: true, - } - - // Run Netlify Build using the main entry point. - await buildSite(buildSiteOptions) - - // Start the dev server, forcing the usage of a static server as opposed to - // the framework server. - await devCommand({ - command: undefined, - useStaticServer: true, - }) - - return { configPath: tempConfigPath } - } - - const startDevOptions = { - ...sharedOptions, - - // Set `quiet` to suppress non-essential output from Netlify Build unless - // the `debug` flag is set. - quiet: !options.debug, - } - - // Run Netlify Build using the `startDev` entry point. - const { error: startDevError, success } = await startDev(devCommand, startDevOptions) - - if (!success) { - error(`Could not start local development server\n\n${startDevError.message}\n\n${startDevError.stack}`) - } - - return {} -} - -const getBuildOptions = ({ - cachedConfig, - options: { configPath, context, cwd = process.cwd(), debug, dry, offline, quiet, saveConfig }, -}) => ({ - cachedConfig, - configPath, - siteId: cachedConfig.siteInfo.id, - token: cachedConfig.token, - dry, - debug, - context, - mode: 'cli', - telemetry: false, - buffer: false, - offline, - cwd, - quiet, - saveConfig, -}) - -const cleanInternalDirectory = async (basePath) => { - const ops = [INTERNAL_FUNCTIONS_FOLDER, INTERNAL_EDGE_FUNCTIONS_FOLDER, 'netlify.toml'].map((name) => { - const fullPath = path.resolve(basePath, getPathInProject([name])) - - return fs.rm(fullPath, { force: true, recursive: true }) + settings, + site, + startNetlifyGraphWatcher, + state, }) - await Promise.all(ops) + printBanner({ url }) } /** @@ -831,7 +226,6 @@ export const createDevCommand = (program) => { .option('-o ,--offline', 'disables any features that require network access') .option('-l, --live', 'start a public live session', false) .option('--functionsPort ', 'port of functions server', (value) => Number.parseInt(value)) - .option('-P, --prod', '(Beta) build the site for production and serve locally', false) .addOption( new Option( '--geo ', @@ -844,7 +238,7 @@ export const createDevCommand = (program) => { new Option( '--country ', 'Two-letter country code (https://ntl.fyi/country-codes) to use as mock geolocation (enables --geo=mock automatically)', - ).argParser(validateGeoCountryCode), + ).argParser(getGeoCountryArgParser('netlify dev --geo=mock --country=FR')), ) .addOption( new Option('--staticServerPort ', 'port of the static app server used when no framework is detected') diff --git a/src/commands/main.mjs b/src/commands/main.mjs index 511b866721d..6037bfd65e2 100644 --- a/src/commands/main.mjs +++ b/src/commands/main.mjs @@ -28,6 +28,7 @@ import { createLoginCommand } from './login/index.mjs' import { createLogoutCommand } from './logout/index.mjs' import { createOpenCommand } from './open/index.mjs' import { createRecipesCommand } from './recipes/index.mjs' +import { createServeCommand } from './serve/serve.mjs' import { createSitesCommand } from './sites/index.mjs' import { createStatusCommand } from './status/index.mjs' import { createSwitchCommand } from './switch/index.mjs' @@ -173,6 +174,7 @@ export const createMainCommand = () => { createLoginCommand(program) createLogoutCommand(program) createOpenCommand(program) + createServeCommand(program) createSitesCommand(program) createStatusCommand(program) createSwitchCommand(program) diff --git a/src/commands/serve/serve.mjs b/src/commands/serve/serve.mjs new file mode 100644 index 00000000000..8ad1803e734 --- /dev/null +++ b/src/commands/serve/serve.mjs @@ -0,0 +1,182 @@ +// @ts-check +import process from 'process' + +import { Option } from 'commander' + +import { promptEditorHelper } from '../../lib/edge-functions/editor-helper.mjs' +import { startFunctionsServer } from '../../lib/functions/server.mjs' +import { printBanner } from '../../utils/banner.mjs' +import { + chalk, + exit, + log, + NETLIFYDEVERR, + NETLIFYDEVLOG, + NETLIFYDEVWARN, + normalizeConfig, +} from '../../utils/command-helpers.mjs' +import detectServerSettings, { getConfigWithPlugins } from '../../utils/detect-server-settings.mjs' +import { getSiteInformation, injectEnvVariables } from '../../utils/dev.mjs' +import { getEnvelopeEnv, normalizeContext } from '../../utils/env/index.mjs' +import { ensureNetlifyIgnore } from '../../utils/gitignore.mjs' +import openBrowser from '../../utils/open-browser.mjs' +import { generateInspectSettings, startProxyServer } from '../../utils/proxy-server.mjs' +import { runBuildTimeline } from '../../utils/run-build.mjs' +import { getGeoCountryArgParser } from '../../utils/validation.mjs' + +/** + * The serve command + * @param {import('commander').OptionValues} options + * @param {import('../base-command.mjs').default} command + */ +const serve = async (options, command) => { + const { api, cachedConfig, config, repositoryRoot, site, siteInfo, state } = command.netlify + config.dev = { ...config.dev } + config.build = { ...config.build } + /** @type {import('../dev/types').DevConfig} */ + const devConfig = { + framework: '#auto', + ...(config.functionsDirectory && { functions: config.functionsDirectory }), + ...(config.build.publish && { publish: config.build.publish }), + ...config.dev, + ...options, + } + + let { env } = cachedConfig + + // Override the `framework` value so that we start a static server and not + // the framework's development server. + devConfig.framework = '#static' + + if (!options.offline && siteInfo.use_envelope) { + env = await getEnvelopeEnv({ api, context: options.context, env, siteInfo }) + log(`${NETLIFYDEVLOG} Injecting environment variable values for ${chalk.yellow('all scopes')}`) + } + + await injectEnvVariables({ devConfig, env, site }) + await promptEditorHelper({ chalk, config, log, NETLIFYDEVLOG, repositoryRoot, state }) + + const { addonsUrls, capabilities, siteUrl, timeouts } = await getSiteInformation({ + // inherited from base command --offline + offline: options.offline, + api, + site, + siteInfo, + }) + + /** @type {Partial} */ + let settings = {} + try { + settings = await detectServerSettings(devConfig, options, site.root) + + cachedConfig.config = getConfigWithPlugins(cachedConfig.config, settings) + } catch (error_) { + log(NETLIFYDEVERR, error_.message) + exit(1) + } + + command.setAnalyticsPayload({ projectType: settings.framework || 'custom', live: options.live, graph: options.graph }) + + log(`${NETLIFYDEVWARN} Building site for production`) + + const { configPath: configPathOverride } = await runBuildTimeline({ cachedConfig, options, settings, site }) + + await startFunctionsServer({ + api, + command, + config, + debug: options.debug, + loadDistFunctions: true, + settings, + site, + siteInfo, + siteUrl, + capabilities, + timeouts, + }) + + // 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 cwd = options.cwd || process.cwd() + const { config: newConfig } = await command.getConfig({ cwd, offline: true, state }) + const normalizedNewConfig = normalizeConfig(newConfig) + + return normalizedNewConfig + } + + const inspectSettings = generateInspectSettings(options.edgeInspect, options.edgeInspectBrk) + const url = await startProxyServer({ + addonsUrls, + config, + configPath: configPathOverride, + env, + geolocationMode: options.geo, + geoCountry: options.country, + getUpdatedConfig, + inspectSettings, + offline: options.offline, + settings, + site, + siteInfo, + state, + }) + + if (devConfig.autoLaunch !== false) { + await openBrowser({ url, silentBrowserNoneError: true }) + } + + process.env.URL = url + process.env.DEPLOY_URL = url + + printBanner({ url }) +} + +/** + * Creates the `netlify serve` command + * @param {import('../base-command.mjs').default} program + * @returns + */ +export const createServeCommand = (program) => + program + .command('serve') + .description('(Beta) build the site for production and serve locally') + .option( + '--context ', + 'Specify a deploy context or branch for environment variables (contexts: "production", "deploy-preview", "branch-deploy", "dev")', + normalizeContext, + ) + .option('-p ,--port ', 'port of netlify dev', (value) => Number.parseInt(value)) + .option('--targetPort ', 'port of target app server', (value) => Number.parseInt(value)) + .option('--framework ', 'framework to use. Defaults to #auto which automatically detects a framework') + .option('-d ,--dir ', 'dir with static files') + .option('-f ,--functions ', 'specify a functions folder to serve') + .option('-o ,--offline', 'disables any features that require network access') + .option('--functionsPort ', 'port of functions server', (value) => Number.parseInt(value)) + .addOption( + new Option( + '--geo ', + '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 ', + '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 of the static app server used when no framework is detected') + .argParser((value) => Number.parseInt(value)) + .hideHelp(), + ) + .addExamples(['netlify serve', 'BROWSER=none netlify serve # disable browser auto opening']) + .action(serve) diff --git a/src/utils/banner.mjs b/src/utils/banner.mjs new file mode 100644 index 00000000000..f899d5e097a --- /dev/null +++ b/src/utils/banner.mjs @@ -0,0 +1,17 @@ +// @ts-check +import boxen from 'boxen' + +import { chalk, log, NETLIFYDEVLOG } from './command-helpers.mjs' + +export const printBanner = ({ url }) => { + const banner = chalk.bold(`${NETLIFYDEVLOG} Server now ready on ${url}`) + + log( + boxen(banner, { + padding: 1, + margin: 1, + align: 'center', + borderColor: '#00c7b7', + }), + ) +} diff --git a/src/utils/detect-server-settings.mjs b/src/utils/detect-server-settings.mjs index 1e8c8b073e2..c69ba210bfb 100644 --- a/src/utils/detect-server-settings.mjs +++ b/src/utils/detect-server-settings.mjs @@ -364,4 +364,38 @@ const formatSettingsArrForInquirer = function (frameworks) { return formattedArr.flat() } +/** + * Returns a copy of the provided config with any plugins provided by the + * server settings + * @param {*} config + * @param {Partial} settings + * @returns {*} Modified config + */ +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(Boolean) + + return { + ...config, + plugins: [...newPlugins, ...config.plugins], + } +} + export default detectServerSettings diff --git a/src/utils/framework-server.mjs b/src/utils/framework-server.mjs new file mode 100644 index 00000000000..59fc5a74dd0 --- /dev/null +++ b/src/utils/framework-server.mjs @@ -0,0 +1,66 @@ +// @ts-check +import waitPort from 'wait-port' + +import { startSpinner, stopSpinner } from '../lib/spinner.cjs' + +import { error, exit, log, NETLIFYDEVERR, NETLIFYDEVLOG } from './command-helpers.mjs' +import { runCommand } from './shell.mjs' +import { startStaticServer } from './static-server.mjs' + +// 10 minutes +const FRAMEWORK_PORT_TIMEOUT = 6e5 + +/** + * @typedef StartReturnObject + * @property {4 | 6 | undefined=} ipVersion The version the open port was found on + */ + +/** + * Start a static server if the `useStaticServer` is provided or a framework specific server + * @param {object} config + * @param {Partial} config.settings + * @returns {Promise} + */ +export const startFrameworkServer = async function ({ settings }) { + if (settings.useStaticServer) { + if (settings.command) { + runCommand(settings.command, settings.env) + } + await startStaticServer({ settings }) + + return {} + } + + log(`${NETLIFYDEVLOG} Starting Netlify Dev with ${settings.framework || 'custom config'}`) + + const spinner = startSpinner({ + text: `Waiting for framework port ${settings.frameworkPort}. This can be configured using the 'targetPort' property in the netlify.toml`, + }) + + runCommand(settings.command, settings.env, spinner) + + let port + try { + port = await waitPort({ + port: settings.frameworkPort, + host: 'localhost', + output: 'silent', + timeout: FRAMEWORK_PORT_TIMEOUT, + ...(settings.pollingStrategies.includes('HTTP') && { protocol: 'http' }), + }) + + if (!port.open) { + throw new Error(`Timed out waiting for port '${settings.frameworkPort}' to be open`) + } + + stopSpinner({ error: false, spinner }) + } catch (error_) { + stopSpinner({ error: true, spinner }) + log(NETLIFYDEVERR, `Netlify Dev could not start or connect to localhost:${settings.frameworkPort}.`) + log(NETLIFYDEVERR, `Please make sure your framework server is running on port ${settings.frameworkPort}`) + error(error_) + exit(1) + } + + return { ipVersion: port?.ipVersion } +} diff --git a/src/utils/graph.mjs b/src/utils/graph.mjs new file mode 100644 index 00000000000..9b51bad6481 --- /dev/null +++ b/src/utils/graph.mjs @@ -0,0 +1,170 @@ +// @ts-check +import events from 'events' +import process from 'process' + +import { + OneGraphCliClient, + loadCLISession, + markCliSessionInactive, + persistNewOperationsDocForSession, + startOneGraphCLISession, +} from '../lib/one-graph/cli-client.mjs' +import { + defaultExampleOperationsDoc, + getGraphEditUrlBySiteId, + getNetlifyGraphConfig, + readGraphQLOperationsSourceFile, +} from '../lib/one-graph/cli-netlify-graph.mjs' + +import { chalk, error, getToken, log, normalizeConfig, warn, watchDebounced } from './command-helpers.mjs' +import { generateNetlifyGraphJWT, processOnExit } from './dev.mjs' +import { addCleanupJob } from './shell.mjs' + +export const startPollingForAPIAuthentication = async function (options) { + const { api, command, config, site, siteInfo } = options + const frequency = 5000 + + const helper = async (maybeSiteData) => { + const siteData = await (maybeSiteData || api.getSite({ siteId: site.id })) + const authlifyTokenId = siteData && siteData.authlify_token_id + + const existingAuthlifyTokenId = config && config.netlifyGraphConfig && config.netlifyGraphConfig.authlifyTokenId + if (authlifyTokenId && authlifyTokenId !== existingAuthlifyTokenId) { + const netlifyToken = await command.authenticate() + // Only inject the authlify config if a token ID exists. This prevents + // calling command.authenticate() (which opens a browser window) if the + // user hasn't enabled API Authentication + const netlifyGraphConfig = { + netlifyToken, + authlifyTokenId: siteData.authlify_token_id, + siteId: site.id, + } + config.netlifyGraphConfig = netlifyGraphConfig + + const netlifyGraphJWT = generateNetlifyGraphJWT(netlifyGraphConfig) + + if (netlifyGraphJWT != null) { + // XXX(anmonteiro): this name is deprecated. Delete after 3/31/2022 + process.env.ONEGRAPH_AUTHLIFY_TOKEN = netlifyGraphJWT + process.env.NETLIFY_GRAPH_TOKEN = netlifyGraphJWT + } + } else if (!authlifyTokenId) { + // If there's no `authlifyTokenId`, it's because the user disabled API + // Auth. Delete the config in this case. + delete config.netlifyGraphConfig + } + + setTimeout(helper, frequency) + } + + await helper(siteInfo) +} + +export const startNetlifyGraph = async ({ + command, + config, + options, + settings, + site, + startNetlifyGraphWatcher, + state, +}) => { + if (startNetlifyGraphWatcher && options.offline) { + warn(`Unable to start Netlify Graph in offline mode`) + } else if (startNetlifyGraphWatcher && !site.id) { + error( + `No siteId defined, unable to start Netlify Graph. To enable, run ${chalk.yellow( + 'netlify init', + )} or ${chalk.yellow('netlify link')}.`, + ) + } else if (startNetlifyGraphWatcher) { + const netlifyToken = await command.authenticate() + await OneGraphCliClient.ensureAppForSite(netlifyToken, site.id) + + let stopWatchingCLISessions + + let liveConfig = { ...config } + let isRestartingSession = false + + const createOrResumeSession = async function () { + const netlifyGraphConfig = await getNetlifyGraphConfig({ command, options, settings }) + + let graphqlDocument = readGraphQLOperationsSourceFile(netlifyGraphConfig) + + if (!graphqlDocument || graphqlDocument.trim().length === 0) { + graphqlDocument = defaultExampleOperationsDoc + } + + stopWatchingCLISessions = await startOneGraphCLISession({ + config: liveConfig, + netlifyGraphConfig, + netlifyToken, + site, + state, + oneGraphSessionId: options.sessionId, + }) + + // Should be created by startOneGraphCLISession + const oneGraphSessionId = loadCLISession(state) + + await persistNewOperationsDocForSession({ + config: liveConfig, + netlifyGraphConfig, + netlifyToken, + oneGraphSessionId, + operationsDoc: graphqlDocument, + siteId: site.id, + siteRoot: site.root, + }) + + return oneGraphSessionId + } + + const configWatcher = new events.EventEmitter() + + // Only set up a watcher if we know the config path. + const { configPath } = command.netlify.site + if (configPath) { + // chokidar handle + command.configWatcherHandle = await watchDebounced(configPath, { + depth: 1, + onChange: async () => { + const cwd = options.cwd || process.cwd() + const [token] = await getToken(options.auth) + const { config: newConfig } = await command.getConfig({ cwd, state, token, ...command.netlify.apiUrlOpts }) + + const normalizedNewConfig = normalizeConfig(newConfig) + configWatcher.emit('change', normalizedNewConfig) + }, + }) + + processOnExit(async () => { + await command.configWatcherHandle.close() + }) + } + + // Set up a handler for config changes. + configWatcher.on('change', async (newConfig) => { + command.netlify.config = newConfig + liveConfig = newConfig + if (isRestartingSession) { + return + } + stopWatchingCLISessions && stopWatchingCLISessions() + isRestartingSession = true + await createOrResumeSession() + isRestartingSession = false + }) + + const oneGraphSessionId = await createOrResumeSession() + const cleanupSession = () => markCliSessionInactive({ netlifyToken, sessionId: oneGraphSessionId, siteId: site.id }) + + addCleanupJob(cleanupSession) + + const graphEditUrl = getGraphEditUrlBySiteId({ siteId: site.id, oneGraphSessionId }) + + log( + `Starting Netlify Graph session, to edit your library visit ${graphEditUrl} or run \`netlify graph:edit\` in another tab`, + ) + } +} diff --git a/src/utils/proxy-server.mjs b/src/utils/proxy-server.mjs new file mode 100644 index 00000000000..ff6de57bc81 --- /dev/null +++ b/src/utils/proxy-server.mjs @@ -0,0 +1,90 @@ +// @ts-check +import { exit, log, NETLIFYDEVERR } from './command-helpers.mjs' +import { startProxy } from './proxy.mjs' + +/** + * @typedef {Object} InspectSettings + * @property {boolean} enabled - Inspect enabled + * @property {boolean} pause - Pause on breakpoints + * @property {string|undefined} address - Host/port override (optional) + */ + +/** + * @param {boolean|string} edgeInspect + * @param {boolean|string} edgeInspectBrk + * @returns {InspectSettings} + */ +export const generateInspectSettings = (edgeInspect, edgeInspectBrk) => { + const enabled = Boolean(edgeInspect) || Boolean(edgeInspectBrk) + const pause = Boolean(edgeInspectBrk) + const getAddress = () => { + if (edgeInspect) { + return typeof edgeInspect === 'string' ? edgeInspect : undefined + } + if (edgeInspectBrk) { + return typeof edgeInspectBrk === 'string' ? edgeInspectBrk : undefined + } + } + + return { + enabled, + pause, + address: getAddress(), + } +} + +/** + * + * @param {object} params + * @param {*} params.addonsUrls + * @param {import('../commands/base-command.mjs').NetlifyOptions["config"]} params.config + * @param {string} [params.configPath] An override for the Netlify config path + * @param {import('../commands/base-command.mjs').NetlifyOptions["cachedConfig"]['env']} params.env + * @param {InspectSettings} params.inspectSettings + * @param {() => Promise} params.getUpdatedConfig + * @param {string} params.geolocationMode + * @param {string} params.geoCountry + * @param {*} params.settings + * @param {boolean} params.offline + * @param {*} params.site + * @param {*} params.siteInfo + * @param {import('./state-config.mjs').default} params.state + * @returns + */ +export const startProxyServer = async ({ + addonsUrls, + config, + configPath, + env, + geoCountry, + geolocationMode, + getUpdatedConfig, + inspectSettings, + offline, + settings, + site, + siteInfo, + state, +}) => { + const url = await startProxy({ + addonsUrls, + config, + configPath: configPath || site.configPath, + env, + geolocationMode, + geoCountry, + getUpdatedConfig, + inspectSettings, + offline, + projectDir: site.root, + settings, + state, + siteInfo, + }) + if (!url) { + log(NETLIFYDEVERR, `Unable to start proxy server on port '${settings.port}'`) + exit(1) + } + + return url +} diff --git a/src/utils/run-build.mjs b/src/utils/run-build.mjs new file mode 100644 index 00000000000..14a190ae7ae --- /dev/null +++ b/src/utils/run-build.mjs @@ -0,0 +1,123 @@ +// @ts-check +import { promises as fs } from 'fs' +import path from 'path' +import process from 'process' + +import { INTERNAL_EDGE_FUNCTIONS_FOLDER } from '../lib/edge-functions/consts.mjs' +import { getPathInProject } from '../lib/settings.cjs' + +import { error } from './command-helpers.mjs' +import { startFrameworkServer } from './framework-server.mjs' +import { INTERNAL_FUNCTIONS_FOLDER } from './functions/index.mjs' + +const netlifyBuildPromise = import('@netlify/build') + +// Copies `netlify.toml`, if one is defined, into the `.netlify` internal +// directory and returns the path to its new location. +const copyConfig = async ({ configPath, siteRoot }) => { + const newConfigPath = path.resolve(siteRoot, getPathInProject(['netlify.toml'])) + + try { + await fs.copyFile(configPath, newConfigPath) + } catch { + // no-op + } + + return newConfigPath +} + +const cleanInternalDirectory = async (basePath) => { + const ops = [INTERNAL_FUNCTIONS_FOLDER, INTERNAL_EDGE_FUNCTIONS_FOLDER, 'netlify.toml'].map((name) => { + const fullPath = path.resolve(basePath, getPathInProject([name])) + + return fs.rm(fullPath, { force: true, recursive: true }) + }) + + await Promise.all(ops) +} + +const getBuildOptions = ({ + cachedConfig, + options: { configPath, context, cwd = process.cwd(), debug, dry, offline, quiet, saveConfig }, +}) => ({ + cachedConfig, + configPath, + siteId: cachedConfig.siteInfo.id, + token: cachedConfig.token, + dry, + debug, + context, + mode: 'cli', + telemetry: false, + buffer: false, + offline, + cwd, + quiet, + saveConfig, +}) + +const runNetlifyBuild = async ({ cachedConfig, options, settings, site, timeline = 'build' }) => { + const { default: buildSite, startDev } = await netlifyBuildPromise + const sharedOptions = getBuildOptions({ + cachedConfig, + options, + }) + const devCommand = async (settingsOverrides = {}) => { + const { ipVersion } = await startFrameworkServer({ + settings: { + ...settings, + ...settingsOverrides, + }, + }) + + settings.frameworkHost = ipVersion === 6 ? '::1' : '127.0.0.1' + } + + if (timeline === 'build') { + // Start by cleaning the internal directory, as it may have artifacts left + // by previous builds. + await cleanInternalDirectory(site.root) + + // Copy `netlify.toml` into the internal directory. This will be the new + // location of the config file for the duration of the command. + const tempConfigPath = await copyConfig({ configPath: cachedConfig.configPath, siteRoot: site.root }) + const buildSiteOptions = { + ...sharedOptions, + outputConfigPath: tempConfigPath, + saveConfig: true, + } + + // Run Netlify Build using the main entry point. + await buildSite(buildSiteOptions) + + // Start the dev server, forcing the usage of a static server as opposed to + // the framework server. + await devCommand({ + command: undefined, + useStaticServer: true, + }) + + return { configPath: tempConfigPath } + } + + const startDevOptions = { + ...sharedOptions, + + // Set `quiet` to suppress non-essential output from Netlify Build unless + // the `debug` flag is set. + quiet: !options.debug, + } + + // Run Netlify Build using the `startDev` entry point. + const { error: startDevError, success } = await startDev(devCommand, startDevOptions) + + if (!success) { + error(`Could not start local development server\n\n${startDevError.message}\n\n${startDevError.stack}`) + } + + return {} +} + +export const runDevTimeline = (options) => runNetlifyBuild({ ...options, timeline: 'dev' }) + +export const runBuildTimeline = (options) => runNetlifyBuild({ ...options, timeline: 'build' }) diff --git a/src/utils/shell.mjs b/src/utils/shell.mjs new file mode 100644 index 00000000000..f765cb7f0ed --- /dev/null +++ b/src/utils/shell.mjs @@ -0,0 +1,120 @@ +// @ts-check +import process from 'process' + +import execa from 'execa' +import stripAnsiCc from 'strip-ansi-control-characters' + +import { chalk, log, NETLIFYDEVERR, NETLIFYDEVWARN } from './command-helpers.mjs' +import { processOnExit } from './dev.mjs' + +/** + * @type {(() => Promise)[]} - array of functions to run before the process exits + */ +const cleanupWork = [] + +let cleanupStarted = false + +/** + * @param {() => Promise} job + */ +export const addCleanupJob = (job) => { + cleanupWork.push(job) +} + +/** + * @param {object} input + * @param {number=} input.exitCode The exit code to return when exiting the process after cleanup + */ +const cleanupBeforeExit = async ({ exitCode }) => { + // If cleanup has started, then wherever started it will be responsible for exiting + if (!cleanupStarted) { + cleanupStarted = true + try { + await Promise.all(cleanupWork.map((cleanup) => cleanup())) + } finally { + process.exit(exitCode) + } + } +} + +/** + * Run a command and pipe stdout, stderr and stdin + * @param {string} command + * @param {NodeJS.ProcessEnv} env + * @returns {execa.ExecaChildProcess} + */ +export const runCommand = (command, env = {}, spinner = null) => { + const commandProcess = execa.command(command, { + preferLocal: true, + // we use reject=false to avoid rejecting synchronously when the command doesn't exist + reject: false, + env, + // windowsHide needs to be false for child process to terminate properly on Windows + windowsHide: false, + }) + + // This ensures that an active spinner stays at the bottom of the commandline + // even though the actual framework command might be outputting stuff + const pipeDataWithSpinner = (writeStream, chunk) => { + if (spinner && spinner.isSpinning) { + spinner.clear() + spinner.isSilent = true + } + writeStream.write(chunk, () => { + if (spinner && spinner.isSpinning) { + spinner.isSilent = false + spinner.render() + } + }) + } + + commandProcess.stdout.pipe(stripAnsiCc.stream()).on('data', pipeDataWithSpinner.bind(null, process.stdout)) + commandProcess.stderr.pipe(stripAnsiCc.stream()).on('data', pipeDataWithSpinner.bind(null, process.stderr)) + process.stdin.pipe(commandProcess.stdin) + + // we can't try->await->catch since we don't want to block on the framework server which + // is a long running process + // eslint-disable-next-line promise/catch-or-return + commandProcess + // eslint-disable-next-line promise/prefer-await-to-then + .then(async () => { + const result = await commandProcess + const [commandWithoutArgs] = command.split(' ') + if (result.failed && isNonExistingCommandError({ command: commandWithoutArgs, error: result })) { + log( + NETLIFYDEVERR, + `Failed running command: ${command}. Please verify ${chalk.magenta(`'${commandWithoutArgs}'`)} exists`, + ) + } else { + const errorMessage = result.failed + ? `${NETLIFYDEVERR} ${result.shortMessage}` + : `${NETLIFYDEVWARN} "${command}" exited with code ${result.exitCode}` + + log(`${errorMessage}. Shutting down Netlify Dev server`) + } + + return await cleanupBeforeExit({ exitCode: 1 }) + }) + processOnExit(async () => await cleanupBeforeExit({})) + + return commandProcess +} + +const isNonExistingCommandError = ({ command, error: commandError }) => { + // `ENOENT` is only returned for non Windows systems + // See https://github.com/sindresorhus/execa/pull/447 + if (commandError.code === 'ENOENT') { + return true + } + + // if the command is a package manager we let it report the error + if (['yarn', 'npm'].includes(command)) { + return false + } + + // this only works on English versions of Windows + return ( + typeof commandError.message === 'string' && + commandError.message.includes('is not recognized as an internal or external command') + ) +} diff --git a/src/utils/static-server.mjs b/src/utils/static-server.mjs new file mode 100644 index 00000000000..cc42937485a --- /dev/null +++ b/src/utils/static-server.mjs @@ -0,0 +1,34 @@ +// @ts-check +import path from 'path' + +import fastifyStatic from '@fastify/static' +import Fastify from 'fastify' + +import { log, NETLIFYDEVLOG } from './command-helpers.mjs' + +export const startStaticServer = async ({ settings }) => { + const server = Fastify() + const rootPath = path.resolve(settings.dist) + server.register(fastifyStatic, { + root: rootPath, + etag: false, + acceptRanges: false, + lastModified: false, + }) + + server.setNotFoundHandler((_req, res) => { + res.code(404).sendFile('404.html', rootPath) + }) + + server.addHook('onRequest', (req, reply, done) => { + reply.header('X-Powered-by', 'netlify-dev') + const validMethods = ['GET', 'HEAD'] + if (!validMethods.includes(req.method)) { + reply.code(405).send('Method Not Allowed') + } + done() + }) + + await server.listen({ port: settings.frameworkPort }) + log(`\n${NETLIFYDEVLOG} Static server listening to`, settings.frameworkPort) +} diff --git a/src/utils/validation.mjs b/src/utils/validation.mjs new file mode 100644 index 00000000000..cd64d9fe794 --- /dev/null +++ b/src/utils/validation.mjs @@ -0,0 +1,15 @@ +// @ts-check +import { BANG, chalk } from './command-helpers.mjs' + +export const getGeoCountryArgParser = (exampleCommand) => (arg) => { + // Validate that the arg passed is two letters only for country + // See https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes + if (!/^[a-z]{2}$/i.test(arg)) { + throw new Error( + `The geo country code must use a two letter abbreviation. + ${chalk.red(BANG)} Example: + ${exampleCommand}`, + ) + } + return arg.toUpperCase() +} diff --git a/tests/integration/600.framework-detection.test.cjs b/tests/integration/600.framework-detection.test.cjs index c220ddd6754..c4ddf3b6dbb 100644 --- a/tests/integration/600.framework-detection.test.cjs +++ b/tests/integration/600.framework-detection.test.cjs @@ -372,11 +372,14 @@ test('should run and serve a production build when the `--prod` flag is set', as }) .buildAsync() - await withDevServer({ cwd: builder.directory, context: null, debug: true, prod: true }, async ({ output, url }) => { - const response = await got(`${url}/hello`).json() - t.deepEqual(response, { CONTEXT_CHECK: 'PRODUCTION' }) + await withDevServer( + { cwd: builder.directory, context: null, debug: true, serve: true }, + async ({ output, url }) => { + const response = await got(`${url}/hello`).json() + t.deepEqual(response, { CONTEXT_CHECK: 'PRODUCTION' }) - t.snapshot(normalize(output, { duration: true, filePath: true })) - }) + t.snapshot(normalize(output, { duration: true, filePath: true })) + }, + ) }) }) diff --git a/tests/integration/snapshots/600.framework-detection.test.cjs.md b/tests/integration/snapshots/600.framework-detection.test.cjs.md index 626a9b17c4d..03a5ad30c3e 100644 --- a/tests/integration/snapshots/600.framework-detection.test.cjs.md +++ b/tests/integration/snapshots/600.framework-detection.test.cjs.md @@ -242,8 +242,7 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 - `◈ Netlify Dev ◈␊ - ◈ Injected netlify.toml file env var: CONTEXT_CHECK␊ + `◈ Injected netlify.toml file env var: CONTEXT_CHECK␊ ◈ Using simple static server because '[dev.framework]' was set to '#static'␊ ◈ Running static server from "site-with-framework/public"␊ ◈ Building site for production␊ diff --git a/tests/integration/snapshots/600.framework-detection.test.cjs.snap b/tests/integration/snapshots/600.framework-detection.test.cjs.snap index 97490bb2fa7419083592d4c303bf264dc3aeaf13..738258628a62f0c1b8ac66f0155ef7aec79940b3 100644 GIT binary patch delta 1982 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 VMcK=aP&r=aH0019<-Yx(D diff --git a/tests/integration/utils/dev-server.cjs b/tests/integration/utils/dev-server.cjs index 9299997b879..2925d2330ab 100644 --- a/tests/integration/utils/dev-server.cjs +++ b/tests/integration/utils/dev-server.cjs @@ -29,7 +29,7 @@ const startServer = async ({ env = {}, args = [], expectFailure = false, - prod = false, + serve = false, prompt, }) => { const port = await getPort() @@ -39,15 +39,8 @@ const startServer = async ({ console.log(`Starting dev server on port: ${port} in directory ${path.basename(cwd)}`) - const baseArgs = [ - 'dev', - offline ? '--offline' : '', - '-p', - port, - '--staticServerPort', - staticPort, - prod ? '--prod' : '', - ] + const baseCommand = serve ? 'serve' : 'dev' + const baseArgs = [offline ? '--offline' : '', '-p', port, '--staticServerPort', staticPort] // We use `null` to override the default context and actually omit the flag // from the command, which is useful in some test scenarios. @@ -55,7 +48,7 @@ const startServer = async ({ baseArgs.push('--context', context) } - const ps = execa(cliPath, [...baseArgs, ...args], getExecaOptions({ cwd, env })) + const ps = execa(cliPath, [baseCommand, ...baseArgs, ...args], getExecaOptions({ cwd, env })) if (process.env.DEBUG_TESTS) { ps.stderr.pipe(process.stderr) From 4739d22f811fe060f5d818e80db08b6453c81951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Fri, 6 Jan 2023 16:58:23 +0000 Subject: [PATCH 26/43] Update src/commands/serve/serve.mjs Co-authored-by: Matt Kane --- src/commands/serve/serve.mjs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/commands/serve/serve.mjs b/src/commands/serve/serve.mjs index 8ad1803e734..74cbe171cfa 100644 --- a/src/commands/serve/serve.mjs +++ b/src/commands/serve/serve.mjs @@ -35,19 +35,17 @@ const serve = async (options, command) => { config.build = { ...config.build } /** @type {import('../dev/types').DevConfig} */ const devConfig = { - framework: '#auto', ...(config.functionsDirectory && { functions: config.functionsDirectory }), ...(config.build.publish && { publish: config.build.publish }), ...config.dev, ...options, + // Override the `framework` value so that we start a static server and not + // the framework's development server. + framework: '#static', } let { env } = cachedConfig - // Override the `framework` value so that we start a static server and not - // the framework's development server. - devConfig.framework = '#static' - if (!options.offline && siteInfo.use_envelope) { env = await getEnvelopeEnv({ api, context: options.context, env, siteInfo }) log(`${NETLIFYDEVLOG} Injecting environment variable values for ${chalk.yellow('all scopes')}`) From 42ae1bbd50b6a8e2866e1a1d65ad713e97b595aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Fri, 6 Jan 2023 16:59:34 +0000 Subject: [PATCH 27/43] refactor: remove unused flags --- src/commands/serve/serve.mjs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/commands/serve/serve.mjs b/src/commands/serve/serve.mjs index 74cbe171cfa..f94471d4cf2 100644 --- a/src/commands/serve/serve.mjs +++ b/src/commands/serve/serve.mjs @@ -40,7 +40,7 @@ const serve = async (options, command) => { ...config.dev, ...options, // Override the `framework` value so that we start a static server and not - // the framework's development server. + // the framework's development server. framework: '#static', } @@ -151,8 +151,6 @@ export const createServeCommand = (program) => normalizeContext, ) .option('-p ,--port ', 'port of netlify dev', (value) => Number.parseInt(value)) - .option('--targetPort ', 'port of target app server', (value) => Number.parseInt(value)) - .option('--framework ', 'framework to use. Defaults to #auto which automatically detects a framework') .option('-d ,--dir ', 'dir with static files') .option('-f ,--functions ', 'specify a functions folder to serve') .option('-o ,--offline', 'disables any features that require network access') From f4f31fb973e07d942454b625d34dd3022425081d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Fri, 6 Jan 2023 17:07:31 +0000 Subject: [PATCH 28/43] refactor: abort server when build fails --- src/utils/run-build.mjs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/utils/run-build.mjs b/src/utils/run-build.mjs index 14a190ae7ae..fe7d43390ac 100644 --- a/src/utils/run-build.mjs +++ b/src/utils/run-build.mjs @@ -88,7 +88,13 @@ const runNetlifyBuild = async ({ cachedConfig, options, settings, site, timeline } // Run Netlify Build using the main entry point. - await buildSite(buildSiteOptions) + const { success } = await buildSite(buildSiteOptions) + + if (!success) { + error('Could not start local server due to a build error') + + return {} + } // Start the dev server, forcing the usage of a static server as opposed to // the framework server. From 28bd2941f334acd4e9217a6f6fab1e3f32415934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 9 Jan 2023 12:10:34 +0000 Subject: [PATCH 29/43] refactor: always start functions server --- src/utils/detect-server-settings.mjs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/utils/detect-server-settings.mjs b/src/utils/detect-server-settings.mjs index c69ba210bfb..781852c277e 100644 --- a/src/utils/detect-server-settings.mjs +++ b/src/utils/detect-server-settings.mjs @@ -11,7 +11,6 @@ import isPlainObject from 'is-plain-obj' import { NETLIFYDEVWARN, chalk, log } from './command-helpers.mjs' import { acquirePort } from './dev.mjs' -import { getInternalFunctionsDir } from './functions/index.mjs' const formatProperty = (str) => chalk.magenta(`'${str}'`) const formatValue = (str) => chalk.green(`'${str}'`) @@ -328,8 +327,7 @@ const detectServerSettings = async (devConfig, options, projectDir) => { errorMessage: `Could not acquire required ${formatProperty('port')}`, }) const functionsDir = devConfig.functions || settings.functions - const internalFunctionsDir = await getInternalFunctionsDir({ base: projectDir }) - const shouldStartFunctionsServer = Boolean(functionsDir || internalFunctionsDir) + const functionsPort = await getPort({ port: devConfig.functionsPort || 0 }) return { ...settings, @@ -337,7 +335,7 @@ const detectServerSettings = async (devConfig, options, projectDir) => { jwtSecret: devConfig.jwtSecret || 'secret', jwtRolePath: devConfig.jwtRolePath || 'app_metadata.authorization.roles', functions: functionsDir, - ...(shouldStartFunctionsServer && { functionsPort: await getPort({ port: devConfig.functionsPort || 0 }) }), + functionsPort, ...(devConfig.https && { https: await readHttpsSettings(devConfig.https) }), } } From dbd9dcc7b83890ef04ffa0ba14e288e00d70071f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 9 Jan 2023 12:33:25 +0000 Subject: [PATCH 30/43] chore: remove ESLint directive --- .../commands/functions-invoke/functions-invoke.test.mjs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/integration/commands/functions-invoke/functions-invoke.test.mjs b/tests/integration/commands/functions-invoke/functions-invoke.test.mjs index 67e4d8c010b..20423c1bdee 100644 --- a/tests/integration/commands/functions-invoke/functions-invoke.test.mjs +++ b/tests/integration/commands/functions-invoke/functions-invoke.test.mjs @@ -1,4 +1,3 @@ -/* eslint-disable require-await */ import { describe, expect, test } from 'vitest' import callCli from '../../utils/call-cli.cjs' @@ -161,4 +160,3 @@ describe('functions:invoke command', () => { }) }) }) -/* eslint-enable require-await */ From bc5c59d2ee8af455c014852681d9eb238426b703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 9 Jan 2023 12:37:19 +0000 Subject: [PATCH 31/43] fix: fix imports --- src/commands/dev/dev.mjs | 2 +- src/lib/functions/registry.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index ade75172ff3..b57cf8a377d 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -28,7 +28,7 @@ import { getNetlifyGraphConfig, readGraphQLOperationsSourceFile, } from '../../lib/one-graph/cli-netlify-graph.mjs' -import { getPathInProject } from '../../lib/settings.cjs' +import { getPathInProject } from '../../lib/settings.mjs' import { startSpinner, stopSpinner } from '../../lib/spinner.mjs' import { BANG, diff --git a/src/lib/functions/registry.mjs b/src/lib/functions/registry.mjs index cdc26afca4c..e5e46c0b6ab 100644 --- a/src/lib/functions/registry.mjs +++ b/src/lib/functions/registry.mjs @@ -16,7 +16,7 @@ import { } from '../../utils/command-helpers.mjs' import { SERVE_FUNCTIONS_FOLDER } from '../../utils/functions/functions.mjs' import { getLogMessage } from '../log.mjs' -import { getPathInProject } from '../settings.cjs' +import { getPathInProject } from '../settings.mjs' import NetlifyFunction from './netlify-function.mjs' import runtimes from './runtimes/index.mjs' From 496783a9b9ee69d6f7d48526fc88fe9a1d3f8f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 9 Jan 2023 12:40:35 +0000 Subject: [PATCH 32/43] refactor: always start functions server --- src/utils/detect-server-settings.mjs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/utils/detect-server-settings.mjs b/src/utils/detect-server-settings.mjs index 1e8c8b073e2..97796374c00 100644 --- a/src/utils/detect-server-settings.mjs +++ b/src/utils/detect-server-settings.mjs @@ -11,7 +11,6 @@ import isPlainObject from 'is-plain-obj' import { NETLIFYDEVWARN, chalk, log } from './command-helpers.mjs' import { acquirePort } from './dev.mjs' -import { getInternalFunctionsDir } from './functions/index.mjs' const formatProperty = (str) => chalk.magenta(`'${str}'`) const formatValue = (str) => chalk.green(`'${str}'`) @@ -328,8 +327,7 @@ const detectServerSettings = async (devConfig, options, projectDir) => { errorMessage: `Could not acquire required ${formatProperty('port')}`, }) const functionsDir = devConfig.functions || settings.functions - const internalFunctionsDir = await getInternalFunctionsDir({ base: projectDir }) - const shouldStartFunctionsServer = Boolean(functionsDir || internalFunctionsDir) + const functionsPort = await getPort({ port: devConfig.functionsPort || 0 }) return { ...settings, @@ -337,7 +335,7 @@ const detectServerSettings = async (devConfig, options, projectDir) => { jwtSecret: devConfig.jwtSecret || 'secret', jwtRolePath: devConfig.jwtRolePath || 'app_metadata.authorization.roles', functions: functionsDir, - ...(shouldStartFunctionsServer && { functionsPort: await getPort({ port: devConfig.functionsPort || 0 }) }), + functionsPort, ...(devConfig.https && { https: await readHttpsSettings(devConfig.https) }), } } From a92f755757d80bc9535f92f6e3d10faca3782b58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 9 Jan 2023 13:54:28 +0000 Subject: [PATCH 33/43] refactor: ensure internal functions directory --- src/commands/dev/dev.mjs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index b57cf8a377d..c56db4bc41a 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -460,9 +460,15 @@ const dev = async (options, command) => { let { env } = cachedConfig - // Add `NETLIFY_DEV` to the environment variables unless `prod` is set. if (!options.prod) { + // Add `NETLIFY_DEV` to the environment variables. env.NETLIFY_DEV = { sources: ['internal'], value: 'true' } + + // Ensure the internal functions directory exists so that any functions + // created by Netlify Build are loaded. + const fullPath = path.resolve(site.root, INTERNAL_FUNCTIONS_FOLDER) + + await fs.mkdir(fullPath, { recursive: true }) } // If the `prod` flag is present, we override the `framework` value so that From af007580b4b49c99444ea4413c8f502921c68df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 9 Jan 2023 13:59:56 +0000 Subject: [PATCH 34/43] refactor: reinstate functions server check --- src/utils/detect-server-settings.mjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/utils/detect-server-settings.mjs b/src/utils/detect-server-settings.mjs index 97796374c00..1e8c8b073e2 100644 --- a/src/utils/detect-server-settings.mjs +++ b/src/utils/detect-server-settings.mjs @@ -11,6 +11,7 @@ import isPlainObject from 'is-plain-obj' import { NETLIFYDEVWARN, chalk, log } from './command-helpers.mjs' import { acquirePort } from './dev.mjs' +import { getInternalFunctionsDir } from './functions/index.mjs' const formatProperty = (str) => chalk.magenta(`'${str}'`) const formatValue = (str) => chalk.green(`'${str}'`) @@ -327,7 +328,8 @@ const detectServerSettings = async (devConfig, options, projectDir) => { errorMessage: `Could not acquire required ${formatProperty('port')}`, }) const functionsDir = devConfig.functions || settings.functions - const functionsPort = await getPort({ port: devConfig.functionsPort || 0 }) + const internalFunctionsDir = await getInternalFunctionsDir({ base: projectDir }) + const shouldStartFunctionsServer = Boolean(functionsDir || internalFunctionsDir) return { ...settings, @@ -335,7 +337,7 @@ const detectServerSettings = async (devConfig, options, projectDir) => { jwtSecret: devConfig.jwtSecret || 'secret', jwtRolePath: devConfig.jwtRolePath || 'app_metadata.authorization.roles', functions: functionsDir, - functionsPort, + ...(shouldStartFunctionsServer && { functionsPort: await getPort({ port: devConfig.functionsPort || 0 }) }), ...(devConfig.https && { https: await readHttpsSettings(devConfig.https) }), } } From e3575986132a83ddec8f7c7cea21d0bdfe72b888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 9 Jan 2023 15:04:32 +0000 Subject: [PATCH 35/43] refactor: add missing import --- src/commands/dev/dev.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index e83f0a45f1f..d653eb667f8 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -5,6 +5,7 @@ import { Option } from 'commander' import { promptEditorHelper } from '../../lib/edge-functions/editor-helper.mjs' import { startFunctionsServer } from '../../lib/functions/server.mjs' +import { printBanner } from '../../utils/banner.mjs' import { BANG, chalk, From 66dc887572ad213bdb73435ea8c617fabb05d304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 9 Jan 2023 15:06:06 +0000 Subject: [PATCH 36/43] fix: fix imports --- src/utils/framework-server.mjs | 2 +- src/utils/run-build.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/framework-server.mjs b/src/utils/framework-server.mjs index 59fc5a74dd0..698f5fcc60c 100644 --- a/src/utils/framework-server.mjs +++ b/src/utils/framework-server.mjs @@ -1,7 +1,7 @@ // @ts-check import waitPort from 'wait-port' -import { startSpinner, stopSpinner } from '../lib/spinner.cjs' +import { startSpinner, stopSpinner } from '../lib/spinner.mjs' import { error, exit, log, NETLIFYDEVERR, NETLIFYDEVLOG } from './command-helpers.mjs' import { runCommand } from './shell.mjs' diff --git a/src/utils/run-build.mjs b/src/utils/run-build.mjs index fe7d43390ac..085d3030a5d 100644 --- a/src/utils/run-build.mjs +++ b/src/utils/run-build.mjs @@ -4,7 +4,7 @@ import path from 'path' import process from 'process' import { INTERNAL_EDGE_FUNCTIONS_FOLDER } from '../lib/edge-functions/consts.mjs' -import { getPathInProject } from '../lib/settings.cjs' +import { getPathInProject } from '../lib/settings.mjs' import { error } from './command-helpers.mjs' import { startFrameworkServer } from './framework-server.mjs' From f28d1642b58db48481ad6b77faaa0a12bd469f15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 9 Jan 2023 15:24:35 +0000 Subject: [PATCH 37/43] refactor: move logic for ensuring internal functions directory --- src/commands/serve/serve.mjs | 6 ++++++ src/utils/detect-server-settings.mjs | 6 ++++-- src/utils/functions/functions.mjs | 8 +++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/commands/serve/serve.mjs b/src/commands/serve/serve.mjs index f94471d4cf2..9100f08c9d1 100644 --- a/src/commands/serve/serve.mjs +++ b/src/commands/serve/serve.mjs @@ -18,6 +18,7 @@ import { import detectServerSettings, { getConfigWithPlugins } from '../../utils/detect-server-settings.mjs' import { getSiteInformation, injectEnvVariables } from '../../utils/dev.mjs' import { getEnvelopeEnv, normalizeContext } from '../../utils/env/index.mjs' +import { getInternalFunctionsDir } from '../../utils/functions/functions.mjs' import { ensureNetlifyIgnore } from '../../utils/gitignore.mjs' import openBrowser from '../../utils/open-browser.mjs' import { generateInspectSettings, startProxyServer } from '../../utils/proxy-server.mjs' @@ -62,6 +63,11 @@ const serve = async (options, command) => { siteInfo, }) + // Ensure the internal functions directory exists so that the functions + // server and registry are initialized, and any functions created by + // Netlify Build are loaded. + await getInternalFunctionsDir({ base: site.root, ensureExists: true }) + /** @type {Partial} */ let settings = {} try { diff --git a/src/utils/detect-server-settings.mjs b/src/utils/detect-server-settings.mjs index 781852c277e..323bb252947 100644 --- a/src/utils/detect-server-settings.mjs +++ b/src/utils/detect-server-settings.mjs @@ -11,6 +11,7 @@ import isPlainObject from 'is-plain-obj' import { NETLIFYDEVWARN, chalk, log } from './command-helpers.mjs' import { acquirePort } from './dev.mjs' +import { getInternalFunctionsDir } from './functions/functions.mjs' const formatProperty = (str) => chalk.magenta(`'${str}'`) const formatValue = (str) => chalk.green(`'${str}'`) @@ -327,7 +328,8 @@ const detectServerSettings = async (devConfig, options, projectDir) => { errorMessage: `Could not acquire required ${formatProperty('port')}`, }) const functionsDir = devConfig.functions || settings.functions - const functionsPort = await getPort({ port: devConfig.functionsPort || 0 }) + const internalFunctionsDir = await getInternalFunctionsDir({ base: projectDir }) + const shouldStartFunctionsServer = Boolean(functionsDir || internalFunctionsDir) return { ...settings, @@ -335,7 +337,7 @@ const detectServerSettings = async (devConfig, options, projectDir) => { jwtSecret: devConfig.jwtSecret || 'secret', jwtRolePath: devConfig.jwtRolePath || 'app_metadata.authorization.roles', functions: functionsDir, - functionsPort, + ...(shouldStartFunctionsServer && { functionsPort: await getPort({ port: devConfig.functionsPort || 0 }) }), ...(devConfig.https && { https: await readHttpsSettings(devConfig.https) }), } } diff --git a/src/utils/functions/functions.mjs b/src/utils/functions/functions.mjs index 006e8056797..4324ff65be5 100644 --- a/src/utils/functions/functions.mjs +++ b/src/utils/functions/functions.mjs @@ -1,4 +1,5 @@ // @ts-check +import { promises as fs } from 'fs' import { resolve } from 'path' import { isDirectoryAsync, isFileAsync } from '../../lib/fs.mjs' @@ -36,8 +37,13 @@ export const getFunctionsDistPath = async ({ base }) => { return isDirectory ? path : null } -export const getInternalFunctionsDir = async ({ base }) => { +export const getInternalFunctionsDir = async ({ base, ensureExists }) => { const path = resolve(base, getPathInProject([INTERNAL_FUNCTIONS_FOLDER])) + + if (ensureExists) { + await fs.mkdir(path, { recursive: true }) + } + const isDirectory = await isDirectoryAsync(path) return isDirectory ? path : null From 73068539bae761000e20d940c3f174a03ce6785a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 9 Jan 2023 15:30:44 +0000 Subject: [PATCH 38/43] chore: fix test --- src/commands/dev/dev.mjs | 1 - .../600.framework-detection.test.cjs | 2 +- .../600.framework-detection.test.cjs.md | 68 ++++++++++++++++++ .../600.framework-detection.test.cjs.snap | Bin 2103 -> 2151 bytes tests/integration/utils/dev-server.cjs | 13 +--- 5 files changed, 72 insertions(+), 12 deletions(-) diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index 36a0b0051c8..d653eb667f8 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -226,7 +226,6 @@ export const createDevCommand = (program) => { .option('-o ,--offline', 'disables any features that require network access') .option('-l, --live', 'start a public live session', false) .option('--functionsPort ', 'port of functions server', (value) => Number.parseInt(value)) - .option('-P, --prod', '(Beta) build the site for production and serve locally', false) .addOption( new Option( '--geo ', diff --git a/tests/integration/600.framework-detection.test.cjs b/tests/integration/600.framework-detection.test.cjs index c4ddf3b6dbb..e443463f237 100644 --- a/tests/integration/600.framework-detection.test.cjs +++ b/tests/integration/600.framework-detection.test.cjs @@ -333,7 +333,7 @@ test('should start static service for frameworks without port, detected framewor }) }) -test('should run and serve a production build when the `--prod` flag is set', async (t) => { +test('should run and serve a production build when using the `serve` command', async (t) => { await withSiteBuilder('site-with-framework', async (builder) => { await builder .withNetlifyToml({ diff --git a/tests/integration/snapshots/600.framework-detection.test.cjs.md b/tests/integration/snapshots/600.framework-detection.test.cjs.md index 03a5ad30c3e..299b753d671 100644 --- a/tests/integration/snapshots/600.framework-detection.test.cjs.md +++ b/tests/integration/snapshots/600.framework-detection.test.cjs.md @@ -238,6 +238,74 @@ Generated by [AVA](https://avajs.dev). 14␊ ◈ "npm run dev" exited with code *. Shutting down Netlify Dev server` +## should run and serve a production build when using the `serve` command + +> Snapshot 1 + + `◈ Injected netlify.toml file env var: CONTEXT_CHECK␊ + ◈ Using simple static server because '[dev.framework]' was set to '#static'␊ + ◈ Running static server from "site-with-framework/public"␊ + ◈ Building site for production␊ + ​␊ + Netlify Build ␊ + ────────────────────────────────────────────────────────────────␊ + ​␊ + > Version␊ + @netlify/build 0.0.0␊ + ​␊ + > Flags␊ + offline: true␊ + outputConfigPath:/file/path␊ + ​␊ + > Current directory␊ + /file/path␊ + ​␊ + > Config file␊ + /file/path␊ + ​␊ + > Context␊ + production␊ + ​␊ + > Loading plugins␊ + -/file/path from netlify.toml␊ + ​␊ + 1./file/path (onPreBuild event) ␊ + ────────────────────────────────────────────────────────────────␊ + ​␊ + Netlify configuration property "redirects" value changed to [ { from: /file/path', to: /file/path' } ].␊ + ​␊ + /file/path onPreBuild completed in Xms)␊ + ​␊ + 2. Functions bundling ␊ + ────────────────────────────────────────────────────────────────␊ + ​␊ + Packaging Functions from new_functions directory:␊ + - hello.js␊ + ​␊ + ​␊ + (Functions bundling completed in Xms)␊ + ​␊ + 3. Save deploy artifacts ␊ + ────────────────────────────────────────────────────────────────␊ + ​␊ + ​␊ + (Save deploy artifacts completed in Xms)␊ + ​␊ + Netlify Build Complete ␊ + ────────────────────────────────────────────────────────────────␊ + ​␊ + (Netlify Build completed in Xms)␊ + ␊ + ◈ Static server listening to 88888␊ + ◈ Loaded function hello http://localhost:88888/.netlify/functions/hello.␊ + ◈ Functions server is listening on 88888␊ + ␊ + ┌──────────────────────────────────────────────────┐␊ + │ │␊ + │ ◈ Server now ready on http://localhost:88888 │␊ + │ │␊ + └──────────────────────────────────────────────────┘` + ## should run and serve a production build when the `--prod` flag is set > Snapshot 1 diff --git a/tests/integration/snapshots/600.framework-detection.test.cjs.snap b/tests/integration/snapshots/600.framework-detection.test.cjs.snap index 738258628a62f0c1b8ac66f0155ef7aec79940b3..568785f39f0bf18fd477f774bbb15216fae40d5b 100644 GIT binary patch literal 2151 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 h5U>Y~W$e2pvE-Soin5p8$IJ0042E4(b2^ diff --git a/tests/integration/utils/dev-server.cjs b/tests/integration/utils/dev-server.cjs index 9299997b879..bc6fff19188 100644 --- a/tests/integration/utils/dev-server.cjs +++ b/tests/integration/utils/dev-server.cjs @@ -29,7 +29,7 @@ const startServer = async ({ env = {}, args = [], expectFailure = false, - prod = false, + serve = false, prompt, }) => { const port = await getPort() @@ -39,15 +39,8 @@ const startServer = async ({ console.log(`Starting dev server on port: ${port} in directory ${path.basename(cwd)}`) - const baseArgs = [ - 'dev', - offline ? '--offline' : '', - '-p', - port, - '--staticServerPort', - staticPort, - prod ? '--prod' : '', - ] + const baseCommand = serve ? 'serve' : 'dev' + const baseArgs = [baseCommand, offline ? '--offline' : '', '-p', port, '--staticServerPort', staticPort] // We use `null` to override the default context and actually omit the flag // from the command, which is useful in some test scenarios. From 4c55658ba02fe0494f3dcd769223cd66b7955a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 9 Jan 2023 15:31:05 +0000 Subject: [PATCH 39/43] chore: update docs --- README.md | 5 +++++ docs/README.md | 4 ++++ docs/commands/dev.md | 1 - docs/commands/index.md | 4 ++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 89e4cc25907..baf228a04a0 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ See the [CLI command line reference](https://cli.netlify.com/commands/) to get s - [login](#login) - [open](#open) - [recipes](#recipes) + - [serve](#serve) - [sites](#sites) - [status](#status) - [switch](#switch) @@ -219,6 +220,10 @@ Open settings for the site linked to the current folder | [`recipes:list`](/docs/commands/recipes.md#recipeslist) | (Beta) List the recipes available to create and modify files in a project | +### [serve](/docs/commands/serve.md) + +(Beta) build the site for production and serve locally + ### [sites](/docs/commands/sites.md) Handle various site operations diff --git a/docs/README.md b/docs/README.md index 754bb421184..922a2b95757 100644 --- a/docs/README.md +++ b/docs/README.md @@ -168,6 +168,10 @@ Open settings for the site linked to the current folder | [`recipes:list`](/docs/commands/recipes.md#recipeslist) | (Beta) List the recipes available to create and modify files in a project | +### [serve](/docs/commands/serve.md) + +(Beta) build the site for production and serve locally + ### [sites](/docs/commands/sites.md) Handle various site operations diff --git a/docs/commands/dev.md b/docs/commands/dev.md index b1b1f6f288f..637361953c4 100644 --- a/docs/commands/dev.md +++ b/docs/commands/dev.md @@ -30,7 +30,6 @@ netlify dev - `live` (*boolean*) - start a public live session - `offline` (*boolean*) - disables any features that require network access - `port` (*string*) - port of netlify dev -- `prod` (*boolean*) - (Beta) build the site for production and serve locally - `sessionId` (*string*) - (Graph) connect to cloud session with ID [sessionId] - `targetPort` (*string*) - port of target app server - `debug` (*boolean*) - Print debugging information diff --git a/docs/commands/index.md b/docs/commands/index.md index 3283866d71d..99acd94f726 100644 --- a/docs/commands/index.md +++ b/docs/commands/index.md @@ -147,6 +147,10 @@ Open settings for the site linked to the current folder | [`recipes:list`](/docs/commands/recipes.md#recipeslist) | (Beta) List the recipes available to create and modify files in a project | +### [serve](/docs/commands/serve.md) + +(Beta) build the site for production and serve locally + ### [sites](/docs/commands/sites.md) Handle various site operations From 96e79a1ff6d93084a75678c05b5201a9d4f4cea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 9 Jan 2023 15:40:25 +0000 Subject: [PATCH 40/43] chore: update snapshot --- .../600.framework-detection.test.cjs.md | 68 ------------------ .../600.framework-detection.test.cjs.snap | Bin 2151 -> 2097 bytes 2 files changed, 68 deletions(-) diff --git a/tests/integration/snapshots/600.framework-detection.test.cjs.md b/tests/integration/snapshots/600.framework-detection.test.cjs.md index 299b753d671..47eb6d26b47 100644 --- a/tests/integration/snapshots/600.framework-detection.test.cjs.md +++ b/tests/integration/snapshots/600.framework-detection.test.cjs.md @@ -305,71 +305,3 @@ Generated by [AVA](https://avajs.dev). │ ◈ Server now ready on http://localhost:88888 │␊ │ │␊ └──────────────────────────────────────────────────┘` - -## should run and serve a production build when the `--prod` flag is set - -> Snapshot 1 - - `◈ Injected netlify.toml file env var: CONTEXT_CHECK␊ - ◈ Using simple static server because '[dev.framework]' was set to '#static'␊ - ◈ Running static server from "site-with-framework/public"␊ - ◈ Building site for production␊ - ​␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - > Version␊ - @netlify/build 0.0.0␊ - ​␊ - > Flags␊ - offline: true␊ - outputConfigPath:/file/path␊ - ​␊ - > Current directory␊ - /file/path␊ - ​␊ - > Config file␊ - /file/path␊ - ​␊ - > Context␊ - production␊ - ​␊ - > Loading plugins␊ - -/file/path from netlify.toml␊ - ​␊ - 1./file/path (onPreBuild event) ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - Netlify configuration property "redirects" value changed to [ { from: /file/path', to: /file/path' } ].␊ - ​␊ - /file/path onPreBuild completed in Xms)␊ - ​␊ - 2. Functions bundling ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - Packaging Functions from new_functions directory:␊ - - hello.js␊ - ​␊ - ​␊ - (Functions bundling completed in Xms)␊ - ​␊ - 3. Save deploy artifacts ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - ​␊ - (Save deploy artifacts completed in Xms)␊ - ​␊ - Netlify Build Complete ␊ - ────────────────────────────────────────────────────────────────␊ - ​␊ - (Netlify Build completed in Xms)␊ - ␊ - ◈ Static server listening to 88888␊ - ◈ Loaded function hello http://localhost:88888/.netlify/functions/hello.␊ - ◈ Functions server is listening on 88888␊ - ␊ - ┌──────────────────────────────────────────────────┐␊ - │ │␊ - │ ◈ Server now ready on http://localhost:88888 │␊ - │ │␊ - └──────────────────────────────────────────────────┘` diff --git a/tests/integration/snapshots/600.framework-detection.test.cjs.snap b/tests/integration/snapshots/600.framework-detection.test.cjs.snap index 568785f39f0bf18fd477f774bbb15216fae40d5b..598af7ce390ffd36e688f08c53c942204da26c9a 100644 GIT binary patch delta 2064 zcmV+r2=Djj5U~(4K~_N^Q*L2!b7*gLAa*kf0{}bfJw?zUTv`4l7^Bu=70x1LEIk+iN!2zL2 zaFKf(f3w-$v17+$ch##`@4fGP?^XNbw4aIiQ2pZ9R5P8?M_XzriY$SIrliO;XaTtt zF;yyxC&}hTAUpx3iDoeN4IqkI*4xfBu+DZ0oIh{;K4L%bptwHHS)=3@L{& zL@Ys?kpVCTIZ=uxi{x5B!)1hE8I^Q#L5%It{O}blS1B z?Xff=k{<~mP2_;;Jt6fcV(8qD8yFftS2{i#LpuX5q!AL#^b5+Ihmv_r{g%kmbt10^ z#VXxha6w|^fRHxLM)MaA40tL7e^?4?Dxc$^HN@8t|7<|~9S?C?@LC?URd!!sF**Bg z0q8{1qmX{@PYs~|_RK1vA19Q3BuhK6F2q*6aiOpV_R6$|G|PmY(WdcGfc@MC`Uo+V z_z5DR@Ja-ChlN`^5fk2!>(-q=;7f%>&LAbv#I;Vun(QcZ{Iy|@Kc1jDe|i|*n!+7Q zVh!7QZin9_NFmnG7XIF?U4sOy;8iSHu9e{;sUz6SC_&mY2bCx|Xp9MrC6!jCZLWCR z=%GYlx&92zxF1Fzh>B25jVeOEU?vUZ6`^Mo8b*;$98Jy@-xZvc3^y9TEHg$MCx$YfGdt9W5G;P zvMitZ!9LPBeT2NvM)ZqVVBGSk;zHZ$GpklaVp7<%<*N@i<`UMLp-+IJPrL`aRQ6yj z)ApRC;#w!Yjn(bbDPy1H7HVnnGK9hk!a z0hEtTn9TBsD*EMbvKAYfxeERS8=Bn$v-lO0YjG<6&kYHNdG&KK)H~X}ef`G$_ix;} zyK{TD)!KU1(P*tcdOjY#Sv&Ewze}^6M2rea;TWV}f3VCqpYD9zlrRtMzz3@twl#O7 z-5=OIwEVe*yo`^=M0TNfd-vTN_ujqVd*?>))(PB-KOR9CZHGQJ;94&mPtW_bR=5W5Q>lzi0N(Vj?zk}edW8S{pqm*PC>$cvG{Xeh z1uY9|e?AvFFLck=4tqooyB&AK1ON0hdp<+XG9xUGvGZ+kEx)Rb#@u>Wz9{>KE<42O`&ayaN{M_ zMto-l7Ux642gm`4%zgNQwP&~Hpx_c-X?y`6f5Jgz*Q!~#W?NHv1+HAQMR++7tH+p{5=?d1(rV$oRb-YHv3^F8c?lJke uDs>^fqDbzPEl#HbM8`qEoiJ9h@0Vn%wkpbAw;!js{rEo*A35;;F#rHJ$O0k& delta 2118 zcmV-M2)XyM5a$pwK~_N^Q*L2!b7*gLAa*kf0|5GF@aB}oWjU>C7xdG4JS)&r9|a!c zp3Ff*QA8B^;KH9Tocq!_{n+@zxi6pp{e>sL z{0Zz)l_v2NZqhN}`4e-g+e~+xW9dratT3yh>Q3Z6kN-QxtlX)ps8YFwg+vQDC%o1{e(sIk*~FG7n{(@Pagm5$>Wb+$?NAZ1h%L9~azk)p0{E=b{oOL(0JS zF_s`s$p8`w8Ih7ki{x*rluUhQ~$dNzQpe+HgY z!E5qulW!a9cuCZ;>!>5;=n_K;=(8hTekU=q$)a28TSsopk*1T7vp5;#qDjL|ES>dO z+IF!tA%dM4K#GU~Rr_41PmQ4qKd!~l__@;Y$uYD$U|eV+-b_EQ$ayH4h16|{EL|sX zdQhy=?G5WBS`Khw(rhq)<4^}re+hu0pt|xI4r)TY3Gr7W#NTltE^=1MgQm*vb1Wuj zpDhGB5%i=;zyHTtp#SFFsz5)DD?3jXW?)r_sd&R&Vh!w+X$@(baxDA=ow@idZB$pV{e z4jVNT7+AJHLzpbBG7K@9XPWbH@e-{?bdIa&Y#govXP1phnp7k~TXE#bIAMvJ`nV&% zPp2GT9?EtAyNH<8u1_)AfAJZ2;k82Zuaw8Ox_v=ykC}FZ6SVwK&4&N@EN$4jVU2^Y zOH5|DzUOM)*xK3%(r)kky$dWGnMikLKCSz|Q8O_X0wqLmUUIS5&(*BdmM}`BPOOUv zeps_JUj?A&WO;^mYWW+R&Ws{z*fMJd67>5ycE^$RJg?NutDFsZe@;uU*w8mmx1rgx z{|9^ep4*eL?Tix3?q^|(SIWEkS=sBS5&Q3IQSj?EMuF3G9hClc2Ums_qM8vp6@rU` z%q=<*r;>Tf(%xc0yYbH&9Dm(dSvd1i>c&%Q z_4?Ke(Q1B8zqTT7eoG^;)oX1>QH7h~O=yQ^s&Df%$+EVmZJ=Agdi?R&#$7F3aU>b@ zL?q+b*gYS7!PQ)sjeb3VXJE^})tm!fF_L4Gg{Z9_&id zgC(gl=OiW9e;RR&8<3ofETP;#)tunB&F?Ed+wXe@d73pxSIyDYi#xiyUX)@)${8K# z!vGGHjkTN1@`%j)#c#3}8=AQaevJ*yUXEG(n$ER275`_N1pTc1xftpl?A^J!^WgoR zyZ3hQ?6q23uUi_e)JM<9qqiz2e&%;+hLea^A<8X-fAsStb!!mynmX(U^!X^zQ7vyL12D2fcT8dbii`Rs87)suWc_XMamWVU@vZ3EPPZ zPOT>&{SVRv#?09|=^wj|Wxk0y5GLSfg$8m}Y zvI|P&e^kHERhFxssU7x-8g@JO3lIF$&Fr}hIm?W&IL6F3&b9om!WtS^o3r2z_zow= z##x#V5~dXde>UHGO4Waz;kSaypiR#91+_katiaUe&#kcwhc9YwQuHbM3~LH)U4&~Z z$u`D!nqzT3By4~jfRT9sKQMT9D+cne;GNnBfAAq324<}a!WDp#gOC|CtzeITeLJSGFZ?->-%gC`H-@}n}4x-IZ+eI7iNMuL8BF2QjX z>en#&kg-1R$WlH9o%>>JAmv#?-C(vHg;lWC^G45&I$O@VSqEVes?#+BdXOe@{T-9@ zU|Z|bD~jY!(c*NfjH| Date: Mon, 9 Jan 2023 16:32:09 +0000 Subject: [PATCH 41/43] refactor: add log message --- src/commands/serve/serve.mjs | 5 ++++- .../600.framework-detection.test.cjs.md | 1 + .../600.framework-detection.test.cjs.snap | Bin 2097 -> 2160 bytes 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/commands/serve/serve.mjs b/src/commands/serve/serve.mjs index 9100f08c9d1..dd6c9e1879d 100644 --- a/src/commands/serve/serve.mjs +++ b/src/commands/serve/serve.mjs @@ -81,7 +81,10 @@ const serve = async (options, command) => { command.setAnalyticsPayload({ projectType: settings.framework || 'custom', live: options.live, graph: options.graph }) - log(`${NETLIFYDEVWARN} Building site for production`) + log(`${NETLIFYDEVLOG} Building site for production`) + log( + `${NETLIFYDEVWARN} Changes will not be hot-reloaded, so if you need to rebuild your site you must exit and run 'netlify serve' again`, + ) const { configPath: configPathOverride } = await runBuildTimeline({ cachedConfig, options, settings, site }) diff --git a/tests/integration/snapshots/600.framework-detection.test.cjs.md b/tests/integration/snapshots/600.framework-detection.test.cjs.md index 47eb6d26b47..6a4df08cd00 100644 --- a/tests/integration/snapshots/600.framework-detection.test.cjs.md +++ b/tests/integration/snapshots/600.framework-detection.test.cjs.md @@ -246,6 +246,7 @@ Generated by [AVA](https://avajs.dev). ◈ Using simple static server because '[dev.framework]' was set to '#static'␊ ◈ Running static server from "site-with-framework/public"␊ ◈ Building site for production␊ + ◈ Changes will not be hot-reloaded, so if you need to rebuild your site you must exit and run 'netlify serve' again␊ ​␊ Netlify Build ␊ ────────────────────────────────────────────────────────────────␊ diff --git a/tests/integration/snapshots/600.framework-detection.test.cjs.snap b/tests/integration/snapshots/600.framework-detection.test.cjs.snap index 598af7ce390ffd36e688f08c53c942204da26c9a..dcbfea67e1dba40fd05939ae3d6ca42107c3a6ee 100644 GIT binary patch literal 2160 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 literal 2097 zcmV-12+sFGRzV-&LU(iJsAE)*X&+v zZ7wqT2p@|G00000000B+T1|`_Nfpi{0wmu!=fIP5=*cSGW|D>M8g1BFC)phZCZl9` z$!au8%H7p=W!hCuRka-pt+XH^A;AHmNN`>5oH!vNfjDu36L3U|fFoC~y!z>Ow|{0Q zKeO50v17+$ch##`@4fGP?^XNbw4aIiQ2pZ9R5P8?M_XzriY$SIrliO;XaTttF;yyx zC&}hTAUpx3iDoe}bMUEJ!kQe<7 zN2MZ5P!wLW;~`}kRgg+CDy7DSl$2|j2zh8eCrsk1LQbvdyHpoB5a3SRNQ+Tsg}{XA zAzs)|==cDI$C?Rl*U4#Y_B9oS*}}NojiOqkd=+`Q6cQ0DAi4`ft#j4wbmELfI-6uh zO`3^(WbEMW>IOE%W=v^nm&0woW*)5;06cm8vnP)~`b<85?)3W+JoN)7&E`6rSsQRM z0ZBl=h0tX5B!)1hE8I^Q#L5%It{O}blS1B?Xff=k{<~m zP2_;;Jt6fcV(8qD8yFftS2{i#LpuX5q!AL#^b5+Ihmv_r{g%kmbt10^#VXxha6w|^ zfRHxLM)MaA40tL7SPE(?pW~o4#Mco2Y(V@S4{=%WS{}4jc3)sIIs0w_=tR<^kbdt^ z4WR$_%qpNCCzO38OFOVG#8$jcLrtwgK{oDrn2r-rT2_m8JN(6U@ zg?m{mwPB7wo}f8;7~Pt}9ZF&i+j(w>-y}#O z*3TCH-mP7O1gzjyELpCV;UcLc*vlwE+A{~0C^%@035+F`R;6vOc-!cqL}0o849(BH7F=Nx=r;xfneJzwjl&ev9u z340&!UEul1BHg?BxZ(d=!^Kz#R3>`kg2!IJ(6dro!H6l7SQjn$VZ+a2l>q}UE3mZF z$lus><`k`lEwf=DNxxrUcbvG$3o6Zm+Sx$hwG7G)ed~A|nl1Y|*wgp?o~&sl!)|#PDfT2&k2fI}EU@X)2oTTDfD~=0mBp)J6 zDEH5eAo$(-_Z6S+{hmRQ))(PB-KOR9CZHGQJ;94&m zPtW_bR=5W5Q>lzi0N(Vj?zk}edW8S{pqm*PC>$cvG{Xeh1uY9|J{LMKbkEifdqfYr z9e2Y6|MW9^K10qjBP@=w^KEb~zpAl@#?|ILcn!WIi1l%v6$8eNf)LK;yGW_WuXp@b zR2#G@_@1OL29OoFzW9Z8b{X(x%}vTa#hhVHp>2wA<0aKbd}jp~=R?8=$N`AVefWX3 zXSe2{;1XVGd;uTAL1fpeS-56kH_B}I&kKoDuwEC(7Qnv-6tIwl{%1SV=%_VkRW%VnVe8||GcjTFvg2{a;HjwHx zS>4)fxfNEKt(iA^e$?p-*3G677EN`$M!*a*ByR38`M7l Date: Mon, 9 Jan 2023 16:33:16 +0000 Subject: [PATCH 42/43] Update src/commands/serve/serve.mjs Co-authored-by: Matt Kane --- src/commands/serve/serve.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/serve/serve.mjs b/src/commands/serve/serve.mjs index dd6c9e1879d..1647653fb66 100644 --- a/src/commands/serve/serve.mjs +++ b/src/commands/serve/serve.mjs @@ -153,7 +153,7 @@ const serve = async (options, command) => { export const createServeCommand = (program) => program .command('serve') - .description('(Beta) build the site for production and serve locally') + .description('(Beta) Build the site for production and serve locally. This does not watch the code for changes, so if you need to rebuild your site then you must exit and run `serve` again.') .option( '--context ', 'Specify a deploy context or branch for environment variables (contexts: "production", "deploy-preview", "branch-deploy", "dev")', From ad1dbf4b6d3ae147b1db9f0afae664a21207d85c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 9 Jan 2023 16:35:05 +0000 Subject: [PATCH 43/43] chore: update docs --- README.md | 2 +- docs/README.md | 2 +- docs/commands/index.md | 2 +- src/commands/serve/serve.mjs | 4 +++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index baf228a04a0..1245de122c2 100644 --- a/README.md +++ b/README.md @@ -222,7 +222,7 @@ Open settings for the site linked to the current folder ### [serve](/docs/commands/serve.md) -(Beta) build the site for production and serve locally +(Beta) Build the site for production and serve locally. This does not watch the code for changes, so if you need to rebuild your site then you must exit and run `serve` again. ### [sites](/docs/commands/sites.md) diff --git a/docs/README.md b/docs/README.md index 922a2b95757..21656429d10 100644 --- a/docs/README.md +++ b/docs/README.md @@ -170,7 +170,7 @@ Open settings for the site linked to the current folder ### [serve](/docs/commands/serve.md) -(Beta) build the site for production and serve locally +(Beta) Build the site for production and serve locally. This does not watch the code for changes, so if you need to rebuild your site then you must exit and run `serve` again. ### [sites](/docs/commands/sites.md) diff --git a/docs/commands/index.md b/docs/commands/index.md index 99acd94f726..544f2120472 100644 --- a/docs/commands/index.md +++ b/docs/commands/index.md @@ -149,7 +149,7 @@ Open settings for the site linked to the current folder ### [serve](/docs/commands/serve.md) -(Beta) build the site for production and serve locally +(Beta) Build the site for production and serve locally. This does not watch the code for changes, so if you need to rebuild your site then you must exit and run `serve` again. ### [sites](/docs/commands/sites.md) diff --git a/src/commands/serve/serve.mjs b/src/commands/serve/serve.mjs index 1647653fb66..839d2530db2 100644 --- a/src/commands/serve/serve.mjs +++ b/src/commands/serve/serve.mjs @@ -153,7 +153,9 @@ const serve = async (options, command) => { export const createServeCommand = (program) => program .command('serve') - .description('(Beta) Build the site for production and serve locally. This does not watch the code for changes, so if you need to rebuild your site then you must exit and run `serve` again.') + .description( + '(Beta) Build the site for production and serve locally. This does not watch the code for changes, so if you need to rebuild your site then you must exit and run `serve` again.', + ) .option( '--context ', 'Specify a deploy context or branch for environment variables (contexts: "production", "deploy-preview", "branch-deploy", "dev")',