From a25c90741e335f3eaf98a45c444a29196fd840bb Mon Sep 17 00:00:00 2001 From: Hrishikesh Kokate Date: Wed, 22 Jul 2026 17:47:32 +0530 Subject: [PATCH 01/11] add build.spa in netlify.toml --- packages/build/src/types/config/build.ts | 5 + packages/config/src/case.ts | 3 + packages/config/src/context.ts | 1 + packages/config/src/normalize.ts | 1 + packages/config/src/validate/helpers.js | 4 + packages/config/src/validate/validations.js | 15 +- .../tests/validate/snapshots/tests.js.md | 136 ++++++++++++++++++ .../tests/validate/snapshots/tests.js.snap | Bin 4649 -> 4785 bytes packages/config/tests/validate/tests.js | 5 + 9 files changed, 169 insertions(+), 1 deletion(-) diff --git a/packages/build/src/types/config/build.ts b/packages/build/src/types/config/build.ts index 46301afc82..0f50e93884 100644 --- a/packages/build/src/types/config/build.ts +++ b/packages/build/src/types/config/build.ts @@ -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) */ diff --git a/packages/config/src/case.ts b/packages/config/src/case.ts index 0bbdad2180..1012bf1e58 100644 --- a/packages/config/src/case.ts +++ b/packages/config/src/case.ts @@ -29,6 +29,8 @@ const normalizeBuildCase = ({ processing = Processing, Publish, publish = Publish, + Spa, + spa = Spa, ...build }: Record = {}): Record => { return { @@ -41,5 +43,6 @@ const normalizeBuildCase = ({ ignore, processing, publish, + spa, } } diff --git a/packages/config/src/context.ts b/packages/config/src/context.ts index 477ff2948b..61e0e279a1 100644 --- a/packages/config/src/context.ts +++ b/packages/config/src/context.ts @@ -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 diff --git a/packages/config/src/normalize.ts b/packages/config/src/normalize.ts index 6e1e6f1481..657ca376b9 100644 --- a/packages/config/src/normalize.ts +++ b/packages/config/src/normalize.ts @@ -30,6 +30,7 @@ const DEFAULT_CONFIG = (packagePath?: string) => ({ publishOrigin: DEFAULT_ORIGIN, processing: { css: {}, html: {}, images: {}, js: {} }, services: {}, + spa: false, }, functions: { [WILDCARD_ALL]: {} }, plugins: [], diff --git a/packages/config/src/validate/helpers.js b/packages/config/src/validate/helpers.js index 67c52f93cd..73066fbb1a 100644 --- a/packages/config/src/validate/helpers.js +++ b/packages/config/src/validate/helpers.js @@ -12,6 +12,10 @@ export const isString = function (value) { return typeof value === 'string' } +export const isBoolean = function (value) { + return typeof value === 'boolean' +} + // Check an object valid properties, including legacy ones export const validProperties = function (propNames, legacyPropNames) { return { diff --git a/packages/config/src/validate/validations.js b/packages/config/src/validate/validations.js index 3e21fe95ab..daafc7c849 100644 --- a/packages/config/src/validate/validations.js +++ b/packages/config/src/validate/validations.js @@ -5,7 +5,14 @@ import validateNpmPackageName from 'validate-npm-package-name' import { validations as edgeFunctionValidations, EDGE_FUNCTIONS_PROPERTIES } from '../edge_functions.js' import { bundlers, WILDCARD_ALL as FUNCTIONS_CONFIG_WILDCARD_ALL } from '../functions_config.js' -import { functionsDirectoryCheck, isArrayOfObjects, isArrayOfStrings, isString, validProperties } from './helpers.js' +import { + functionsDirectoryCheck, + isArrayOfObjects, + isArrayOfStrings, + isBoolean, + isString, + validProperties, +} from './helpers.js' /** * @param {string} cron @@ -225,6 +232,12 @@ export const POST_NORMALIZE_VALIDATIONS = [ message: 'must be a string.', example: () => ({ build: { edge_functions: 'edge-functions' } }), }, + { + property: 'build.spa', + check: isBoolean, + message: 'must be a boolean.', + example: () => ({ build: { spa: true } }), + }, { property: 'functions.*', check: isPlainObj, diff --git a/packages/config/tests/validate/snapshots/tests.js.md b/packages/config/tests/validate/snapshots/tests.js.md index a6aedf6af0..c23a0e64d4 100644 --- a/packages/config/tests/validate/snapshots/tests.js.md +++ b/packages/config/tests/validate/snapshots/tests.js.md @@ -497,6 +497,142 @@ Generated by [AVA](https://avajs.dev). `When resolving config file packages/config/tests/validate/fixtures/build_edge_functions_parent/netlify.toml:␊ Configuration property "build.edge_functions" "edge_functions_dir" must be inside the repository root directory.` +## build.spa: boolean + +> Snapshot 1 + + `{␊ + "accounts": [],␊ + "branch": "branch",␊ + "buildDir": "packages/config/tests/validate/fixtures/build_spa_boolean",␊ + "config": {␊ + "build": {␊ + "environment": {},␊ + "processing": {␊ + "css": {},␊ + "html": {},␊ + "images": {},␊ + "js": {}␊ + },␊ + "publish": "packages/config/tests/validate/fixtures/build_spa_boolean/publish",␊ + "publishOrigin": "config",␊ + "services": {},␊ + "spa": "true"␊ + },␊ + "functions": {␊ + "*": {}␊ + },␊ + "headers": [],␊ + "plugins": [],␊ + "redirects": []␊ + },␊ + "configPath": "packages/config/tests/validate/fixtures/build_spa_boolean/netlify.toml",␊ + "context": "production",␊ + "env": {␊ + "BRANCH": {␊ + "sources": [␊ + "general"␊ + ],␊ + "value": "branch"␊ + },␊ + "BUILD_ID": {␊ + "sources": [␊ + "general"␊ + ],␊ + "value": "0"␊ + },␊ + "CACHED_COMMIT_REF": {␊ + "sources": [␊ + "general"␊ + ],␊ + "value": "HEXADECIMAL_ID"␊ + },␊ + "COMMIT_REF": {␊ + "sources": [␊ + "general"␊ + ],␊ + "value": "HEXADECIMAL_ID"␊ + },␊ + "CONTEXT": {␊ + "sources": [␊ + "general"␊ + ],␊ + "value": "production"␊ + },␊ + "DEPLOY_ID": {␊ + "sources": [␊ + "general"␊ + ],␊ + "value": "0"␊ + },␊ + "DEPLOY_PRIME_URL": {␊ + "sources": [␊ + "general"␊ + ],␊ + "value": "https://branch--site-name.netlify.app"␊ + },␊ + "DEPLOY_URL": {␊ + "sources": [␊ + "general"␊ + ],␊ + "value": "https://0--site-name.netlify.app"␊ + },␊ + "GATSBY_TELEMETRY_DISABLED": {␊ + "sources": [␊ + "general"␊ + ],␊ + "value": "1"␊ + },␊ + "HEAD": {␊ + "sources": [␊ + "general"␊ + ],␊ + "value": "branch"␊ + },␊ + "LANG": {␊ + "sources": [␊ + "general"␊ + ],␊ + "value": "en_US.UTF-8"␊ + },␊ + "LANGUAGE": {␊ + "sources": [␊ + "general"␊ + ],␊ + "value": "en_US:en"␊ + },␊ + "LC_ALL": {␊ + "sources": [␊ + "general"␊ + ],␊ + "value": "en_US.UTF-8"␊ + },␊ + "NETLIFY_LOCAL": {␊ + "sources": [␊ + "general"␊ + ],␊ + "value": "true"␊ + },␊ + "NEXT_TELEMETRY_DISABLED": {␊ + "sources": [␊ + "general"␊ + ],␊ + "value": "1"␊ + },␊ + "PULL_REQUEST": {␊ + "sources": [␊ + "general"␊ + ],␊ + "value": "false"␊ + }␊ + },␊ + "headersPath": "packages/config/tests/validate/fixtures/build_spa_boolean/publish/_headers",␊ + "integrations": [],␊ + "redirectsPath": "packages/config/tests/validate/fixtures/build_spa_boolean/publish/_redirects",␊ + "repositoryRoot": "packages/config/tests/validate/fixtures/build_spa_boolean",␊ + "siteInfo": {}␊ + }` + ## build.base: string > Snapshot 1 diff --git a/packages/config/tests/validate/snapshots/tests.js.snap b/packages/config/tests/validate/snapshots/tests.js.snap index 84cf4386415e7c09d7686d5e82db075445dd5511..ec124f85c3ef44534494c965fe8a716e232fffa4 100644 GIT binary patch literal 4785 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≪`nop<@zIy%0DQR%Fxc3 zQM^%R1Qb79J-5ANB`YsUOqAhxM;V!yfjY?f2-|9EE%vFmHYM%!YU@czaB}$4yAt3K z$hiW6+w1{zf2UFT9rM{s&-&>RCuwyN_7H|IbZt+6C3{9X&)aj(fjiNvJSwBDW)3mO zgcdmrF7Nz#B}@`;(3~(yFK_(xb>zqQIh>jDk zUdiQ3si=XrKh|jbT9vffWGD|%S~B!KCxbIz1dszp%1BW>MD&c4`>HO6eHga`BK#DS zDOS!9GSK_q8omEf0lm@;eq!57yt41tkDbs^pB50T=%*xg8UQpoiu?yW75U{+e8G1p z!!>&|xfUzte+-<#81i2eNXNwGKgBRL|H)>M%%AEtFgqnoxlx=wKafa+c6jCxlVe-N0(@z@+XGC=3-UG0} zm|Mcm<>QJ&bOP5iS2XiIAa0UiJ~3t0u9l~C7O$TrC0o3blZZx+=bfdsJ^#xSBzP*l zqDoEGE&FCJ(V5VI1EM+DBH}Y!|0-_tWqN>GP3Z|4G^+H7Ebu$P`XWc<6+;_eKz3E` zv)GW>!Co^sSp1hv!t%$r-qBp>GgT#Dg-)NJ?JZ_!nCA{~nXp$7&N*edtOoyU20kXl zg&npmnoOY!_~I8s#M}6Z#>T%@zcx7=53^BnY2}N)DtS@WRka7vhT% zRiR#|oF~1^b;|R)yh>Ln&sAL7+T?{nWzHVnjoiT5Md6~@`HHW_&Q2Ah#jGN!DXS(k zJm#BOLL&meO+bdjrE?k~i6x<96gH;>)8>=}#x*i?i_741#e8SeL>r&VSdO^A)rkAc zDv0COlt7&jl;KVtt?YoC-QFfRx_h7fd(8gX?d{##1pD2?Zm+uu?jH3H?%uh-G{sV5 zGSqmp+q2Z3vcRZjR@BjPB1%{n4_vO|1X~6tU@=9S^dbyY)D7A1KWL8dy|rh*%-&0u z^(IwTWtG)OcFROW->DHNUo$1z@Zk_4YneGdm>GG~_ zof)oBSXE}528=lw?VEq81*%T*f(5nfe5f#dpxP6B{r0YP#hT zp?Fb20-Ex{Ti)BewcXlkZB@C(z%&IcD!q8p@!ID!ziMAwciJTky`^bCA(_CY=rT6-2Zn^b1?9KPGl0FYIh;;ooz2zRT%FC;*<78? z)!Ez&oXurAy>;ewS=Ci#c&6AiC)YDggT*oU64PK;Z);)r=PF3=WM$4j4JHK59`_>- zoRw?%pTe_{uk7s;;z9OI5Z@9QEW37-`)=m4G(l_{=0T~lLzy++xFLuwsJv~+XNP=) zR7Gn*mob*X*6hQk&(dv7V`G$??9dbKfwUI#iHSo8V-bJz)yu!me{TN{|KIi3Ua|fk LGdTbBoxlJ9i;!Vz literal 4649 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 { t.snapshot(normalizeOutput(output)) }) +test('build.spa: boolean', async (t) => { + const output = await new Fixture('./fixtures/build_spa_boolean').runWithConfig() + t.snapshot(normalizeOutput(output)) +}) + test('build.base: string', async (t) => { const output = await new Fixture('./fixtures/build_base_string').runWithConfig() t.snapshot(normalizeOutput(output)) From 1c3b2de6f5aaf8a3d7a7c5339b72d54e194f8d8c Mon Sep 17 00:00:00 2001 From: Hrishikesh Kokate Date: Wed, 22 Jul 2026 19:10:18 +0530 Subject: [PATCH 02/11] add build.spa in frameworks api as well --- .../src/plugins_core/frameworks_api/index.ts | 1 + packages/build/tests/frameworks_api/tests.js | 7 + packages/config/src/mutations/apply.js | 1 + packages/config/src/normalize.ts | 1 - .../tests/validate/snapshots/tests.js.md | 143 ++---------------- .../tests/validate/snapshots/tests.js.snap | Bin 4785 -> 4697 bytes 6 files changed, 21 insertions(+), 132 deletions(-) diff --git a/packages/build/src/plugins_core/frameworks_api/index.ts b/packages/build/src/plugins_core/frameworks_api/index.ts index 0b6970f3b4..8faad1c9a1 100644 --- a/packages/build/src/plugins_core/frameworks_api/index.ts +++ b/packages/build/src/plugins_core/frameworks_api/index.ts @@ -17,6 +17,7 @@ import { filterConfig, loadConfigFile } from './util.js' const ALLOWED_PROPERTIES = [ ['build', 'functions'], ['build', 'publish'], + ['build', 'spa'], ['edge_functions'], ['functions', '*'], ['functions', '*', '*'], diff --git a/packages/build/tests/frameworks_api/tests.js b/packages/build/tests/frameworks_api/tests.js index aae2f8eb74..133ffad478 100644 --- a/packages/build/tests/frameworks_api/tests.js +++ b/packages/build/tests/frameworks_api/tests.js @@ -89,6 +89,13 @@ 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('./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', diff --git a/packages/config/src/mutations/apply.js b/packages/config/src/mutations/apply.js index e8096c2a55..8e99cee6d6 100644 --- a/packages/config/src/mutations/apply.js +++ b/packages/config/src/mutations/apply.js @@ -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 }, diff --git a/packages/config/src/normalize.ts b/packages/config/src/normalize.ts index 657ca376b9..6e1e6f1481 100644 --- a/packages/config/src/normalize.ts +++ b/packages/config/src/normalize.ts @@ -30,7 +30,6 @@ const DEFAULT_CONFIG = (packagePath?: string) => ({ publishOrigin: DEFAULT_ORIGIN, processing: { css: {}, html: {}, images: {}, js: {} }, services: {}, - spa: false, }, functions: { [WILDCARD_ALL]: {} }, plugins: [], diff --git a/packages/config/tests/validate/snapshots/tests.js.md b/packages/config/tests/validate/snapshots/tests.js.md index c23a0e64d4..d133b61c52 100644 --- a/packages/config/tests/validate/snapshots/tests.js.md +++ b/packages/config/tests/validate/snapshots/tests.js.md @@ -501,137 +501,18 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 - `{␊ - "accounts": [],␊ - "branch": "branch",␊ - "buildDir": "packages/config/tests/validate/fixtures/build_spa_boolean",␊ - "config": {␊ - "build": {␊ - "environment": {},␊ - "processing": {␊ - "css": {},␊ - "html": {},␊ - "images": {},␊ - "js": {}␊ - },␊ - "publish": "packages/config/tests/validate/fixtures/build_spa_boolean/publish",␊ - "publishOrigin": "config",␊ - "services": {},␊ - "spa": "true"␊ - },␊ - "functions": {␊ - "*": {}␊ - },␊ - "headers": [],␊ - "plugins": [],␊ - "redirects": []␊ - },␊ - "configPath": "packages/config/tests/validate/fixtures/build_spa_boolean/netlify.toml",␊ - "context": "production",␊ - "env": {␊ - "BRANCH": {␊ - "sources": [␊ - "general"␊ - ],␊ - "value": "branch"␊ - },␊ - "BUILD_ID": {␊ - "sources": [␊ - "general"␊ - ],␊ - "value": "0"␊ - },␊ - "CACHED_COMMIT_REF": {␊ - "sources": [␊ - "general"␊ - ],␊ - "value": "HEXADECIMAL_ID"␊ - },␊ - "COMMIT_REF": {␊ - "sources": [␊ - "general"␊ - ],␊ - "value": "HEXADECIMAL_ID"␊ - },␊ - "CONTEXT": {␊ - "sources": [␊ - "general"␊ - ],␊ - "value": "production"␊ - },␊ - "DEPLOY_ID": {␊ - "sources": [␊ - "general"␊ - ],␊ - "value": "0"␊ - },␊ - "DEPLOY_PRIME_URL": {␊ - "sources": [␊ - "general"␊ - ],␊ - "value": "https://branch--site-name.netlify.app"␊ - },␊ - "DEPLOY_URL": {␊ - "sources": [␊ - "general"␊ - ],␊ - "value": "https://0--site-name.netlify.app"␊ - },␊ - "GATSBY_TELEMETRY_DISABLED": {␊ - "sources": [␊ - "general"␊ - ],␊ - "value": "1"␊ - },␊ - "HEAD": {␊ - "sources": [␊ - "general"␊ - ],␊ - "value": "branch"␊ - },␊ - "LANG": {␊ - "sources": [␊ - "general"␊ - ],␊ - "value": "en_US.UTF-8"␊ - },␊ - "LANGUAGE": {␊ - "sources": [␊ - "general"␊ - ],␊ - "value": "en_US:en"␊ - },␊ - "LC_ALL": {␊ - "sources": [␊ - "general"␊ - ],␊ - "value": "en_US.UTF-8"␊ - },␊ - "NETLIFY_LOCAL": {␊ - "sources": [␊ - "general"␊ - ],␊ - "value": "true"␊ - },␊ - "NEXT_TELEMETRY_DISABLED": {␊ - "sources": [␊ - "general"␊ - ],␊ - "value": "1"␊ - },␊ - "PULL_REQUEST": {␊ - "sources": [␊ - "general"␊ - ],␊ - "value": "false"␊ - }␊ - },␊ - "headersPath": "packages/config/tests/validate/fixtures/build_spa_boolean/publish/_headers",␊ - "integrations": [],␊ - "redirectsPath": "packages/config/tests/validate/fixtures/build_spa_boolean/publish/_redirects",␊ - "repositoryRoot": "packages/config/tests/validate/fixtures/build_spa_boolean",␊ - "siteInfo": {}␊ - }` + `When resolving config file packages/config/tests/validate/fixtures/build_spa_boolean/netlify.toml:␊ + Configuration property build.spa must be a boolean.␊ + ␊ + Invalid syntax␊ + ␊ + [build]␊ + spa = "true"␊ + ␊ + Valid syntax␊ + ␊ + [build]␊ + spa = true` ## build.base: string diff --git a/packages/config/tests/validate/snapshots/tests.js.snap b/packages/config/tests/validate/snapshots/tests.js.snap index ec124f85c3ef44534494c965fe8a716e232fffa4..862b43aa245e98774adde8da093e309680f847fd 100644 GIT binary patch literal 4697 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 literal 4785 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≪`nop<@zIy%0DQR%Fxc3 zQM^%R1Qb79J-5ANB`YsUOqAhxM;V!yfjY?f2-|9EE%vFmHYM%!YU@czaB}$4yAt3K z$hiW6+w1{zf2UFT9rM{s&-&>RCuwyN_7H|IbZt+6C3{9X&)aj(fjiNvJSwBDW)3mO zgcdmrF7Nz#B}@`;(3~(yFK_(xb>zqQIh>jDk zUdiQ3si=XrKh|jbT9vffWGD|%S~B!KCxbIz1dszp%1BW>MD&c4`>HO6eHga`BK#DS zDOS!9GSK_q8omEf0lm@;eq!57yt41tkDbs^pB50T=%*xg8UQpoiu?yW75U{+e8G1p z!!>&|xfUzte+-<#81i2eNXNwGKgBRL|H)>M%%AEtFgqnoxlx=wKafa+c6jCxlVe-N0(@z@+XGC=3-UG0} zm|Mcm<>QJ&bOP5iS2XiIAa0UiJ~3t0u9l~C7O$TrC0o3blZZx+=bfdsJ^#xSBzP*l zqDoEGE&FCJ(V5VI1EM+DBH}Y!|0-_tWqN>GP3Z|4G^+H7Ebu$P`XWc<6+;_eKz3E` zv)GW>!Co^sSp1hv!t%$r-qBp>GgT#Dg-)NJ?JZ_!nCA{~nXp$7&N*edtOoyU20kXl zg&npmnoOY!_~I8s#M}6Z#>T%@zcx7=53^BnY2}N)DtS@WRka7vhT% zRiR#|oF~1^b;|R)yh>Ln&sAL7+T?{nWzHVnjoiT5Md6~@`HHW_&Q2Ah#jGN!DXS(k zJm#BOLL&meO+bdjrE?k~i6x<96gH;>)8>=}#x*i?i_741#e8SeL>r&VSdO^A)rkAc zDv0COlt7&jl;KVtt?YoC-QFfRx_h7fd(8gX?d{##1pD2?Zm+uu?jH3H?%uh-G{sV5 zGSqmp+q2Z3vcRZjR@BjPB1%{n4_vO|1X~6tU@=9S^dbyY)D7A1KWL8dy|rh*%-&0u z^(IwTWtG)OcFROW->DHNUo$1z@Zk_4YneGdm>GG~_ zof)oBSXE}528=lw?VEq81*%T*f(5nfe5f#dpxP6B{r0YP#hT zp?Fb20-Ex{Ti)BewcXlkZB@C(z%&IcD!q8p@!ID!ziMAwciJTky`^bCA(_CY=rT6-2Zn^b1?9KPGl0FYIh;;ooz2zRT%FC;*<78? z)!Ez&oXurAy>;ewS=Ci#c&6AiC)YDggT*oU64PK;Z);)r=PF3=WM$4j4JHK59`_>- zoRw?%pTe_{uk7s;;z9OI5Z@9QEW37-`)=m4G(l_{=0T~lLzy++xFLuwsJv~+XNP=) zR7Gn*mob*X*6hQk&(dv7V`G$??9dbKfwUI#iHSo8V-bJz)yu!me{TN{|KIi3Ua|fk LGdTbBoxlJ9i;!Vz From c0ffd123a9bc45785fac56f956cf261f2c3967ee Mon Sep 17 00:00:00 2001 From: Hrishikesh Kokate Date: Wed, 22 Jul 2026 21:38:06 +0530 Subject: [PATCH 03/11] add fallback rewrite --- eslint_temporary_suppressions.js | 6 +++ .../src/plugins_core/spa_fallback/index.ts | 51 ++++++++++++++++++ packages/build/src/steps/get.ts | 2 + .../build/tests/core/snapshots/tests.js.md | 48 ++++++++--------- .../build/tests/core/snapshots/tests.js.snap | Bin 5958 -> 5964 bytes .../fixtures/spa_config/.gitignore | 2 + .../fixtures/spa_config/build.mjs | 11 ++++ .../fixtures/spa_config/netlify.toml | 2 + .../fixtures/spa_default/netlify.toml | 2 + .../fixtures/spa_disabled/netlify.toml | 3 ++ .../fixtures/spa_enabled/netlify.toml | 3 ++ .../netlify.toml | 8 +++ packages/build/tests/spa_fallback/tests.js | 42 +++++++++++++++ .../tests/telemetry/snapshots/tests.js.md | 10 ++-- .../tests/telemetry/snapshots/tests.js.snap | Bin 1787 -> 1785 bytes .../build/tests/time/snapshots/tests.js.md | 5 ++ .../build/tests/time/snapshots/tests.js.snap | Bin 599 -> 615 bytes .../fixtures/build_spa_boolean/netlify.toml | 3 ++ 18 files changed, 169 insertions(+), 29 deletions(-) create mode 100644 packages/build/src/plugins_core/spa_fallback/index.ts create mode 100644 packages/build/tests/frameworks_api/fixtures/spa_config/.gitignore create mode 100644 packages/build/tests/frameworks_api/fixtures/spa_config/build.mjs create mode 100644 packages/build/tests/frameworks_api/fixtures/spa_config/netlify.toml create mode 100644 packages/build/tests/spa_fallback/fixtures/spa_default/netlify.toml create mode 100644 packages/build/tests/spa_fallback/fixtures/spa_disabled/netlify.toml create mode 100644 packages/build/tests/spa_fallback/fixtures/spa_enabled/netlify.toml create mode 100644 packages/build/tests/spa_fallback/fixtures/spa_enabled_existing_catch_all/netlify.toml create mode 100644 packages/build/tests/spa_fallback/tests.js create mode 100644 packages/config/tests/validate/fixtures/build_spa_boolean/netlify.toml diff --git a/eslint_temporary_suppressions.js b/eslint_temporary_suppressions.js index 2ed0c0edec..0a93a1f4fe 100644 --- a/eslint_temporary_suppressions.js +++ b/eslint_temporary_suppressions.js @@ -1103,6 +1103,12 @@ export default [ '@typescript-eslint/consistent-indexed-object-style': 'off', }, }, + { + 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: { diff --git a/packages/build/src/plugins_core/spa_fallback/index.ts b/packages/build/src/plugins_core/spa_fallback/index.ts new file mode 100644 index 0000000000..2d366990e3 --- /dev/null +++ b/packages/build/src/plugins_core/spa_fallback/index.ts @@ -0,0 +1,51 @@ +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 { + if ( + !coreStepFunctionArgs.netlifyConfig.build.spa + || hasCatchAllRedirect(coreStepFunctionArgs.netlifyConfig.redirects) + ) { + return Promise.resolve({}) + } + + const newConfig: Partial = { + 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, +} diff --git a/packages/build/src/steps/get.ts b/packages/build/src/steps/get.ts index 4a5ef12876..11b333a3ee 100644 --- a/packages/build/src/steps/get.ts +++ b/packages/build/src/steps/get.ts @@ -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 @@ -85,6 +86,7 @@ const addCoreSteps = function (steps): CoreStep[] { bundleFunctions, bundleEdgeFunctions, copyDbMigrations, + applySpaFallback, scanForSecrets, uploadBlobs, deploySite, diff --git a/packages/build/tests/core/snapshots/tests.js.md b/packages/build/tests/core/snapshots/tests.js.md index fbc35ef89b..2bd17153ca 100644 --- a/packages/build/tests/core/snapshots/tests.js.md +++ b/packages/build/tests/core/snapshots/tests.js.md @@ -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␊ ` @@ -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␊ ` @@ -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␊ ` @@ -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␊ ` diff --git a/packages/build/tests/core/snapshots/tests.js.snap b/packages/build/tests/core/snapshots/tests.js.snap index 98c0306cefe20292abc341f767845e531842c08b..f01549d261a79aa0afbdbd685b6737a26a6f2b5d 100644 GIT binary patch literal 5964 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 uzs%GT**EBlbK+FCB2)l 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 diff --git a/packages/build/tests/frameworks_api/fixtures/spa_config/.gitignore b/packages/build/tests/frameworks_api/fixtures/spa_config/.gitignore new file mode 100644 index 0000000000..ec6cc0eb0d --- /dev/null +++ b/packages/build/tests/frameworks_api/fixtures/spa_config/.gitignore @@ -0,0 +1,2 @@ +# .netlify/v1/config.json is generated at build time by build.mjs. +.netlify diff --git a/packages/build/tests/frameworks_api/fixtures/spa_config/build.mjs b/packages/build/tests/frameworks_api/fixtures/spa_config/build.mjs new file mode 100644 index 0000000000..a5bd3486c5 --- /dev/null +++ b/packages/build/tests/frameworks_api/fixtures/spa_config/build.mjs @@ -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)) diff --git a/packages/build/tests/frameworks_api/fixtures/spa_config/netlify.toml b/packages/build/tests/frameworks_api/fixtures/spa_config/netlify.toml new file mode 100644 index 0000000000..3b2b6da08b --- /dev/null +++ b/packages/build/tests/frameworks_api/fixtures/spa_config/netlify.toml @@ -0,0 +1,2 @@ +[build] +command = "node build.mjs" diff --git a/packages/build/tests/spa_fallback/fixtures/spa_default/netlify.toml b/packages/build/tests/spa_fallback/fixtures/spa_default/netlify.toml new file mode 100644 index 0000000000..85f658d6f6 --- /dev/null +++ b/packages/build/tests/spa_fallback/fixtures/spa_default/netlify.toml @@ -0,0 +1,2 @@ +[build] +command = "echo hi" diff --git a/packages/build/tests/spa_fallback/fixtures/spa_disabled/netlify.toml b/packages/build/tests/spa_fallback/fixtures/spa_disabled/netlify.toml new file mode 100644 index 0000000000..2a45d5b2c7 --- /dev/null +++ b/packages/build/tests/spa_fallback/fixtures/spa_disabled/netlify.toml @@ -0,0 +1,3 @@ +[build] +command = "echo hi" +spa = false diff --git a/packages/build/tests/spa_fallback/fixtures/spa_enabled/netlify.toml b/packages/build/tests/spa_fallback/fixtures/spa_enabled/netlify.toml new file mode 100644 index 0000000000..171ab2c9d7 --- /dev/null +++ b/packages/build/tests/spa_fallback/fixtures/spa_enabled/netlify.toml @@ -0,0 +1,3 @@ +[build] +command = "echo hi" +spa = true diff --git a/packages/build/tests/spa_fallback/fixtures/spa_enabled_existing_catch_all/netlify.toml b/packages/build/tests/spa_fallback/fixtures/spa_enabled_existing_catch_all/netlify.toml new file mode 100644 index 0000000000..6c3f4812da --- /dev/null +++ b/packages/build/tests/spa_fallback/fixtures/spa_enabled_existing_catch_all/netlify.toml @@ -0,0 +1,8 @@ +[build] +command = "echo hi" +spa = true + +[[redirects]] +from = "/*" +to = "/200.html" +status = 200 diff --git a/packages/build/tests/spa_fallback/tests.js b/packages/build/tests/spa_fallback/tests.js new file mode 100644 index 0000000000..7d2ab81115 --- /dev/null +++ b/packages/build/tests/spa_fallback/tests.js @@ -0,0 +1,42 @@ +import { Fixture } from '@netlify/testing' +import test from 'ava' + +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('./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('./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('./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( + './fixtures/spa_enabled_existing_catch_all', + ).runWithBuildAndIntrospect() + + t.true(success) + t.deepEqual(netlifyConfig.redirects, [{ ...SPA_FALLBACK_REDIRECT, to: '/200.html' }]) +}) diff --git a/packages/build/tests/telemetry/snapshots/tests.js.md b/packages/build/tests/telemetry/snapshots/tests.js.md index 58dc8aea3d..36329e0ca0 100644 --- a/packages/build/tests/telemetry/snapshots/tests.js.md +++ b/packages/build/tests/telemetry/snapshots/tests.js.md @@ -121,7 +121,7 @@ Generated by [AVA](https://avajs.dev). plugins: [], siteId: 'test', status: 'success', - steps: 2, + steps: 3, }, timestamp: 'number', userId: 'buildbot_user', @@ -157,7 +157,7 @@ Generated by [AVA](https://avajs.dev). ], siteId: 'test', status: 'success', - steps: 3, + steps: 4, }, timestamp: 'number', userId: 'buildbot_user', @@ -193,7 +193,7 @@ Generated by [AVA](https://avajs.dev). ], siteId: 'test', status: 'success', - steps: 3, + steps: 4, }, timestamp: 'number', userId: 'buildbot_user', @@ -229,7 +229,7 @@ Generated by [AVA](https://avajs.dev). ], siteId: 'test', status: 'success', - steps: 3, + steps: 4, }, timestamp: 'number', userId: 'buildbot_user', @@ -265,7 +265,7 @@ Generated by [AVA](https://avajs.dev). ], siteId: 'test', status: 'success', - steps: 3, + steps: 4, }, timestamp: 'number', userId: 'buildbot_user', diff --git a/packages/build/tests/telemetry/snapshots/tests.js.snap b/packages/build/tests/telemetry/snapshots/tests.js.snap index 38d2b9c5f219cb27074bb8786e44abca6a8c1db2..549ca4cc5baff5802e22d846b7c540c40b2ef3ce 100644 GIT binary patch delta 1645 zcmV-z29o*v4fzc+K~_N^Q*L2!b7*gLAa*kf0|1nUSY!n!IrLI!X6CpMw3GCganKOL z1>pdTJOS=AixQDCaDO}f=c})(zCXXN?*8WOL7&Uus($@Cn%Ho>_KgkXI6|YwRHs3J zTGI#xDrQhqAt{%Uj^77dRFWBNoI^N`-` z$N&4AKRVKXzW*5gE-D>M;Zyp!$gR=sXV$oHX?bbcN_?BMNPqj3hS*P|9yKaOpHhX1 z)UlCjd`U{vqX`SHvIuqFx@%Ay-8H3~x`X(dNfqjD;V++39UEK=d(?{hH2YdljPCi? zwy2a;&q!f=sQRQd;b{~LeIeH7vf@=+tBj*C7h(~iibZr$N!ttRiMG{X)>RzEqDQkq zcC`$yqS-025r2yhG}4I?9}pT zRgMyl2ERgibk9x;1ce1c8Vh>gNZ;${$F;$PhN)uKY=1qvccT(^>)Wu)vVuy%$65X< zqp6X_(NUf(l!=grGUK43Y%svU77MqK{!7jDVZj*h@N74xQkF%)D#zLHEyAb&Db`X(3XlJ6CuZNF^5TZ zFi-1?d7M72rC99I0pmJ9`^4q73r}vYJ!v%+%B2~iTI|u6vtYf2A^McQ;=fW}eZIck zpuFVlb2ahqt*$2X#5o55_XEIL(mq34-%^T=34bZKPMtah#0NrvED-WY*{CG}AO(Qe z0pM-|@DwQ&wAcGrFfhcaHcy{E4det656DSEt`LCRhzq23hIoA$j)9zlv&1Qn%?*)- zsC2pi5(@%M49&kfC;}PUSSt286H&?{q%1W2iFb`+!PU%KKPLdc zBmjRS03Q>8yPcY~+}*6TLv;(cP?&PBKxGhbW*eJHe#zLZk}$LNbI!J{{Y1$Gm5EW; zG#@kZ2(@9Oq#C+NM}1WF`CH4ZRr5cQ^?%%#X44+G(_DDV0l4k}yzc<~#{sz01z2?f zxC`)x3-D_f;5`@MBeynvZ)*B}t@GnqZ^aeA8j&`ZD>c#il2Ch3XFXmA;V?Bc)6&#F>G3QG_Yx4^V!1Yr!K&6T!25h0RL)W zGYK1;Tqg-R`FYXC%P{bO2e9J74Y4zUfmN@>z?KK_Ll59r9>Dt^z<<4(XWRP*_ChSQ zVVrM_lgh-c3}iJyPfR#rVX2#&Xn*NSeQpPL?A+F_QYB!QrU6^r`Yc`1QXGJ@=Nis7 zoNYMUaCTOl{jCS^R}Y}wnh9r*Px5Rp+@39-6m@cYb#^hx%&_~wG$6?*I%#|)V4J@$3YZ9x^DO3*y9G9XYFii26Xzc;z=!UE5cxm1Ox|-I z!2KS;_dI}~cmVHwnV!DhKFvfMp!pm4X%@QQZ&=u{aONy*zvckE;Q;*J0rhc ztWl1C7dJs!YG?WTPZ!|d2V186zgnhrJ%EQifEPZ!i+j}r_}!OjnKDru7%2k z?T^bU*IjDIH+IyHpKof%k4o+M=k}wT<)&!7DH=ZlMdL-lp5Ly}uGfuk43XO9LB4u= rk!i~MQrQl}6zxxQ3WCn4n*8Rv`$AQjA9&~tdG+}x69p^C@;LwigQG6} delta 1647 zcmV-#29Wvr4f_o;K~_N^Q*L2!b7*gLAa*kf0|3L?Qnjgw7g7MCuD*S6PHQZ{0PhjqaLqnYx4cnn@MvZn0iIr8+ja7WSwW^=bBRJu$lH zTic>iQavMu?V;+E(uAi`EcAs~o6CwjsVu z9FGzlegFI<^gF@3HH*;RzJxlFLL&`i zh&}qy@_%wJ7vu|SMQRYv??+0eNi3qLm>%vT)D<M^ySQ1Z()c&rLXv}l)KN@ zw;PmO&OW<|cW0kS~IBW0tO1b`F( zUI&1?3BXgNP|#lQU%|i-r#d`+`ZSOeKs+EP3AsW5ZX+&`)*0gUWjF?M3eFOzyf!yP z7NXMS`AaMaFfla$*Fh1;(E3tw&Y6f(79nM+8A=tul=-Gbiol>eG$>4mqlnC~IyK`2 z>3>jaLp27S0h(ag8Oac7s!?@Vgu>KD)*hi5%CH*z#r5xO)I{7=@k+dF6br6q*7`XC z_$2}O8v*#30Nm}=tmW=ztsSacxP`)$X9X&Qcr!cLO!6&bvkJn@*3UWHw)PVx6I3Qf zUDJHb#3R&(jgo5UA|3Tn)#q<5vsTUjM1R(EUz$yO-A;4iEeGJb1Mt2B@E-@@P8VR+ z1>i2g8!o`FU4ZvofREhT^u4L+`?b!GXT23Wel;R(ELUox^ChA7p3Z`hAs$F!Tk2=o z2=^B9rM}3S8AvtSTW}%g#i~q;<@6w1tP|e$V$Bt)LVX~4-cfm=-3+Hh3^QpJJ%4ir z1M1FXNuCRN!~rtDBluer!Cyk=o70ftGGM$+YHkdh)*TIO8rXccu=%MA@EaH4PcFc} z8rV$2#s=3(LQZ~Ov~e>GJm3MWcyL4POkiNuD>1O;0sPPd_>~9nz6bDMueP%7eI0ut z7TPe*H^xb2;#LN-nxH2p7GYtjn}3^V=}LWW2VdB^tzD%`z%ES#w)pC^bVW;X0M4Fk zINNZx;cUa%S#kEa9>8BcfOcypoIO6tv%PS8ws=z1$?etI#UL}o?gP`J%I0d06*~n-uW^;eZ75}iPk~$H}KOebid!QuwmiMS=fHf0eHg!_`L)0fdjDM z0z9}s|5~%<-?3DLIEaNc!XYBC#wb7Yn=IhUsJwHRcOn@HZWNc^Uz)gV;r{CgU^#FeNWlB>fY6Antxuzzs z^riiAS>?J*?fAxy+VS&E?f6lt9sk^ZRI}U^jW@r@x;n;zuZ t(~C?~)|bk57^Y}{no|&TM%Cmu*WDMY%KX4X&kU>2KLJiij7ai10013FH@E-* diff --git a/packages/build/tests/time/snapshots/tests.js.md b/packages/build/tests/time/snapshots/tests.js.md index 61e615840e..0247116fe2 100644 --- a/packages/build/tests/time/snapshots/tests.js.md +++ b/packages/build/tests/time/snapshots/tests.js.md @@ -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` @@ -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` @@ -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` @@ -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` @@ -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` diff --git a/packages/build/tests/time/snapshots/tests.js.snap b/packages/build/tests/time/snapshots/tests.js.snap index 04c785367d92a52f6a75ef558e33bd33c76c12d8..c5a23478b067737403cf5baf633fabcb011a4b39 100644 GIT binary patch literal 615 zcmV-t0+{_lRzVuhC1>h9 zTlNcgMO_$}`45ne@%{t;JCyXtwWWpJ;c_4)QN8-d+BnjE*d@7p;$E|s}hZ4`M zp9Bd`%gj=)iwN8TSuV<#*41Z21ghmz^f5rrqs$BfRW1U}W3c7}#+$w1K;~df3fUwE zj=`^PM-1LHFqV5k(xI0;b=aO*1@yn?EwlIhEs_eWu-RO^p0^hzXkyTWYJd0!-#5Z{ zIq1UIifSyVh!4@S+ngu~&-wiZcX6p36N6?f!Tk}^(#yKVlCg8lv1p6&UI+Ka29Ar2 zCfH^GF%|KTD|_wn>K1@QF92IFUuz3NE$50fPxt%(=<$aCA0Bo7zYYI)s_9hoAE;)y z{;nx<>qTBs)?`Yg6)B^s*x{H*8;-fZ?U)rKIhkd%VST~GK$$8?3MBNvs74JQeezKU zJzCzPBnK04bOxSk=goJwi~j|_ss85Vzb~^@Epj>Cd!YxZtNW7e>~b)>JpIWos0wVQ znEuxd#eCWR4{4VJYWS64+E}CMQPhZ1C*q zON&8}Wf72+aHc9W3D%{iA_$!evg^j61Az<2mcyrWrGVkx@YiVYa8O?l#)HShpTo4$ zMx~Z#&CM(@6gX3r2iBI7qorC-nzp2QFEd+~Kx*p1BXQy@l7wt@zK~6aN1b`OhZ0Y% zp9cv}%gj=)iwN8TS+2{M*41xA1ghm@^f5rrqs$BfRW1U}W3c7}##_C>;J51+gSQBb z4b|+Q={hxW8usnNmdr*2IQnz01;S4c}Q>K+@$wR4U|ThI3qTBs)`UQ$ z6)B^sxJC-!TT=MCFNG~5Ia+4RN&UgZK$$8?3MBNvsHP1beezKUJ-EzY7E>5W7 lmqzFk^JYX{Q}-39hsYbm?Ean5XR^ Date: Wed, 22 Jul 2026 22:33:33 +0530 Subject: [PATCH 04/11] fix format --- .../build/src/plugins_core/spa_fallback/index.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/build/src/plugins_core/spa_fallback/index.ts b/packages/build/src/plugins_core/spa_fallback/index.ts index 2d366990e3..7311e45414 100644 --- a/packages/build/src/plugins_core/spa_fallback/index.ts +++ b/packages/build/src/plugins_core/spa_fallback/index.ts @@ -14,28 +14,25 @@ function hasCatchAllRedirect(redirects: NetlifyConfig['redirects']) { return redirects.some((r) => r.from === '/*') } -function coreStep(coreStepFunctionArgs: CoreStepFunctionArgs) : ReturnType { +function coreStep(coreStepFunctionArgs: CoreStepFunctionArgs): ReturnType { if ( - !coreStepFunctionArgs.netlifyConfig.build.spa - || hasCatchAllRedirect(coreStepFunctionArgs.netlifyConfig.redirects) + !coreStepFunctionArgs.netlifyConfig.build.spa || + hasCatchAllRedirect(coreStepFunctionArgs.netlifyConfig.redirects) ) { return Promise.resolve({}) } const newConfig: Partial = { - redirects: [ - ...coreStepFunctionArgs.netlifyConfig.redirects, - SPA_FALLBACK_REDIRECT, - ], + redirects: [...coreStepFunctionArgs.netlifyConfig.redirects, SPA_FALLBACK_REDIRECT], } const configMutations = getConfigMutations( coreStepFunctionArgs.netlifyConfig, { ...coreStepFunctionArgs.netlifyConfig, - ...newConfig + ...newConfig, }, - applySpaFallback.event + applySpaFallback.event, ) as unknown[] return Promise.resolve({ configMutations }) From 4467f3a0cef1818d89cbb37d674ed2131bdedfdf Mon Sep 17 00:00:00 2001 From: Hrishikesh Kokate Date: Wed, 22 Jul 2026 22:48:28 +0530 Subject: [PATCH 05/11] fix tests 1 --- packages/build/tests/frameworks_api/tests.js | 5 ++++- packages/build/tests/spa_fallback/tests.js | 16 +++++++++++++--- packages/config/tests/validate/tests.js | 2 +- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/build/tests/frameworks_api/tests.js b/packages/build/tests/frameworks_api/tests.js index 9659236be2..1d24305f98 100644 --- a/packages/build/tests/frameworks_api/tests.js +++ b/packages/build/tests/frameworks_api/tests.js @@ -96,7 +96,10 @@ 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('./fixtures/spa_config').runWithBuildAndIntrospect() + const { netlifyConfig, success } = await new Fixture( + test.meta.file, + './fixtures/spa_config', + ).runWithBuildAndIntrospect() t.true(success) t.true(netlifyConfig.build.spa) diff --git a/packages/build/tests/spa_fallback/tests.js b/packages/build/tests/spa_fallback/tests.js index 7d2ab81115..785cee20c0 100644 --- a/packages/build/tests/spa_fallback/tests.js +++ b/packages/build/tests/spa_fallback/tests.js @@ -12,21 +12,30 @@ const SPA_FALLBACK_REDIRECT = { } test('Injects an SPA fallback redirect when `build.spa` is `true`', async (t) => { - const { netlifyConfig, success } = await new Fixture('./fixtures/spa_enabled').runWithBuildAndIntrospect() + 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('./fixtures/spa_disabled').runWithBuildAndIntrospect() + 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('./fixtures/spa_default').runWithBuildAndIntrospect() + const { netlifyConfig, success } = await new Fixture( + test.meta.file, + './fixtures/spa_default', + ).runWithBuildAndIntrospect() t.true(success) t.deepEqual(netlifyConfig.redirects, []) @@ -34,6 +43,7 @@ test('Does not inject an SPA fallback redirect when `build.spa` is not set', asy 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() diff --git a/packages/config/tests/validate/tests.js b/packages/config/tests/validate/tests.js index 7a15fd658b..c94a938782 100644 --- a/packages/config/tests/validate/tests.js +++ b/packages/config/tests/validate/tests.js @@ -119,7 +119,7 @@ test('build.edge_functions: parent directory', async (t) => { }) test('build.spa: boolean', async (t) => { - const output = await new Fixture('./fixtures/build_spa_boolean').runWithConfig() + const output = await new Fixture(test.meta.file, './fixtures/build_spa_boolean').runWithConfig() t.snapshot(normalizeOutput(output)) }) From 81340daa4afb643f23966eaf5deca6e898cf27d7 Mon Sep 17 00:00:00 2001 From: Hrishikesh Kokate Date: Fri, 24 Jul 2026 22:25:24 +0530 Subject: [PATCH 06/11] change test to vitest and disable supression --- eslint_temporary_suppressions.js | 18 +++++---- packages/build/.gitignore | 1 + packages/build/package.json | 2 +- .../src/plugins/child/{diff.js => diff.ts} | 38 ++++++++++++++++--- .../src/plugins_core/spa_fallback/index.ts | 2 +- .../build/tests/frameworks_api/spa.test.ts | 12 ++++++ packages/build/tests/frameworks_api/tests.js | 10 ----- .../{tests.js => spa_fallback.test.ts} | 34 ++++++++--------- packages/build/tsconfig.build.json | 11 ++++++ packages/build/tsconfig.json | 3 +- packages/build/tsconfig.test.json | 7 ++++ packages/build/vite.config.ts | 10 +++++ tsconfig.base.json | 2 +- 13 files changed, 105 insertions(+), 45 deletions(-) create mode 100644 packages/build/.gitignore rename packages/build/src/plugins/child/{diff.js => diff.ts} (63%) create mode 100644 packages/build/tests/frameworks_api/spa.test.ts rename packages/build/tests/spa_fallback/{tests.js => spa_fallback.test.ts} (62%) create mode 100644 packages/build/tsconfig.build.json create mode 100644 packages/build/tsconfig.test.json diff --git a/eslint_temporary_suppressions.js b/eslint_temporary_suppressions.js index 0a93a1f4fe..a0f2b5451d 100644 --- a/eslint_temporary_suppressions.js +++ b/eslint_temporary_suppressions.js @@ -1019,7 +1019,6 @@ export default [ { files: ['packages/build/src/plugins_core/frameworks_api/index.ts'], rules: { - 'n/no-missing-import': 'off', '@typescript-eslint/restrict-template-expressions': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/no-unsafe-assignment': 'off', @@ -1103,12 +1102,6 @@ export default [ '@typescript-eslint/consistent-indexed-object-style': 'off', }, }, - { - 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: { @@ -2852,4 +2845,15 @@ export default [ 'import/no-named-as-default-member': 'off', }, }, + { + // Same class of gap as packages/build-info's own *.test.ts suppressions (e.g. + // tests/bin.test.ts): `@netlify/testing` isn't declared as a dependency anywhere + // (to avoid a circular dependency), so its types don't fully resolve here. + files: ['packages/build/tests/frameworks_api/spa.test.ts', 'packages/build/tests/spa_fallback/spa_fallback.test.ts'], + rules: { + 'n/no-missing-import': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + }, + }, ] diff --git a/packages/build/.gitignore b/packages/build/.gitignore new file mode 100644 index 0000000000..d350fb11a1 --- /dev/null +++ b/packages/build/.gitignore @@ -0,0 +1 @@ +tsconfig*.tsbuildinfo diff --git a/packages/build/package.json b/packages/build/package.json index e2b996a966..916371e715 100644 --- a/packages/build/package.json +++ b/packages/build/package.json @@ -24,7 +24,7 @@ "scripts": { "prebuild": "rm -rf lib", "postbuild": "npx cpy \"src/**/*.yml\" \"lib/\"", - "build": "tsc", + "build": "tsc --project tsconfig.build.json", "test:types": "tsd", "test": "ava && tsd && vitest run", "test:dev": "ava -w", diff --git a/packages/build/src/plugins/child/diff.js b/packages/build/src/plugins/child/diff.ts similarity index 63% rename from packages/build/src/plugins/child/diff.js rename to packages/build/src/plugins/child/diff.ts index d5cda92bdf..3fb20d1256 100644 --- a/packages/build/src/plugins/child/diff.js +++ b/packages/build/src/plugins/child/diff.ts @@ -5,8 +5,15 @@ import rfdc from 'rfdc' const clone = rfdc() +export type ConfigMutation = { + keys: string[] + keysString: string + value: unknown + event: string +} + // Copy `netlifyConfig` so we can compare before/after mutating it -export const cloneNetlifyConfig = function (netlifyConfig) { +export function cloneNetlifyConfig(netlifyConfig: T): T { return clone(netlifyConfig) } @@ -18,22 +25,40 @@ export const cloneNetlifyConfig = function (netlifyConfig) { // - Apply the change to `netlifyConfig` in the parent process so it can // run `@netlify/config` to normalize and validate the new values // `configMutations` is passed to parent process as JSON -export const getConfigMutations = function (netlifyConfig, netlifyConfigCopy, event) { - const configMutations = diffObjects(netlifyConfig, netlifyConfigCopy, []) + +export function getConfigMutations( + netlifyConfig: object, + netlifyConfigCopy: object, + event: string +): ConfigMutation[] { + const configMutations = diffObjects( + netlifyConfig as Record, + netlifyConfigCopy as Record, + [], + ) + return configMutations.map((configMutation) => getConfigMutation(configMutation, event)) } +type DiffResult = { keys: string[]; value: unknown } + // We only recurse over plain objects, not arrays. Which means array properties // can only be modified all at once. -const diffObjects = function (objA, objB, parentKeys) { + +function diffObjects( + objA: Record, + objB: Record, + parentKeys: string[], +): DiffResult[] { const allKeys = [...new Set([...Object.keys(objA), ...Object.keys(objB)])] + return allKeys.flatMap((key) => { const valueA = objA[key] const valueB = objB[key] const keys = [...parentKeys, key] if (isPlainObj(valueA) && isPlainObj(valueB)) { - return diffObjects(valueA, valueB, keys) + return diffObjects(valueA as Record, valueB as Record, keys) } if (isDeepStrictEqual(valueA, valueB)) { @@ -44,8 +69,9 @@ const diffObjects = function (objA, objB, parentKeys) { }) } -const getConfigMutation = function ({ keys, value }, event) { +function getConfigMutation({ keys, value }: DiffResult, event: string): ConfigMutation { const serializedKeys = keys.map(String) + return { keys: serializedKeys, keysString: serializedKeys.join('.'), diff --git a/packages/build/src/plugins_core/spa_fallback/index.ts b/packages/build/src/plugins_core/spa_fallback/index.ts index 7311e45414..0cbec5be7f 100644 --- a/packages/build/src/plugins_core/spa_fallback/index.ts +++ b/packages/build/src/plugins_core/spa_fallback/index.ts @@ -33,7 +33,7 @@ function coreStep(coreStepFunctionArgs: CoreStepFunctionArgs): ReturnType { + const { netlifyConfig, success } = await new Fixture( + import.meta.url, + './fixtures/spa_config', + ).runWithBuildAndIntrospect() + + expect(success).toBe(true) + expect(netlifyConfig.build.spa).toBe(true) +}) diff --git a/packages/build/tests/frameworks_api/tests.js b/packages/build/tests/frameworks_api/tests.js index 1d24305f98..9c62ca03ab 100644 --- a/packages/build/tests/frameworks_api/tests.js +++ b/packages/build/tests/frameworks_api/tests.js @@ -95,16 +95,6 @@ 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', diff --git a/packages/build/tests/spa_fallback/tests.js b/packages/build/tests/spa_fallback/spa_fallback.test.ts similarity index 62% rename from packages/build/tests/spa_fallback/tests.js rename to packages/build/tests/spa_fallback/spa_fallback.test.ts index 785cee20c0..8ac1842b1b 100644 --- a/packages/build/tests/spa_fallback/tests.js +++ b/packages/build/tests/spa_fallback/spa_fallback.test.ts @@ -1,5 +1,5 @@ import { Fixture } from '@netlify/testing' -import test from 'ava' +import { expect, test } from 'vitest' const SPA_FALLBACK_REDIRECT = { conditions: {}, @@ -11,42 +11,42 @@ const SPA_FALLBACK_REDIRECT = { to: '/index.html', } -test('Injects an SPA fallback redirect when `build.spa` is `true`', async (t) => { +test('Injects an SPA fallback redirect when `build.spa` is `true`', async () => { const { netlifyConfig, success } = await new Fixture( - test.meta.file, + import.meta.url, './fixtures/spa_enabled', ).runWithBuildAndIntrospect() - t.true(success) - t.deepEqual(netlifyConfig.redirects, [SPA_FALLBACK_REDIRECT]) + expect(success).toBe(true) + expect(netlifyConfig.redirects).toEqual([SPA_FALLBACK_REDIRECT]) }) -test('Does not inject an SPA fallback redirect when `build.spa` is `false`', async (t) => { +test('Does not inject an SPA fallback redirect when `build.spa` is `false`', async () => { const { netlifyConfig, success } = await new Fixture( - test.meta.file, + import.meta.url, './fixtures/spa_disabled', ).runWithBuildAndIntrospect() - t.true(success) - t.deepEqual(netlifyConfig.redirects, []) + expect(success).toBe(true) + expect(netlifyConfig.redirects).toEqual([]) }) -test('Does not inject an SPA fallback redirect when `build.spa` is not set', async (t) => { +test('Does not inject an SPA fallback redirect when `build.spa` is not set', async () => { const { netlifyConfig, success } = await new Fixture( - test.meta.file, + import.meta.url, './fixtures/spa_default', ).runWithBuildAndIntrospect() - t.true(success) - t.deepEqual(netlifyConfig.redirects, []) + expect(success).toBe(true) + expect(netlifyConfig.redirects).toEqual([]) }) -test('Does not override a catch-all redirect already declared by the user', async (t) => { +test('Does not override a catch-all redirect already declared by the user', async () => { const { netlifyConfig, success } = await new Fixture( - test.meta.file, + import.meta.url, './fixtures/spa_enabled_existing_catch_all', ).runWithBuildAndIntrospect() - t.true(success) - t.deepEqual(netlifyConfig.redirects, [{ ...SPA_FALLBACK_REDIRECT, to: '/200.html' }]) + expect(success).toBe(true) + expect(netlifyConfig.redirects).toEqual([{ ...SPA_FALLBACK_REDIRECT, to: '/200.html' }]) }) diff --git a/packages/build/tsconfig.build.json b/packages/build/tsconfig.build.json new file mode 100644 index 0000000000..28845ac036 --- /dev/null +++ b/packages/build/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": true, + "outDir": "lib" /* Specify an output folder for all emitted files. */, + "rootDir": "src", + "strictBindCallApply": false /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + }, + "include": ["src/**/*.js", "src/**/*.ts"], + "exclude": ["tests/**"] +} diff --git a/packages/build/tsconfig.json b/packages/build/tsconfig.json index fad347476b..cdf1e2286e 100644 --- a/packages/build/tsconfig.json +++ b/packages/build/tsconfig.json @@ -4,6 +4,5 @@ "outDir": "lib" /* Specify an output folder for all emitted files. */, "strictBindCallApply": false /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ }, - "include": ["src/**/*.js", "src/**/*.ts"], - "exclude": ["tests/**"] + "references": [{ "path": "./tsconfig.build.json" }, { "path": "./tsconfig.test.json" }] } diff --git a/packages/build/tsconfig.test.json b/packages/build/tsconfig.test.json new file mode 100644 index 0000000000..331a2f6358 --- /dev/null +++ b/packages/build/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["src/**/*.test.ts", "tests/**/*.test.ts"] +} diff --git a/packages/build/vite.config.ts b/packages/build/vite.config.ts index 3ac77adf8a..0bfd63b7cb 100644 --- a/packages/build/vite.config.ts +++ b/packages/build/vite.config.ts @@ -9,5 +9,15 @@ export default defineConfig({ environment: 'node', testTimeout: 100000, include: ['src/**/*.test.ts', 'tests/**/*.test.ts'], + server: { + deps: { + // These are workspace packages symlinked from outside `node_modules`, + // so Vite's default externalization heuristic treats them as source + // and re-transforms them, breaking CJS/ESM interop for their + // dependencies (e.g. `omit.js`). Force them to load as plain Node + // modules instead. + external: [/@netlify\/config/, /@netlify\/testing/, /packages\/config\//, /packages\/testing\//], + }, + }, }, }) diff --git a/tsconfig.base.json b/tsconfig.base.json index 2730ce03d3..86b1664ebf 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -27,7 +27,7 @@ "moduleResolution": "NodeNext" /* Specify how TypeScript looks up a file from a given module specifier. */, "baseUrl": "./" /* Specify the base directory to resolve non-relative module names. */, "paths": { - "@netlify/testing": ["packages/src/index.ts"] + "@netlify/testing": ["packages/testing/src/index.ts"] } /* Specify a set of entries that re-map imports to additional lookup locations. */, // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ From 9b2009fd570ed25447a066a71a43fd6408926015 Mon Sep 17 00:00:00 2001 From: Hrishikesh Kokate Date: Fri, 24 Jul 2026 22:37:28 +0530 Subject: [PATCH 07/11] add warning for invalid rewrite --- .../src/plugins_core/spa_fallback/index.ts | 42 +++++++++++++++---- .../netlify.toml | 8 ++++ .../tests/spa_fallback/spa_fallback.test.ts | 16 ++++++- 3 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 packages/build/tests/spa_fallback/fixtures/spa_enabled_existing_correct_catch_all/netlify.toml diff --git a/packages/build/src/plugins_core/spa_fallback/index.ts b/packages/build/src/plugins_core/spa_fallback/index.ts index 0cbec5be7f..5a417b0ea7 100644 --- a/packages/build/src/plugins_core/spa_fallback/index.ts +++ b/packages/build/src/plugins_core/spa_fallback/index.ts @@ -1,4 +1,5 @@ import type { NetlifyConfig } from '../../index.js' +import { logWarning } from '../../log/logger.js' import { getConfigMutations } from '../../plugins/child/diff.js' import { CoreStep, CoreStepFunction, CoreStepFunctionArgs } from '../types.js' @@ -10,26 +11,49 @@ const SPA_FALLBACK_REDIRECT = { to: '/index.html', } -function hasCatchAllRedirect(redirects: NetlifyConfig['redirects']) { - return redirects.some((r) => r.from === '/*') +function findCatchAllRedirect(redirects: NetlifyConfig['redirects']) { + return redirects.find((r) => r.from === '/*') +} + +// A catch-all redirect only serves the single-page application the way ours +// would if it rewrites (rather than redirects) to the same destination. +function matchesSpaFallback(redirect: NonNullable>) { + return redirect.to === SPA_FALLBACK_REDIRECT.to && redirect.status === SPA_FALLBACK_REDIRECT.status } function coreStep(coreStepFunctionArgs: CoreStepFunctionArgs): ReturnType { - if ( - !coreStepFunctionArgs.netlifyConfig.build.spa || - hasCatchAllRedirect(coreStepFunctionArgs.netlifyConfig.redirects) - ) { + const { netlifyConfig, logs } = coreStepFunctionArgs + + if (!netlifyConfig.build.spa) { + return Promise.resolve({}) + } + + const existingCatchAll = findCatchAllRedirect(netlifyConfig.redirects) + + if (existingCatchAll) { + // `build.spa` asked us to add a catch-all redirect, but the site already + // has one. If it doesn't already do what ours would, warn the user + // instead of silently overriding their explicit configuration. + if (!matchesSpaFallback(existingCatchAll)) { + logWarning( + logs, + ` +Warning: "build.spa" is enabled, but a catch-all redirect ("/*") already exists that does not rewrite to "${SPA_FALLBACK_REDIRECT.to}" with a "${String(SPA_FALLBACK_REDIRECT.status)}" status, so Netlify did not add its own. +Please make sure your existing catch-all redirect correctly serves your single-page application.`, + ) + } + return Promise.resolve({}) } const newConfig: Partial = { - redirects: [...coreStepFunctionArgs.netlifyConfig.redirects, SPA_FALLBACK_REDIRECT], + redirects: [...netlifyConfig.redirects, SPA_FALLBACK_REDIRECT], } const configMutations = getConfigMutations( - coreStepFunctionArgs.netlifyConfig, + netlifyConfig, { - ...coreStepFunctionArgs.netlifyConfig, + ...netlifyConfig, ...newConfig, }, applySpaFallback.event, diff --git a/packages/build/tests/spa_fallback/fixtures/spa_enabled_existing_correct_catch_all/netlify.toml b/packages/build/tests/spa_fallback/fixtures/spa_enabled_existing_correct_catch_all/netlify.toml new file mode 100644 index 0000000000..60b0e304f1 --- /dev/null +++ b/packages/build/tests/spa_fallback/fixtures/spa_enabled_existing_correct_catch_all/netlify.toml @@ -0,0 +1,8 @@ +[build] +command = "echo hi" +spa = true + +[[redirects]] +from = "/*" +to = "/index.html" +status = 200 diff --git a/packages/build/tests/spa_fallback/spa_fallback.test.ts b/packages/build/tests/spa_fallback/spa_fallback.test.ts index 8ac1842b1b..472dc4f021 100644 --- a/packages/build/tests/spa_fallback/spa_fallback.test.ts +++ b/packages/build/tests/spa_fallback/spa_fallback.test.ts @@ -41,12 +41,24 @@ test('Does not inject an SPA fallback redirect when `build.spa` is not set', asy expect(netlifyConfig.redirects).toEqual([]) }) -test('Does not override a catch-all redirect already declared by the user', async () => { - const { netlifyConfig, success } = await new Fixture( +test('Does not override a mismatched catch-all redirect already declared by the user, and warns about it', async () => { + const { netlifyConfig, success, output } = await new Fixture( import.meta.url, './fixtures/spa_enabled_existing_catch_all', ).runWithBuildAndIntrospect() expect(success).toBe(true) expect(netlifyConfig.redirects).toEqual([{ ...SPA_FALLBACK_REDIRECT, to: '/200.html' }]) + expect(output).toContain('a catch-all redirect ("/*") already exists that does not rewrite to "/index.html"') +}) + +test('Does not warn when the existing catch-all redirect already matches what Netlify would add', async () => { + const { netlifyConfig, success, output } = await new Fixture( + import.meta.url, + './fixtures/spa_enabled_existing_correct_catch_all', + ).runWithBuildAndIntrospect() + + expect(success).toBe(true) + expect(netlifyConfig.redirects).toEqual([SPA_FALLBACK_REDIRECT]) + expect(output).not.toContain('build.spa') }) From d6230534f425d3ee27117ebebf530d622bd3443c Mon Sep 17 00:00:00 2001 From: Hrishikesh Kokate Date: Fri, 24 Jul 2026 22:41:43 +0530 Subject: [PATCH 08/11] coderabbit 1 --- eslint_temporary_suppressions.js | 5 ++++- packages/build/tsconfig.build.json | 2 +- packages/build/tsconfig.json | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/eslint_temporary_suppressions.js b/eslint_temporary_suppressions.js index a0f2b5451d..6ec7fe1a54 100644 --- a/eslint_temporary_suppressions.js +++ b/eslint_temporary_suppressions.js @@ -2849,7 +2849,10 @@ export default [ // Same class of gap as packages/build-info's own *.test.ts suppressions (e.g. // tests/bin.test.ts): `@netlify/testing` isn't declared as a dependency anywhere // (to avoid a circular dependency), so its types don't fully resolve here. - files: ['packages/build/tests/frameworks_api/spa.test.ts', 'packages/build/tests/spa_fallback/spa_fallback.test.ts'], + files: [ + 'packages/build/tests/frameworks_api/spa.test.ts', + 'packages/build/tests/spa_fallback/spa_fallback.test.ts', + ], rules: { 'n/no-missing-import': 'off', '@typescript-eslint/no-unsafe-assignment': 'off', diff --git a/packages/build/tsconfig.build.json b/packages/build/tsconfig.build.json index 28845ac036..d0db50270f 100644 --- a/packages/build/tsconfig.build.json +++ b/packages/build/tsconfig.build.json @@ -7,5 +7,5 @@ "strictBindCallApply": false /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ }, "include": ["src/**/*.js", "src/**/*.ts"], - "exclude": ["tests/**"] + "exclude": ["tests/**", "src/**/*.test.ts"] } diff --git a/packages/build/tsconfig.json b/packages/build/tsconfig.json index cdf1e2286e..714f7322ac 100644 --- a/packages/build/tsconfig.json +++ b/packages/build/tsconfig.json @@ -4,5 +4,5 @@ "outDir": "lib" /* Specify an output folder for all emitted files. */, "strictBindCallApply": false /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ }, - "references": [{ "path": "./tsconfig.build.json" }, { "path": "./tsconfig.test.json" }] + "references": [{ "path": "./tsconfig.build.json" }] } From b7ae7e445b505ffdae412b591740ef790d35374c Mon Sep 17 00:00:00 2001 From: Hrishikesh Kokate Date: Fri, 24 Jul 2026 22:44:57 +0530 Subject: [PATCH 09/11] fix fmt --- packages/build/src/plugins/child/diff.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/build/src/plugins/child/diff.ts b/packages/build/src/plugins/child/diff.ts index 3fb20d1256..293cc8464c 100644 --- a/packages/build/src/plugins/child/diff.ts +++ b/packages/build/src/plugins/child/diff.ts @@ -26,11 +26,7 @@ export function cloneNetlifyConfig(netlifyConfig: T): T { // run `@netlify/config` to normalize and validate the new values // `configMutations` is passed to parent process as JSON -export function getConfigMutations( - netlifyConfig: object, - netlifyConfigCopy: object, - event: string -): ConfigMutation[] { +export function getConfigMutations(netlifyConfig: object, netlifyConfigCopy: object, event: string): ConfigMutation[] { const configMutations = diffObjects( netlifyConfig as Record, netlifyConfigCopy as Record, @@ -45,11 +41,7 @@ type DiffResult = { keys: string[]; value: unknown } // We only recurse over plain objects, not arrays. Which means array properties // can only be modified all at once. -function diffObjects( - objA: Record, - objB: Record, - parentKeys: string[], -): DiffResult[] { +function diffObjects(objA: Record, objB: Record, parentKeys: string[]): DiffResult[] { const allKeys = [...new Set([...Object.keys(objA), ...Object.keys(objB)])] return allKeys.flatMap((key) => { From 9bc662ccd13444a0eafa2bcef97b6600bffcd087 Mon Sep 17 00:00:00 2001 From: Hrishikesh Kokate Date: Tue, 4 Aug 2026 19:48:55 +0530 Subject: [PATCH 10/11] rename build.spa to spa_fallback --- .../src/plugins_core/frameworks_api/index.ts | 2 +- .../src/plugins_core/spa_fallback/index.ts | 10 +++++----- packages/build/src/types/config/build.ts | 4 ---- .../build/src/types/config/netlify_config.ts | 4 ++++ .../.gitignore | 0 .../build.mjs | 4 +--- .../netlify.toml | 0 .../build/tests/frameworks_api/spa.test.ts | 6 +++--- .../fixtures/spa_disabled/netlify.toml | 3 ++- .../fixtures/spa_enabled/netlify.toml | 3 ++- .../netlify.toml | 3 ++- .../netlify.toml | 3 ++- .../tests/spa_fallback/spa_fallback.test.ts | 8 ++++---- packages/config/src/case.ts | 3 --- packages/config/src/context.ts | 1 - packages/config/src/mutations/apply.js | 2 +- packages/config/src/validate/validations.js | 4 ++-- .../netlify.toml | 3 ++- .../tests/validate/snapshots/tests.js.md | 12 +++++------- .../tests/validate/snapshots/tests.js.snap | Bin 4697 -> 4705 bytes packages/config/tests/validate/tests.js | 4 ++-- 21 files changed, 38 insertions(+), 41 deletions(-) rename packages/build/tests/frameworks_api/fixtures/{spa_config => spa_fallback_config}/.gitignore (100%) rename packages/build/tests/frameworks_api/fixtures/{spa_config => spa_fallback_config}/build.mjs (85%) rename packages/build/tests/frameworks_api/fixtures/{spa_config => spa_fallback_config}/netlify.toml (100%) rename packages/config/tests/validate/fixtures/{build_spa_boolean => spa_fallback_boolean}/netlify.toml (54%) diff --git a/packages/build/src/plugins_core/frameworks_api/index.ts b/packages/build/src/plugins_core/frameworks_api/index.ts index 8faad1c9a1..3a4785bebc 100644 --- a/packages/build/src/plugins_core/frameworks_api/index.ts +++ b/packages/build/src/plugins_core/frameworks_api/index.ts @@ -17,13 +17,13 @@ import { filterConfig, loadConfigFile } from './util.js' const ALLOWED_PROPERTIES = [ ['build', 'functions'], ['build', 'publish'], - ['build', 'spa'], ['edge_functions'], ['functions', '*'], ['functions', '*', '*'], ['headers'], ['images', 'remote_images'], ['redirects'], + ['spa_fallback'], ] // For array properties, any values set in this API will be merged with the diff --git a/packages/build/src/plugins_core/spa_fallback/index.ts b/packages/build/src/plugins_core/spa_fallback/index.ts index 5a417b0ea7..7dbfe64136 100644 --- a/packages/build/src/plugins_core/spa_fallback/index.ts +++ b/packages/build/src/plugins_core/spa_fallback/index.ts @@ -24,21 +24,21 @@ function matchesSpaFallback(redirect: NonNullable { const { netlifyConfig, logs } = coreStepFunctionArgs - if (!netlifyConfig.build.spa) { + if (!netlifyConfig.spa_fallback) { return Promise.resolve({}) } const existingCatchAll = findCatchAllRedirect(netlifyConfig.redirects) if (existingCatchAll) { - // `build.spa` asked us to add a catch-all redirect, but the site already - // has one. If it doesn't already do what ours would, warn the user - // instead of silently overriding their explicit configuration. + // `spa_fallback` asked us to add a catch-all redirect, but the site + // already has one. If it doesn't already do what ours would, warn the + // user instead of silently overriding their explicit configuration. if (!matchesSpaFallback(existingCatchAll)) { logWarning( logs, ` -Warning: "build.spa" is enabled, but a catch-all redirect ("/*") already exists that does not rewrite to "${SPA_FALLBACK_REDIRECT.to}" with a "${String(SPA_FALLBACK_REDIRECT.status)}" status, so Netlify did not add its own. +Warning: "spa_fallback" is enabled, but a catch-all redirect ("/*") already exists that does not rewrite to "${SPA_FALLBACK_REDIRECT.to}" with a "${String(SPA_FALLBACK_REDIRECT.status)}" status, so Netlify did not add its own. Please make sure your existing catch-all redirect correctly serves your single-page application.`, ) } diff --git a/packages/build/src/types/config/build.ts b/packages/build/src/types/config/build.ts index 0f50e93884..cf5ec798bb 100644 --- a/packages/build/src/types/config/build.ts +++ b/packages/build/src/types/config/build.ts @@ -27,10 +27,6 @@ export interface Build { */ 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) */ diff --git a/packages/build/src/types/config/netlify_config.ts b/packages/build/src/types/config/netlify_config.ts index b9fe641253..c7069ba21e 100644 --- a/packages/build/src/types/config/netlify_config.ts +++ b/packages/build/src/types/config/netlify_config.ts @@ -71,4 +71,8 @@ export interface NetlifyConfig { * object with options for database configuration */ database?: DatabaseConfig + /** + * Whether the site is a single-page application (SPA). Defaults to `false`. + */ + spa_fallback?: boolean } diff --git a/packages/build/tests/frameworks_api/fixtures/spa_config/.gitignore b/packages/build/tests/frameworks_api/fixtures/spa_fallback_config/.gitignore similarity index 100% rename from packages/build/tests/frameworks_api/fixtures/spa_config/.gitignore rename to packages/build/tests/frameworks_api/fixtures/spa_fallback_config/.gitignore diff --git a/packages/build/tests/frameworks_api/fixtures/spa_config/build.mjs b/packages/build/tests/frameworks_api/fixtures/spa_fallback_config/build.mjs similarity index 85% rename from packages/build/tests/frameworks_api/fixtures/spa_config/build.mjs rename to packages/build/tests/frameworks_api/fixtures/spa_fallback_config/build.mjs index a5bd3486c5..f12a94f901 100644 --- a/packages/build/tests/frameworks_api/fixtures/spa_config/build.mjs +++ b/packages/build/tests/frameworks_api/fixtures/spa_fallback_config/build.mjs @@ -1,9 +1,7 @@ import { mkdir, writeFile } from 'node:fs/promises' const config = { - build: { - spa: true, - }, + spa_fallback: true, } await mkdir('.netlify/v1', { recursive: true }) diff --git a/packages/build/tests/frameworks_api/fixtures/spa_config/netlify.toml b/packages/build/tests/frameworks_api/fixtures/spa_fallback_config/netlify.toml similarity index 100% rename from packages/build/tests/frameworks_api/fixtures/spa_config/netlify.toml rename to packages/build/tests/frameworks_api/fixtures/spa_fallback_config/netlify.toml diff --git a/packages/build/tests/frameworks_api/spa.test.ts b/packages/build/tests/frameworks_api/spa.test.ts index a626ab7f02..ac05253b6a 100644 --- a/packages/build/tests/frameworks_api/spa.test.ts +++ b/packages/build/tests/frameworks_api/spa.test.ts @@ -1,12 +1,12 @@ import { Fixture } from '@netlify/testing' import { expect, test } from 'vitest' -test('Honors `build.spa` declared through the Frameworks API config file', async () => { +test('Honors `spa_fallback` declared through the Frameworks API config file', async () => { const { netlifyConfig, success } = await new Fixture( import.meta.url, - './fixtures/spa_config', + './fixtures/spa_fallback_config', ).runWithBuildAndIntrospect() expect(success).toBe(true) - expect(netlifyConfig.build.spa).toBe(true) + expect(netlifyConfig.spa_fallback).toBe(true) }) diff --git a/packages/build/tests/spa_fallback/fixtures/spa_disabled/netlify.toml b/packages/build/tests/spa_fallback/fixtures/spa_disabled/netlify.toml index 2a45d5b2c7..2d7b163023 100644 --- a/packages/build/tests/spa_fallback/fixtures/spa_disabled/netlify.toml +++ b/packages/build/tests/spa_fallback/fixtures/spa_disabled/netlify.toml @@ -1,3 +1,4 @@ +spa_fallback = false + [build] command = "echo hi" -spa = false diff --git a/packages/build/tests/spa_fallback/fixtures/spa_enabled/netlify.toml b/packages/build/tests/spa_fallback/fixtures/spa_enabled/netlify.toml index 171ab2c9d7..608c7196db 100644 --- a/packages/build/tests/spa_fallback/fixtures/spa_enabled/netlify.toml +++ b/packages/build/tests/spa_fallback/fixtures/spa_enabled/netlify.toml @@ -1,3 +1,4 @@ +spa_fallback = true + [build] command = "echo hi" -spa = true diff --git a/packages/build/tests/spa_fallback/fixtures/spa_enabled_existing_catch_all/netlify.toml b/packages/build/tests/spa_fallback/fixtures/spa_enabled_existing_catch_all/netlify.toml index 6c3f4812da..2d246d25a7 100644 --- a/packages/build/tests/spa_fallback/fixtures/spa_enabled_existing_catch_all/netlify.toml +++ b/packages/build/tests/spa_fallback/fixtures/spa_enabled_existing_catch_all/netlify.toml @@ -1,6 +1,7 @@ +spa_fallback = true + [build] command = "echo hi" -spa = true [[redirects]] from = "/*" diff --git a/packages/build/tests/spa_fallback/fixtures/spa_enabled_existing_correct_catch_all/netlify.toml b/packages/build/tests/spa_fallback/fixtures/spa_enabled_existing_correct_catch_all/netlify.toml index 60b0e304f1..079a79d1a1 100644 --- a/packages/build/tests/spa_fallback/fixtures/spa_enabled_existing_correct_catch_all/netlify.toml +++ b/packages/build/tests/spa_fallback/fixtures/spa_enabled_existing_correct_catch_all/netlify.toml @@ -1,6 +1,7 @@ +spa_fallback = true + [build] command = "echo hi" -spa = true [[redirects]] from = "/*" diff --git a/packages/build/tests/spa_fallback/spa_fallback.test.ts b/packages/build/tests/spa_fallback/spa_fallback.test.ts index 472dc4f021..d6c9b25d15 100644 --- a/packages/build/tests/spa_fallback/spa_fallback.test.ts +++ b/packages/build/tests/spa_fallback/spa_fallback.test.ts @@ -11,7 +11,7 @@ const SPA_FALLBACK_REDIRECT = { to: '/index.html', } -test('Injects an SPA fallback redirect when `build.spa` is `true`', async () => { +test('Injects an SPA fallback redirect when `spa_fallback` is `true`', async () => { const { netlifyConfig, success } = await new Fixture( import.meta.url, './fixtures/spa_enabled', @@ -21,7 +21,7 @@ test('Injects an SPA fallback redirect when `build.spa` is `true`', async () => expect(netlifyConfig.redirects).toEqual([SPA_FALLBACK_REDIRECT]) }) -test('Does not inject an SPA fallback redirect when `build.spa` is `false`', async () => { +test('Does not inject an SPA fallback redirect when `spa_fallback` is `false`', async () => { const { netlifyConfig, success } = await new Fixture( import.meta.url, './fixtures/spa_disabled', @@ -31,7 +31,7 @@ test('Does not inject an SPA fallback redirect when `build.spa` is `false`', asy expect(netlifyConfig.redirects).toEqual([]) }) -test('Does not inject an SPA fallback redirect when `build.spa` is not set', async () => { +test('Does not inject an SPA fallback redirect when `spa_fallback` is not set', async () => { const { netlifyConfig, success } = await new Fixture( import.meta.url, './fixtures/spa_default', @@ -60,5 +60,5 @@ test('Does not warn when the existing catch-all redirect already matches what Ne expect(success).toBe(true) expect(netlifyConfig.redirects).toEqual([SPA_FALLBACK_REDIRECT]) - expect(output).not.toContain('build.spa') + expect(output).not.toContain('already exists') }) diff --git a/packages/config/src/case.ts b/packages/config/src/case.ts index 1012bf1e58..0bbdad2180 100644 --- a/packages/config/src/case.ts +++ b/packages/config/src/case.ts @@ -29,8 +29,6 @@ const normalizeBuildCase = ({ processing = Processing, Publish, publish = Publish, - Spa, - spa = Spa, ...build }: Record = {}): Record => { return { @@ -43,6 +41,5 @@ const normalizeBuildCase = ({ ignore, processing, publish, - spa, } } diff --git a/packages/config/src/context.ts b/packages/config/src/context.ts index 61e0e279a1..477ff2948b 100644 --- a/packages/config/src/context.ts +++ b/packages/config/src/context.ts @@ -91,7 +91,6 @@ const BUILD_PROPERTIES = new Set([ 'ignore', 'processing', 'publish', - 'spa', ]) // `config.functions` is a plain object while `config.build.functions` is a diff --git a/packages/config/src/mutations/apply.js b/packages/config/src/mutations/apply.js index 8e99cee6d6..9bdb1fb5bd 100644 --- a/packages/config/src/mutations/apply.js +++ b/packages/config/src/mutations/apply.js @@ -69,7 +69,6 @@ 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 }, @@ -78,6 +77,7 @@ const MUTABLE_PROPS = { images: { lastEvent: 'onPostBuild' }, 'images.remote_images': { lastEvent: 'onPostBuild' }, redirects: { lastEvent: 'onPostBuild' }, + spa_fallback: { lastEvent: 'onPostBuild' }, dev: { lastEvent: 'onPreDev' }, 'dev.processing': { lastEvent: 'onPreDev' }, 'dev.processing.html': { lastEvent: 'onPreDev' }, diff --git a/packages/config/src/validate/validations.js b/packages/config/src/validate/validations.js index daafc7c849..c0dafa41df 100644 --- a/packages/config/src/validate/validations.js +++ b/packages/config/src/validate/validations.js @@ -233,10 +233,10 @@ export const POST_NORMALIZE_VALIDATIONS = [ example: () => ({ build: { edge_functions: 'edge-functions' } }), }, { - property: 'build.spa', + property: 'spa_fallback', check: isBoolean, message: 'must be a boolean.', - example: () => ({ build: { spa: true } }), + example: () => ({ spa_fallback: true }), }, { property: 'functions.*', diff --git a/packages/config/tests/validate/fixtures/build_spa_boolean/netlify.toml b/packages/config/tests/validate/fixtures/spa_fallback_boolean/netlify.toml similarity index 54% rename from packages/config/tests/validate/fixtures/build_spa_boolean/netlify.toml rename to packages/config/tests/validate/fixtures/spa_fallback_boolean/netlify.toml index b8feaddc05..68d04bb6d4 100644 --- a/packages/config/tests/validate/fixtures/build_spa_boolean/netlify.toml +++ b/packages/config/tests/validate/fixtures/spa_fallback_boolean/netlify.toml @@ -1,3 +1,4 @@ +spa_fallback = "true" + [build] -spa = "true" publish = "publish" diff --git a/packages/config/tests/validate/snapshots/tests.js.md b/packages/config/tests/validate/snapshots/tests.js.md index d133b61c52..a1a57c6cdc 100644 --- a/packages/config/tests/validate/snapshots/tests.js.md +++ b/packages/config/tests/validate/snapshots/tests.js.md @@ -497,22 +497,20 @@ Generated by [AVA](https://avajs.dev). `When resolving config file packages/config/tests/validate/fixtures/build_edge_functions_parent/netlify.toml:␊ Configuration property "build.edge_functions" "edge_functions_dir" must be inside the repository root directory.` -## build.spa: boolean +## spa_fallback: boolean > Snapshot 1 - `When resolving config file packages/config/tests/validate/fixtures/build_spa_boolean/netlify.toml:␊ - Configuration property build.spa must be a boolean.␊ + `When resolving config file packages/config/tests/validate/fixtures/spa_fallback_boolean/netlify.toml:␊ + Configuration property spa_fallback must be a boolean.␊ ␊ Invalid syntax␊ ␊ - [build]␊ - spa = "true"␊ + spa_fallback = "true"␊ ␊ Valid syntax␊ ␊ - [build]␊ - spa = true` + spa_fallback = true` ## build.base: string diff --git a/packages/config/tests/validate/snapshots/tests.js.snap b/packages/config/tests/validate/snapshots/tests.js.snap index 862b43aa245e98774adde8da093e309680f847fd..02af59e0bddb67f16723a4f28d85b55df149d9f6 100644 GIT binary patch delta 3518 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 delta 3510 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 { t.snapshot(normalizeOutput(output)) }) -test('build.spa: boolean', async (t) => { - const output = await new Fixture(test.meta.file, './fixtures/build_spa_boolean').runWithConfig() +test('spa_fallback: boolean', async (t) => { + const output = await new Fixture(test.meta.file, './fixtures/spa_fallback_boolean').runWithConfig() t.snapshot(normalizeOutput(output)) }) From 9d954926ec3b3df029a5586c52d8ef60ce14cd2e Mon Sep 17 00:00:00 2001 From: Hrishikesh Kokate Date: Tue, 4 Aug 2026 19:54:17 +0530 Subject: [PATCH 11/11] coderabbit test added --- .../fixtures/spa_fallback_config_precedence/.gitignore | 2 ++ .../fixtures/spa_fallback_config_precedence/build.mjs | 9 +++++++++ .../spa_fallback_config_precedence/netlify.toml | 4 ++++ packages/build/tests/frameworks_api/spa.test.ts | 10 ++++++++++ 4 files changed, 25 insertions(+) create mode 100644 packages/build/tests/frameworks_api/fixtures/spa_fallback_config_precedence/.gitignore create mode 100644 packages/build/tests/frameworks_api/fixtures/spa_fallback_config_precedence/build.mjs create mode 100644 packages/build/tests/frameworks_api/fixtures/spa_fallback_config_precedence/netlify.toml diff --git a/packages/build/tests/frameworks_api/fixtures/spa_fallback_config_precedence/.gitignore b/packages/build/tests/frameworks_api/fixtures/spa_fallback_config_precedence/.gitignore new file mode 100644 index 0000000000..ec6cc0eb0d --- /dev/null +++ b/packages/build/tests/frameworks_api/fixtures/spa_fallback_config_precedence/.gitignore @@ -0,0 +1,2 @@ +# .netlify/v1/config.json is generated at build time by build.mjs. +.netlify diff --git a/packages/build/tests/frameworks_api/fixtures/spa_fallback_config_precedence/build.mjs b/packages/build/tests/frameworks_api/fixtures/spa_fallback_config_precedence/build.mjs new file mode 100644 index 0000000000..f12a94f901 --- /dev/null +++ b/packages/build/tests/frameworks_api/fixtures/spa_fallback_config_precedence/build.mjs @@ -0,0 +1,9 @@ +import { mkdir, writeFile } from 'node:fs/promises' + +const config = { + spa_fallback: true, +} + +await mkdir('.netlify/v1', { recursive: true }) + +await writeFile('.netlify/v1/config.json', JSON.stringify(config)) diff --git a/packages/build/tests/frameworks_api/fixtures/spa_fallback_config_precedence/netlify.toml b/packages/build/tests/frameworks_api/fixtures/spa_fallback_config_precedence/netlify.toml new file mode 100644 index 0000000000..b7f8d1bdaf --- /dev/null +++ b/packages/build/tests/frameworks_api/fixtures/spa_fallback_config_precedence/netlify.toml @@ -0,0 +1,4 @@ +spa_fallback = false + +[build] +command = "node build.mjs" diff --git a/packages/build/tests/frameworks_api/spa.test.ts b/packages/build/tests/frameworks_api/spa.test.ts index ac05253b6a..380fe88789 100644 --- a/packages/build/tests/frameworks_api/spa.test.ts +++ b/packages/build/tests/frameworks_api/spa.test.ts @@ -10,3 +10,13 @@ test('Honors `spa_fallback` declared through the Frameworks API config file', as expect(success).toBe(true) expect(netlifyConfig.spa_fallback).toBe(true) }) + +test('`netlify.toml` takes precedence over the Frameworks API for `spa_fallback`', async () => { + const { netlifyConfig, success } = await new Fixture( + import.meta.url, + './fixtures/spa_fallback_config_precedence', + ).runWithBuildAndIntrospect() + + expect(success).toBe(true) + expect(netlifyConfig.spa_fallback).toBe(false) +})