Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions eslint_temporary_suppressions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,12 @@ export default [
'@typescript-eslint/consistent-indexed-object-style': 'off',
},
},
{
Comment thread
serhalp marked this conversation as resolved.
Outdated
files: ['packages/build/src/plugins_core/spa_fallback/index.ts'],
rules: {
'n/no-missing-import': 'off',
},
},
{
files: ['packages/build/src/plugins_core/types.ts'],
rules: {
Expand Down
1 change: 1 addition & 0 deletions packages/build/src/plugins_core/frameworks_api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { filterConfig, loadConfigFile } from './util.js'
const ALLOWED_PROPERTIES = [
['build', 'functions'],
['build', 'publish'],
['build', 'spa'],
['edge_functions'],
['functions', '*'],
['functions', '*', '*'],
Expand Down
48 changes: 48 additions & 0 deletions packages/build/src/plugins_core/spa_fallback/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { NetlifyConfig } from '../../index.js'
import { getConfigMutations } from '../../plugins/child/diff.js'
import { CoreStep, CoreStepFunction, CoreStepFunctionArgs } from '../types.js'

// The catch-all redirect that makes a single-page application's client-side
// router handle every path.
const SPA_FALLBACK_REDIRECT = {
from: '/*',
status: 200,
to: '/index.html',
}

function hasCatchAllRedirect(redirects: NetlifyConfig['redirects']) {
return redirects.some((r) => r.from === '/*')
}

function coreStep(coreStepFunctionArgs: CoreStepFunctionArgs): ReturnType<CoreStepFunction> {
if (
!coreStepFunctionArgs.netlifyConfig.build.spa ||
hasCatchAllRedirect(coreStepFunctionArgs.netlifyConfig.redirects)
) {
return Promise.resolve({})
}
Comment thread
serhalp marked this conversation as resolved.
Outdated

const newConfig: Partial<NetlifyConfig> = {
redirects: [...coreStepFunctionArgs.netlifyConfig.redirects, SPA_FALLBACK_REDIRECT],
}

const configMutations = getConfigMutations(
coreStepFunctionArgs.netlifyConfig,
{
...coreStepFunctionArgs.netlifyConfig,
...newConfig,
},
applySpaFallback.event,
) as unknown[]

return Promise.resolve({ configMutations })
}

export const applySpaFallback: CoreStep = {
coreStep,
coreStepDescription: () => '',
coreStepId: 'spa_fallback',
coreStepName: 'Applying SPA fallback redirect',
event: 'onPostBuild',
quiet: true,
}
2 changes: 2 additions & 0 deletions packages/build/src/steps/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { preCleanup } from '../plugins_core/pre_cleanup/index.js'
import { preDevCleanup } from '../plugins_core/pre_dev_cleanup/index.js'
import { saveArtifacts } from '../plugins_core/save_artifacts/index.js'
import { scanForSecrets } from '../plugins_core/secrets_scanning/index.js'
import { applySpaFallback } from '../plugins_core/spa_fallback/index.js'
import { CoreStep, Event } from '../plugins_core/types.js'

// Get all build steps
Expand Down Expand Up @@ -85,6 +86,7 @@ const addCoreSteps = function (steps): CoreStep[] {
bundleFunctions,
bundleEdgeFunctions,
copyDbMigrations,
applySpaFallback,
scanForSecrets,
uploadBlobs,
deploySite,
Expand Down
5 changes: 5 additions & 0 deletions packages/build/src/types/config/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export interface Build {
* Includes the path to a site's [Edge Functions directory](https://docs.netlify.com/edge-functions/optional-configuration/#edge-functions-directory)
*/
edge_functions?: string

/**
* Whether the site is a single-page application (SPA). Defaults to `false`.
*/
spa?: boolean
/**
* Contains a site's [environment variables](https://docs.netlify.com/configure-builds/environment-variables/#netlify-configuration-variables)
*/
Expand Down
48 changes: 24 additions & 24 deletions packages/build/tests/core/snapshots/tests.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -917,9 +917,9 @@ Generated by [AVA](https://avajs.dev).
Running \`netlify build\` will execute this build flow␊
┌──────────────────────────────────┐␊
│ Event │ Location │␊
└──────────────────────────────────┘␊
┌──────────────────┬──────────────────┐␊
│ Event │ Location │␊
└──────────────────┴──────────────────┘␊
If this looks good to you, run \`netlify build\` to execute the build␊
`
Expand Down Expand Up @@ -972,18 +972,18 @@ Generated by [AVA](https://avajs.dev).
Running \`netlify build\` will execute this build flow␊
┌──────────────────────────────────┐␊
│ Event │ Location │␊
└──────────────────────────────────┘␊
┌─────────────────┐␊
│ 1. onPreBuild ↓ │ Plugin ./plugin␊
└─────────────────┘ ␊
┌─────────────────┐␊
│ 2. onBuild ↓ │ build.command from netlify.toml␊
└─────────────────┘ ␊
┌─────────────────┐␊
│ 3. onBuild ↓ │ Functions bundling␊
└─────────────────┘ ␊
┌──────────────────┬──────────────────┐␊
│ Event │ Location │␊
└──────────────────┴──────────────────┘␊
┌─────────────────┐␊
│ 1. onPreBuild ↓ │ Plugin ./plugin␊
└─────────────────┘ ␊
┌─────────────────┐␊
│ 2. onBuild ↓ │ build.command from netlify.toml␊
└─────────────────┘ ␊
┌─────────────────┐␊
│ 3. onBuild ↓ │ Functions bundling␊
└─────────────────┘ ␊
If this looks good to you, run \`netlify build\` to execute the build␊
`
Expand Down Expand Up @@ -1026,9 +1026,9 @@ Generated by [AVA](https://avajs.dev).
Running \`netlify build\` will execute this build flow␊
┌──────────────────────────────────┐␊
│ Event │ Location │␊
└──────────────────────────────────┘␊
┌──────────────────┬──────────────────┐␊
│ Event │ Location │␊
└──────────────────┴──────────────────┘␊
If this looks good to you, run \`netlify build\` to execute the build␊
`
Expand Down Expand Up @@ -1073,12 +1073,12 @@ Generated by [AVA](https://avajs.dev).
Running \`netlify build\` will execute this build flow␊
┌──────────────────────────────────┐␊
│ Event │ Location │␊
└──────────────────────────────────┘␊
┌─────────────────┐␊
│ 1. onBuild ↓ │ Build command from Netlify app␊
└─────────────────┘ ␊
┌──────────────────┬──────────────────┐␊
│ Event │ Location │␊
└──────────────────┴──────────────────┘␊
┌─────────────────┐␊
│ 1. onBuild ↓ │ Build command from Netlify app␊
└─────────────────┘ ␊
If this looks good to you, run \`netlify build\` to execute the build␊
`
Expand Down
Binary file modified packages/build/tests/core/snapshots/tests.js.snap
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# .netlify/v1/config.json is generated at build time by build.mjs.
.netlify
11 changes: 11 additions & 0 deletions packages/build/tests/frameworks_api/fixtures/spa_config/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { mkdir, writeFile } from 'node:fs/promises'

const config = {
build: {
spa: true,
},
}

await mkdir('.netlify/v1', { recursive: true })

await writeFile('.netlify/v1/config.json', JSON.stringify(config))
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[build]
command = "node build.mjs"
10 changes: 10 additions & 0 deletions packages/build/tests/frameworks_api/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ test('Honors `edge_functions` declared through the Frameworks API config file',
])
})

test('Honors `build.spa` declared through the Frameworks API config file', async (t) => {
const { netlifyConfig, success } = await new Fixture(
test.meta.file,
'./fixtures/spa_config',
).runWithBuildAndIntrospect()

t.true(success)
t.true(netlifyConfig.build.spa)
})

test('Loads configuration data that has been generated by the build command using the legacy API path', async (t) => {
const expectedImageDomains = [
'domain1.from-toml.netlify',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[build]
command = "echo hi"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build]
command = "echo hi"
spa = false
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build]
command = "echo hi"
spa = true
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[build]
command = "echo hi"
spa = true

[[redirects]]
from = "/*"
to = "/200.html"
status = 200
52 changes: 52 additions & 0 deletions packages/build/tests/spa_fallback/tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Fixture } from '@netlify/testing'
import test from 'ava'
Comment thread
serhalp marked this conversation as resolved.
Outdated

const SPA_FALLBACK_REDIRECT = {
conditions: {},
headers: {},
force: false,
from: '/*',
query: {},
status: 200,
to: '/index.html',
}

test('Injects an SPA fallback redirect when `build.spa` is `true`', async (t) => {
const { netlifyConfig, success } = await new Fixture(
test.meta.file,
'./fixtures/spa_enabled',
).runWithBuildAndIntrospect()

t.true(success)
t.deepEqual(netlifyConfig.redirects, [SPA_FALLBACK_REDIRECT])
})

test('Does not inject an SPA fallback redirect when `build.spa` is `false`', async (t) => {
const { netlifyConfig, success } = await new Fixture(
test.meta.file,
'./fixtures/spa_disabled',
).runWithBuildAndIntrospect()

t.true(success)
t.deepEqual(netlifyConfig.redirects, [])
})

test('Does not inject an SPA fallback redirect when `build.spa` is not set', async (t) => {
const { netlifyConfig, success } = await new Fixture(
test.meta.file,
'./fixtures/spa_default',
).runWithBuildAndIntrospect()

t.true(success)
t.deepEqual(netlifyConfig.redirects, [])
})

test('Does not override a catch-all redirect already declared by the user', async (t) => {
const { netlifyConfig, success } = await new Fixture(
test.meta.file,
'./fixtures/spa_enabled_existing_catch_all',
).runWithBuildAndIntrospect()

t.true(success)
t.deepEqual(netlifyConfig.redirects, [{ ...SPA_FALLBACK_REDIRECT, to: '/200.html' }])
})
10 changes: 5 additions & 5 deletions packages/build/tests/telemetry/snapshots/tests.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ Generated by [AVA](https://avajs.dev).
plugins: [],
siteId: 'test',
status: 'success',
steps: 2,
steps: 3,
},
timestamp: 'number',
userId: 'buildbot_user',
Expand Down Expand Up @@ -157,7 +157,7 @@ Generated by [AVA](https://avajs.dev).
],
siteId: 'test',
status: 'success',
steps: 3,
steps: 4,
},
timestamp: 'number',
userId: 'buildbot_user',
Expand Down Expand Up @@ -193,7 +193,7 @@ Generated by [AVA](https://avajs.dev).
],
siteId: 'test',
status: 'success',
steps: 3,
steps: 4,
},
timestamp: 'number',
userId: 'buildbot_user',
Expand Down Expand Up @@ -229,7 +229,7 @@ Generated by [AVA](https://avajs.dev).
],
siteId: 'test',
status: 'success',
steps: 3,
steps: 4,
},
timestamp: 'number',
userId: 'buildbot_user',
Expand Down Expand Up @@ -265,7 +265,7 @@ Generated by [AVA](https://avajs.dev).
],
siteId: 'test',
status: 'success',
steps: 3,
steps: 4,
},
timestamp: 'number',
userId: 'buildbot_user',
Expand Down
Binary file modified packages/build/tests/telemetry/snapshots/tests.js.snap
Binary file not shown.
5 changes: 5 additions & 0 deletions packages/build/tests/time/snapshots/tests.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Generated by [AVA](https://avajs.dev).
buildbot.build.stage.duration:0|d|#stage:others,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:pre_cleanup,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:resolve_config,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:spa_fallback,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:start_plugins,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:system,parent:run_netlify_build_per_type␊
buildbot.build.stage.duration:0|d|#stage:user,parent:run_netlify_build_per_type`
Expand All @@ -36,6 +37,7 @@ Generated by [AVA](https://avajs.dev).
buildbot.build.stage.duration:0|d|#stage:pre_cleanup,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:resolve_config,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:run_plugins,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:spa_fallback,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:start_plugins,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:system,parent:run_netlify_build_per_type␊
buildbot.build.stage.duration:0|d|#stage:user,parent:run_netlify_build_per_type`
Expand All @@ -53,6 +55,7 @@ Generated by [AVA](https://avajs.dev).
buildbot.build.stage.duration:0|d|#stage:pre_cleanup,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:resolve_config,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:run_plugins,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:spa_fallback,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:start_plugins,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:system,parent:run_netlify_build_per_type␊
buildbot.build.stage.duration:0|d|#stage:user,parent:run_netlify_build_per_type`
Expand All @@ -69,6 +72,7 @@ Generated by [AVA](https://avajs.dev).
buildbot.build.stage.duration:0|d|#stage:others,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:pre_cleanup,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:resolve_config,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:spa_fallback,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:start_plugins,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:system,parent:run_netlify_build_per_type␊
buildbot.build.stage.duration:0|d|#stage:user,parent:run_netlify_build_per_type`
Expand All @@ -85,6 +89,7 @@ Generated by [AVA](https://avajs.dev).
buildbot.build.stage.duration:0|d|#stage:others,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:pre_cleanup,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:resolve_config,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:spa_fallback,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:start_plugins,parent:run_netlify_build␊
buildbot.build.stage.duration:0|d|#stage:system,parent:run_netlify_build_per_type␊
buildbot.build.stage.duration:0|d|#stage:user,parent:run_netlify_build_per_type`
Binary file modified packages/build/tests/time/snapshots/tests.js.snap
Binary file not shown.
3 changes: 3 additions & 0 deletions packages/config/src/case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const normalizeBuildCase = ({
processing = Processing,
Publish,
publish = Publish,
Spa,
spa = Spa,
...build
}: Record<string, unknown> = {}): Record<string, unknown> => {
return {
Expand All @@ -41,5 +43,6 @@ const normalizeBuildCase = ({
ignore,
processing,
publish,
spa,
}
}
1 change: 1 addition & 0 deletions packages/config/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const BUILD_PROPERTIES = new Set([
'ignore',
'processing',
'publish',
'spa',
])

// `config.functions` is a plain object while `config.build.functions` is a
Expand Down
1 change: 1 addition & 0 deletions packages/config/src/mutations/apply.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const MUTABLE_PROPS = {
'build.processing.skip_processing': { lastEvent: 'onPostBuild' },
'build.publish': { lastEvent: 'onPostBuild' },
'build.services': { lastEvent: 'onPostBuild' },
'build.spa': { lastEvent: 'onPostBuild' },
'build.services.*': { lastEvent: 'onPostBuild' },
edge_functions: { lastEvent: 'onPostBuild' },
'functions.*': { lastEvent: 'onBuild', denormalize: denormalizeFunctionsTopProps },
Expand Down
Loading
Loading