diff --git a/src/commands/deploy.js b/src/commands/deploy.js index 89340943872..48b1da4dff8 100644 --- a/src/commands/deploy.js +++ b/src/commands/deploy.js @@ -1,6 +1,7 @@ const path = require('path') const process = require('process') +const { updateConfig, restoreConfig } = require('@netlify/config') const { flags: flagsLib } = require('@oclif/command') const chalk = require('chalk') const { get } = require('dot-prop') @@ -307,7 +308,7 @@ const runDeploy = async ({ const handleBuild = async ({ context, flags }) => { if (!flags.build) { - return + return {} } const [token] = await getToken() const options = await getBuildOptions({ @@ -315,11 +316,11 @@ const handleBuild = async ({ context, flags }) => { token, flags, }) - const { exitCode, newConfig } = await runBuild(options) + const { exitCode, newConfig, configMutations } = await runBuild(options) if (exitCode !== 0) { context.exit(exitCode) } - return newConfig + return { newConfig, configMutations } } const printResults = ({ flags, results, deployToProduction, exit }) => { @@ -429,7 +430,7 @@ class DeployCommand extends Command { return triggerDeploy({ api, siteId, siteData, error }) } - const newConfig = await handleBuild({ context: this, flags }) + const { newConfig, configMutations = [] } = await handleBuild({ context: this, flags }) const config = newConfig || this.netlify.config const deployFolder = await getDeployFolder({ flags, config, site, siteData }) @@ -450,6 +451,15 @@ class DeployCommand extends Command { error, }) const functionsConfig = normalizeFunctionsConfig({ functionsConfig: config.functions, projectRoot: site.root }) + + const redirectsPath = `${deployFolder}/_redirects` + await updateConfig(configMutations, { + buildDir: deployFolder, + configPath, + redirectsPath, + context: this.netlify.cachedConfig.context, + branch: this.netlify.cachedConfig.branch, + }) const results = await runDeploy({ flags, deployToProduction, @@ -468,6 +478,10 @@ class DeployCommand extends Command { exit, }) + if (configMutations.length !== 0) { + await restoreConfig({ buildDir: deployFolder, configPath, redirectsPath }) + } + printResults({ flags, results, deployToProduction, exit }) if (flags.open) { diff --git a/src/lib/build.js b/src/lib/build.js index 2121d775ab3..32057d41eac 100644 --- a/src/lib/build.js +++ b/src/lib/build.js @@ -25,8 +25,8 @@ const getBuildOptions = ({ }) const runBuild = async (options) => { - const { severityCode: exitCode, netlifyConfig: newConfig } = await build(options) - return { exitCode, newConfig } + const { severityCode: exitCode, netlifyConfig: newConfig, configMutations } = await build(options) + return { exitCode, newConfig, configMutations } } module.exports = { getBuildOptions, runBuild } diff --git a/tests/command.deploy.test.js b/tests/command.deploy.test.js index 4c5cb99a244..c86f44d9222 100644 --- a/tests/command.deploy.test.js +++ b/tests/command.deploy.test.js @@ -1,3 +1,4 @@ +/* eslint-disable require-await */ const process = require('process') const test = require('ava') @@ -399,7 +400,6 @@ if (process.env.NETLIFY_TEST_DISABLE_LIVE !== 'true') { }) test.serial('should deploy functions from internal functions directory', async (t) => { - /* eslint-disable require-await */ await withSiteBuilder('site-with-internal-functions', async (builder) => { await builder .withNetlifyToml({ @@ -451,14 +451,12 @@ if (process.env.NETLIFY_TEST_DISABLE_LIVE !== 'true') { t.is(await got(`${deployUrl}/.netlify/functions/func-1`).text(), 'User 1') t.is(await got(`${deployUrl}/.netlify/functions/func-2`).text(), 'User 2') t.is(await got(`${deployUrl}/.netlify/functions/func-3`).text(), 'Internal 3') - /* eslint-enable require-await */ }) }) test.serial( 'should deploy functions from internal functions directory when setting `base` to a sub-directory', async (t) => { - /* eslint-disable require-await */ await withSiteBuilder('site-with-internal-functions-sub-directory', async (builder) => { await builder .withNetlifyToml({ @@ -487,14 +485,90 @@ if (process.env.NETLIFY_TEST_DISABLE_LIVE !== 'true') { ) t.is(await got(`${deployUrl}/.netlify/functions/func-1`).text(), 'Internal') - /* eslint-enable require-await */ }) }, ) + test.serial('should handle redirects mutated by plugins', async (t) => { + await withSiteBuilder('site-with-public-folder', async (builder) => { + const content = '

⊂◉‿◉つ

' + await builder + .withContentFile({ + path: 'public/index.html', + content, + }) + .withNetlifyToml({ + config: { + build: { publish: 'public' }, + functions: { directory: 'functions' }, + redirects: [{ from: '/*', to: '/index.html', status: 200 }], + plugins: [{ package: './plugins/mutator' }], + }, + }) + .withFunction({ + path: 'hello.js', + handler: async () => ({ + statusCode: 200, + body: 'hello', + }), + }) + .withRedirectsFile({ + pathPrefix: 'public', + redirects: [{ from: `/api/*`, to: `/.netlify/functions/:splat`, status: '200' }], + }) + .withBuildPlugin({ + name: 'mutator', + plugin: { + onPostBuild: ({ netlifyConfig }) => { + netlifyConfig.redirects = [ + { + from: '/other-api/*', + to: '/.netlify/functions/:splat', + status: 200, + }, + ...netlifyConfig.redirects, + ] + }, + }, + }) + .buildAsync() + + const deploy = await callCli( + ['deploy', '--json', '--build'], + { + cwd: builder.directory, + env: { NETLIFY_SITE_ID: t.context.siteId }, + }, + true, + ) + + const fullDeploy = await callCli( + ['api', 'getDeploy', '--data', JSON.stringify({ deploy_id: deploy.deploy_id })], + { + cwd: builder.directory, + env: { NETLIFY_SITE_ID: t.context.siteId }, + }, + true, + ) + + const redirectsMessage = fullDeploy.summary.messages.find(({ title }) => title === '3 redirect rules processed') + t.is(redirectsMessage.description, 'All redirect rules deployed without errors.') + + await validateDeploy({ deploy, siteName: SITE_NAME, content, t }) + + // plugin redirect + t.is(await got(`${deploy.deploy_url}/other-api/hello`).text(), 'hello') + // _redirects redirect + t.is(await got(`${deploy.deploy_url}/api/hello`).text(), 'hello') + // netlify.toml redirect + t.is(await got(`${deploy.deploy_url}/not-existing`).text(), content) + }) + }) + test.after('cleanup', async (t) => { const { siteId } = t.context console.log(`deleting test site "${SITE_NAME}". ${siteId}`) await callCli(['sites:delete', siteId, '--force']) }) } +/* eslint-enable require-await */