diff --git a/src/commands/deploy/deploy.ts b/src/commands/deploy/deploy.ts index 2fbcb3211b0..7b85c210d40 100644 --- a/src/commands/deploy/deploy.ts +++ b/src/commands/deploy/deploy.ts @@ -37,6 +37,7 @@ import { logJson, warn, type APIError, + NETLIFYDEVWARN, } from '../../utils/command-helpers.js' import { DEFAULT_CONCURRENT_HASH, DEFAULT_DEPLOY_TIMEOUT } from '../../utils/deploy/constants.js' import { type DeployEvent, deploySite } from '../../utils/deploy/deploy-site.js' @@ -944,6 +945,22 @@ const prepAndRunDeploy = async ({ const deployFolder = await getDeployFolder({ command, options, config, site, siteData }) const functionsFolder = getFunctionsFolder({ workingDir, options, config, site, siteData }) + // When deploying without running a build, warn if build plugins are configured + // because their config mutations are lost without a build run + // (see https://github.com/netlify/cli/issues/3792). + if (!options.build) { + type ConfigPlugin = { package?: unknown; origin?: string } + const plugins = + (config?.plugins as ConfigPlugin[] | undefined) ?? + (command.netlify.cachedConfig.config as { plugins?: ConfigPlugin[] } | undefined)?.plugins + const configuredPlugins = plugins?.filter((plugin) => plugin.origin !== 'default') ?? [] + if (configuredPlugins.length > 0) { + log( + `${NETLIFYDEVWARN} Site uses build plugins (${configuredPlugins.map((p) => p.package).join(', ')}) but no build is being run.\n` + + ` Config changes made by these plugins will not be applied. Use ${chalk.cyanBright('netlify deploy --build')} to build and deploy together.`, + ) + } + } const { configPath } = site // build flag wasn't used and edge functions directories exist diff --git a/src/utils/redirects.ts b/src/utils/redirects.ts index ffa925e1a8d..9f7a70d740a 100644 --- a/src/utils/redirects.ts +++ b/src/utils/redirects.ts @@ -36,6 +36,11 @@ const getErrorMessage = function ({ message }) { // - `from` is called `origin` // - `query` is called `params` // - `conditions.role|country|language` are capitalized +// Leading and trailing whitespace in `from` and `to` is trimmed so that typos +// such as `to = " https://example.com"` do not silently break redirects +// (see https://github.com/netlify/cli/issues/4707). +const trimValue = (value) => (typeof value === 'string' ? value.trim() : value) + const normalizeRedirect = function ({ // @ts-expect-error TS(7031) FIXME: Binding element 'country' implicitly has an 'any' ... Remove this comment to see the full error message conditions: { country, language, role, ...conditions }, @@ -45,11 +50,15 @@ const normalizeRedirect = function ({ query, // @ts-expect-error TS(7031) FIXME: Binding element 'signed' implicitly has an 'any' t... Remove this comment to see the full error message signed, + // @ts-expect-error TS(7031) FIXME: Binding element 'to' implicitly has an 'any type... + to, ...redirect }) { return { ...redirect, - origin: from, + origin: trimValue(from), + path: trimValue(from), + to: trimValue(to), params: query, conditions: { ...conditions, diff --git a/tests/unit/utils/redirects.test.ts b/tests/unit/utils/redirects.test.ts index 31492fd5da0..4484af07778 100644 --- a/tests/unit/utils/redirects.test.ts +++ b/tests/unit/utils/redirects.test.ts @@ -230,3 +230,29 @@ test('should parse redirect rules from _redirects file and netlify.toml', async expect(redirects).toEqual(expected) }) }) + +test('should trim leading and trailing whitespace from redirect `from` and `to`', async (t) => { + await withSiteBuilder(t, async (builder) => { + await builder + .withNetlifyToml({ + config: { + redirects: [ + { + from: ' /leading-space ', + status: 200, + to: ' https://www.netlify.com ', + }, + ], + }, + }) + .build() + + // @ts-expect-error TS(2345) FIXME: Argument of type '{ configPath: string; }' is not ... Remove this comment to see the full error message + const redirects = await parseRedirects({ configPath: `${builder.directory}/netlify.toml` }) + expect(redirects[0]).toMatchObject({ + origin: '/leading-space', + path: '/leading-space', + to: 'https://www.netlify.com', + }) + }) +})