Skip to content

Commit 9fdd02e

Browse files
committed
feat(serve): Add --spa-fallback flag for SPA routing
When developing Single Page Applications (SPAs) that use client-side routing with the HTML5 History API (e.g., Angular's PathLocationStrategy), refreshing the page on a deep link results in a 404 error because the server can't find a corresponding file for the route. This change introduces a --spa-fallback flag to the webdev serve command. When enabled, the development server will serve the root index.html file for any GET request that would otherwise result in a 404, as long as the path does not appear to be a direct file asset (i.e., does not contain a file extension). This allows the client-side router to take over and handle the request, enabling a seamless development workflow for modern web applications.
1 parent bd0bcb8 commit 9fdd02e

File tree

3 files changed

+37
-2
lines changed

3 files changed

+37
-2
lines changed

webdev/lib/src/command/configuration.dart

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const disableDdsFlag = 'disable-dds';
3838
const enableExperimentOption = 'enable-experiment';
3939
const canaryFeaturesFlag = 'canary';
4040
const offlineFlag = 'offline';
41+
const spaFallbackFlag = 'spa-fallback';
4142

4243
ReloadConfiguration _parseReloadConfiguration(ArgResults argResults) {
4344
var auto = argResults.options.contains(autoOption)
@@ -109,6 +110,7 @@ class Configuration {
109110
final List<String>? _experiments;
110111
final bool? _canaryFeatures;
111112
final bool? _offline;
113+
final bool? _spaFallback;
112114

113115
Configuration({
114116
bool? autoRun,
@@ -136,6 +138,7 @@ class Configuration {
136138
List<String>? experiments,
137139
bool? canaryFeatures,
138140
bool? offline,
141+
bool? spaFallback,
139142
}) : _autoRun = autoRun,
140143
_chromeDebugPort = chromeDebugPort,
141144
_debugExtension = debugExtension,
@@ -158,7 +161,8 @@ class Configuration {
158161
_nullSafety = nullSafety,
159162
_experiments = experiments,
160163
_canaryFeatures = canaryFeatures,
161-
_offline = offline {
164+
_offline = offline,
165+
_spaFallback = spaFallback {
162166
_validateConfiguration();
163167
}
164168

@@ -234,7 +238,8 @@ class Configuration {
234238
nullSafety: other._nullSafety ?? _nullSafety,
235239
experiments: other._experiments ?? _experiments,
236240
canaryFeatures: other._canaryFeatures ?? _canaryFeatures,
237-
offline: other._offline ?? _offline);
241+
offline: other._offline ?? _offline,
242+
spaFallback: other._spaFallback ?? _spaFallback);
238243

239244
factory Configuration.noInjectedClientDefaults() =>
240245
Configuration(autoRun: false, debug: false, debugExtension: false);
@@ -291,6 +296,8 @@ class Configuration {
291296

292297
bool get offline => _offline ?? false;
293298

299+
bool get spaFallback => _spaFallback ?? false;
300+
294301
/// Returns a new configuration with values updated from the parsed args.
295302
static Configuration fromArgs(ArgResults? argResults,
296303
{Configuration? defaultConfiguration}) {
@@ -419,6 +426,10 @@ class Configuration {
419426
? argResults[offlineFlag] as bool?
420427
: defaultConfiguration.verbose;
421428

429+
final spaFallback = argResults.options.contains(spaFallbackFlag)
430+
? argResults[spaFallbackFlag] as bool?
431+
: defaultConfiguration.spaFallback;
432+
422433
return Configuration(
423434
autoRun: defaultConfiguration.autoRun,
424435
chromeDebugPort: chromeDebugPort,
@@ -445,6 +456,7 @@ class Configuration {
445456
experiments: experiments,
446457
canaryFeatures: canaryFeatures,
447458
offline: offline,
459+
spaFallback: spaFallback,
448460
);
449461
}
450462
}

webdev/lib/src/command/serve_command.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ refresh: Performs a full page refresh.
7575
..addFlag(logRequestsFlag,
7676
negatable: false,
7777
help: 'Enables logging for each request to the server.')
78+
..addFlag('spa-fallback',
79+
negatable: false,
80+
help: 'Serves index.html for any 404 from a non-asset request. '
81+
'Useful for single-page applications with client-side routing.')
7882
..addOption(tlsCertChainFlag,
7983
help:
8084
'The file location to a TLS Certificate to create an HTTPs server.\n'

webdev/lib/src/serve/webdev_server.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,25 @@ class WebDevServer {
195195
cascade = cascade.add(assetHandler);
196196
}
197197

198+
if (options.configuration.spaFallback) {
199+
FutureOr<Response> spaFallbackHandler(Request request) async {
200+
final uri = request.requestedUri;
201+
final hasExtension =
202+
uri.pathSegments.isNotEmpty && uri.pathSegments.last.contains('.');
203+
if (request.method != 'GET' || hasExtension) {
204+
return Response.notFound('Not Found');
205+
}
206+
final indexResponse =
207+
await assetHandler(request.change(path: 'index.html'));
208+
209+
return indexResponse.statusCode == 200
210+
? indexResponse
211+
: Response.notFound('Not Found');
212+
}
213+
214+
cascade = cascade.add(spaFallbackHandler);
215+
}
216+
198217
final hostname = options.configuration.hostname;
199218
final tlsCertChain = options.configuration.tlsCertChain ?? '';
200219
final tlsCertKey = options.configuration.tlsCertKey ?? '';

0 commit comments

Comments
 (0)