Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit d4b6898

Browse files
authored
[web] Put all index.html operations in one place (#118188)
* [web] Put all index.html operations in one place * review comments * fix build * change quotes * fix test
1 parent b0f1714 commit d4b6898

File tree

8 files changed

+292
-113
lines changed

8 files changed

+292
-113
lines changed

packages/flutter_tools/lib/src/build_system/targets/web.dart

+7-22
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import '../../dart/language_version.dart';
1717
import '../../dart/package_map.dart';
1818
import '../../flutter_plugins.dart';
1919
import '../../globals.dart' as globals;
20+
import '../../html_utils.dart';
2021
import '../../project.dart';
2122
import '../../web/compile.dart';
2223
import '../../web/file_generators/flutter_js.dart' as flutter_js;
@@ -50,9 +51,6 @@ const String kCspMode = 'cspMode';
5051
/// Base href to set in index.html in flutter build command
5152
const String kBaseHref = 'baseHref';
5253

53-
/// Placeholder for base href
54-
const String kBaseHrefPlaceholder = r'$FLUTTER_BASE_HREF';
55-
5654
/// The caching strategy to use for service worker generation.
5755
const String kServiceWorkerStrategy = 'ServiceWorkerStrategy';
5856

@@ -442,25 +440,12 @@ class WebReleaseBundle extends Target {
442440
// because it would need to be the hash for the entire bundle and not just the resource
443441
// in question.
444442
if (environment.fileSystem.path.basename(inputFile.path) == 'index.html') {
445-
final String randomHash = Random().nextInt(4294967296).toString();
446-
String resultString = inputFile.readAsStringSync()
447-
.replaceFirst(
448-
'var serviceWorkerVersion = null',
449-
"var serviceWorkerVersion = '$randomHash'",
450-
)
451-
// This is for legacy index.html that still use the old service
452-
// worker loading mechanism.
453-
.replaceFirst(
454-
"navigator.serviceWorker.register('flutter_service_worker.js')",
455-
"navigator.serviceWorker.register('flutter_service_worker.js?v=$randomHash')",
456-
);
457-
final String? baseHref = environment.defines[kBaseHref];
458-
if (resultString.contains(kBaseHrefPlaceholder) && baseHref == null) {
459-
resultString = resultString.replaceAll(kBaseHrefPlaceholder, '/');
460-
} else if (resultString.contains(kBaseHrefPlaceholder) && baseHref != null) {
461-
resultString = resultString.replaceAll(kBaseHrefPlaceholder, baseHref);
462-
}
463-
outputFile.writeAsStringSync(resultString);
443+
final IndexHtml indexHtml = IndexHtml(inputFile.readAsStringSync());
444+
indexHtml.applySubstitutions(
445+
baseHref: environment.defines[kBaseHref] ?? '/',
446+
serviceWorkerVersion: Random().nextInt(4294967296).toString(),
447+
);
448+
outputFile.writeAsStringSync(indexHtml.content);
464449
continue;
465450
}
466451
inputFile.copySync(outputFile.path);

packages/flutter_tools/lib/src/commands/build_web.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import '../base/file_system.dart';
77
import '../build_info.dart';
88
import '../build_system/targets/web.dart';
99
import '../features.dart';
10+
import '../html_utils.dart';
1011
import '../project.dart';
1112
import '../runner/flutter_command.dart'
1213
show DevelopmentArtifact, FlutterCommandResult;
@@ -127,7 +128,7 @@ class BuildWebCommand extends BuildSubCommand {
127128
baseHref != null) {
128129
throwToolExit(
129130
"Couldn't find the placeholder for base href. "
130-
r'Please add `<base href="$FLUTTER_BASE_HREF">` to web/index.html'
131+
'Please add `<base href="$kBaseHrefPlaceholder">` to web/index.html'
131132
);
132133
}
133134

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:html/dom.dart';
6+
import 'package:html/parser.dart';
7+
8+
import 'base/common.dart';
9+
10+
/// Placeholder for base href
11+
const String kBaseHrefPlaceholder = r'$FLUTTER_BASE_HREF';
12+
13+
/// Utility class for parsing and performing operations on the contents of the
14+
/// index.html file.
15+
///
16+
/// For example, to parse the base href from the index.html file:
17+
///
18+
/// ```dart
19+
/// String parseBaseHref(File indexHtmlFile) {
20+
/// final IndexHtml indexHtml = IndexHtml(indexHtmlFile.readAsStringSync());
21+
/// return indexHtml.getBaseHref();
22+
/// }
23+
/// ```
24+
class IndexHtml {
25+
IndexHtml(this._content);
26+
27+
String get content => _content;
28+
String _content;
29+
30+
Document _getDocument() => parse(_content);
31+
32+
/// Parses the base href from the index.html file.
33+
String getBaseHref() {
34+
final Element? baseElement = _getDocument().querySelector('base');
35+
final String? baseHref = baseElement?.attributes == null
36+
? null
37+
: baseElement!.attributes['href'];
38+
39+
if (baseHref == null || baseHref == kBaseHrefPlaceholder) {
40+
return '';
41+
}
42+
43+
if (!baseHref.startsWith('/')) {
44+
throw ToolExit(
45+
'Error: The base href in "web/index.html" must be absolute (i.e. start '
46+
'with a "/"), but found: `${baseElement!.outerHtml}`.\n'
47+
'$_kBasePathExample',
48+
);
49+
}
50+
51+
if (!baseHref.endsWith('/')) {
52+
throw ToolExit(
53+
'Error: The base href in "web/index.html" must end with a "/", but found: `${baseElement!.outerHtml}`.\n'
54+
'$_kBasePathExample',
55+
);
56+
}
57+
58+
return stripLeadingSlash(stripTrailingSlash(baseHref));
59+
}
60+
61+
/// Applies substitutions to the content of the index.html file.
62+
void applySubstitutions({
63+
required String baseHref,
64+
required String? serviceWorkerVersion,
65+
}) {
66+
if (_content.contains(kBaseHrefPlaceholder)) {
67+
_content = _content.replaceAll(kBaseHrefPlaceholder, baseHref);
68+
}
69+
70+
if (serviceWorkerVersion != null) {
71+
_content = _content
72+
.replaceFirst(
73+
'var serviceWorkerVersion = null',
74+
'var serviceWorkerVersion = "$serviceWorkerVersion"',
75+
)
76+
// This is for legacy index.html that still uses the old service
77+
// worker loading mechanism.
78+
.replaceFirst(
79+
"navigator.serviceWorker.register('flutter_service_worker.js')",
80+
"navigator.serviceWorker.register('flutter_service_worker.js?v=$serviceWorkerVersion')",
81+
);
82+
}
83+
}
84+
}
85+
86+
/// Strips the leading slash from a path.
87+
String stripLeadingSlash(String path) {
88+
while (path.startsWith('/')) {
89+
path = path.substring(1);
90+
}
91+
return path;
92+
}
93+
94+
/// Strips the trailing slash from a path.
95+
String stripTrailingSlash(String path) {
96+
while (path.endsWith('/')) {
97+
path = path.substring(0, path.length - 1);
98+
}
99+
return path;
100+
}
101+
102+
const String _kBasePathExample = '''
103+
For example, to serve from the root use:
104+
105+
<base href="/">
106+
107+
To serve from a subpath "foo" (i.e. http://localhost:8080/foo/ instead of http://localhost:8080/) use:
108+
109+
<base href="/foo/">
110+
111+
For more information, see: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
112+
''';

packages/flutter_tools/lib/src/isolated/devfs_web.dart

+28-87
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import 'dart:typed_data';
88
import 'package:dwds/data/build_result.dart';
99
// ignore: import_of_legacy_library_into_null_safe
1010
import 'package:dwds/dwds.dart';
11-
import 'package:html/dom.dart';
12-
import 'package:html/parser.dart';
1311
import 'package:logging/logging.dart' as logging;
1412
import 'package:meta/meta.dart';
1513
import 'package:mime/mime.dart' as mime;
@@ -29,14 +27,14 @@ import '../base/platform.dart';
2927
import '../build_info.dart';
3028
import '../build_system/targets/scene_importer.dart';
3129
import '../build_system/targets/shader_compiler.dart';
32-
import '../build_system/targets/web.dart';
3330
import '../bundle_builder.dart';
3431
import '../cache.dart';
3532
import '../compile.dart';
3633
import '../convert.dart';
3734
import '../dart/package_map.dart';
3835
import '../devfs.dart';
3936
import '../globals.dart' as globals;
37+
import '../html_utils.dart';
4038
import '../project.dart';
4139
import '../vmservice.dart';
4240
import '../web/bootstrap.dart';
@@ -136,9 +134,7 @@ class WebAssetServer implements AssetReader {
136134
this._modules,
137135
this._digests,
138136
this._nullSafetyMode,
139-
) : basePath = _parseBasePathFromIndexHtml(globals.fs.currentDirectory
140-
.childDirectory('web')
141-
.childFile('index.html'));
137+
) : basePath = _getIndexHtml().getBaseHref();
142138

143139
// Fallback to "application/octet-stream" on null which
144140
// makes no claims as to the structure of the data.
@@ -299,7 +295,7 @@ class WebAssetServer implements AssetReader {
299295
server,
300296
PackageUriMapper(packageConfig),
301297
digestProvider,
302-
server.basePath!,
298+
server.basePath,
303299
).strategy,
304300
expressionCompiler: expressionCompiler,
305301
spawnDds: enableDds,
@@ -345,12 +341,11 @@ class WebAssetServer implements AssetReader {
345341
@visibleForTesting
346342
Uint8List? getMetadata(String path) => _webMemoryFS.metadataFiles[path];
347343

348-
@visibleForTesting
349-
350344
/// The base path to serve from.
351345
///
352346
/// It should have no leading or trailing slashes.
353-
String? basePath = '';
347+
@visibleForTesting
348+
String basePath;
354349

355350
// handle requests for JavaScript source, dart sources maps, or asset files.
356351
@visibleForTesting
@@ -496,27 +491,20 @@ class WebAssetServer implements AssetReader {
496491
WebRendererMode webRenderer = WebRendererMode.html;
497492

498493
shelf.Response _serveIndex() {
494+
495+
final IndexHtml indexHtml = _getIndexHtml();
496+
497+
indexHtml.applySubstitutions(
498+
// Currently, we don't support --base-href for the "run" command.
499+
baseHref: '/',
500+
serviceWorkerVersion: null,
501+
);
502+
499503
final Map<String, String> headers = <String, String>{
500504
HttpHeaders.contentTypeHeader: 'text/html',
505+
HttpHeaders.contentLengthHeader: indexHtml.content.length.toString(),
501506
};
502-
final File indexFile = globals.fs.currentDirectory
503-
.childDirectory('web')
504-
.childFile('index.html');
505-
506-
if (indexFile.existsSync()) {
507-
String indexFileContent = indexFile.readAsStringSync();
508-
if (indexFileContent.contains(kBaseHrefPlaceholder)) {
509-
indexFileContent = indexFileContent.replaceAll(kBaseHrefPlaceholder, '/');
510-
headers[HttpHeaders.contentLengthHeader] = indexFileContent.length.toString();
511-
return shelf.Response.ok(indexFileContent,headers: headers);
512-
}
513-
headers[HttpHeaders.contentLengthHeader] =
514-
indexFile.lengthSync().toString();
515-
return shelf.Response.ok(indexFile.openRead(), headers: headers);
516-
}
517-
518-
headers[HttpHeaders.contentLengthHeader] = _kDefaultIndex.length.toString();
519-
return shelf.Response.ok(_kDefaultIndex, headers: headers);
507+
return shelf.Response.ok(indexHtml.content, headers: headers);
520508
}
521509

522510
// Attempt to resolve `path` to a dart file.
@@ -783,9 +771,9 @@ class WebDevFS implements DevFS {
783771
webAssetServer.webRenderer = WebRendererMode.canvaskit;
784772
}
785773
if (hostname == 'any') {
786-
_baseUri = Uri.http('localhost:$selectedPort', webAssetServer.basePath!);
774+
_baseUri = Uri.http('localhost:$selectedPort', webAssetServer.basePath);
787775
} else {
788-
_baseUri = Uri.http('$hostname:$selectedPort', webAssetServer.basePath!);
776+
_baseUri = Uri.http('$hostname:$selectedPort', webAssetServer.basePath);
789777
}
790778
return _baseUri!;
791779
}
@@ -977,12 +965,11 @@ class ReleaseAssetServer {
977965
final FileSystemUtils _fileSystemUtils;
978966
final Platform _platform;
979967

980-
@visibleForTesting
981-
982968
/// The base path to serve from.
983969
///
984970
/// It should have no leading or trailing slashes.
985-
final String? basePath;
971+
@visibleForTesting
972+
final String basePath;
986973

987974
// Locations where source files, assets, or source maps may be located.
988975
List<Uri> _searchPaths() => <Uri>[
@@ -1070,67 +1057,21 @@ Future<Directory> _loadDwdsDirectory(
10701057
return fileSystem.directory(packageConfig['dwds']!.packageUriRoot);
10711058
}
10721059

1073-
String? _stripBasePath(String path, String? basePath) {
1074-
path = _stripLeadingSlashes(path);
1075-
if (basePath != null && path.startsWith(basePath)) {
1060+
String? _stripBasePath(String path, String basePath) {
1061+
path = stripLeadingSlash(path);
1062+
if (path.startsWith(basePath)) {
10761063
path = path.substring(basePath.length);
10771064
} else {
10781065
// The given path isn't under base path, return null to indicate that.
10791066
return null;
10801067
}
1081-
return _stripLeadingSlashes(path);
1082-
}
1083-
1084-
String _stripLeadingSlashes(String path) {
1085-
while (path.startsWith('/')) {
1086-
path = path.substring(1);
1087-
}
1088-
return path;
1089-
}
1090-
1091-
String _stripTrailingSlashes(String path) {
1092-
while (path.endsWith('/')) {
1093-
path = path.substring(0, path.length - 1);
1094-
}
1095-
return path;
1068+
return stripLeadingSlash(path);
10961069
}
10971070

1098-
String? _parseBasePathFromIndexHtml(File indexHtml) {
1071+
IndexHtml _getIndexHtml() {
1072+
final File indexHtml =
1073+
globals.fs.currentDirectory.childDirectory('web').childFile('index.html');
10991074
final String htmlContent =
11001075
indexHtml.existsSync() ? indexHtml.readAsStringSync() : _kDefaultIndex;
1101-
final Document document = parse(htmlContent);
1102-
final Element? baseElement = document.querySelector('base');
1103-
String? baseHref =
1104-
baseElement?.attributes == null ? null : baseElement!.attributes['href'];
1105-
1106-
if (baseHref == null || baseHref == kBaseHrefPlaceholder) {
1107-
baseHref = '';
1108-
} else if (!baseHref.startsWith('/')) {
1109-
throw ToolExit(
1110-
'Error: The base href in "web/index.html" must be absolute (i.e. start '
1111-
'with a "/"), but found: `${baseElement!.outerHtml}`.\n'
1112-
'$basePathExample',
1113-
);
1114-
} else if (!baseHref.endsWith('/')) {
1115-
throw ToolExit(
1116-
'Error: The base href in "web/index.html" must end with a "/", but found: `${baseElement!.outerHtml}`.\n'
1117-
'$basePathExample',
1118-
);
1119-
} else {
1120-
baseHref = _stripLeadingSlashes(_stripTrailingSlashes(baseHref));
1121-
}
1122-
1123-
return baseHref;
1076+
return IndexHtml(htmlContent);
11241077
}
1125-
1126-
const String basePathExample = '''
1127-
For example, to serve from the root use:
1128-
1129-
<base href="/">
1130-
1131-
To serve from a subpath "foo" (i.e. http://localhost:8080/foo/ instead of http://localhost:8080/) use:
1132-
1133-
<base href="/foo/">
1134-
1135-
For more information, see: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
1136-
''';

packages/flutter_tools/lib/src/isolated/resident_web_runner.dart

-1
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,6 @@ class ResidentWebRunner extends ResidentRunner {
372372
true,
373373
debuggingOptions.nativeNullAssertions,
374374
false,
375-
baseHref: kBaseHref,
376375
);
377376
} on ToolExit {
378377
return OperationResult(1, 'Failed to recompile application.');

packages/flutter_tools/test/general.shard/build_system/targets/web_test.dart

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:flutter_tools/src/build_system/build_system.dart';
1212
import 'package:flutter_tools/src/build_system/depfile.dart';
1313
import 'package:flutter_tools/src/build_system/targets/web.dart';
1414
import 'package:flutter_tools/src/globals.dart' as globals;
15+
import 'package:flutter_tools/src/html_utils.dart';
1516
import 'package:flutter_tools/src/isolated/mustache_template.dart';
1617
import 'package:flutter_tools/src/web/compile.dart';
1718
import 'package:flutter_tools/src/web/file_generators/flutter_js.dart' as flutter_js;

0 commit comments

Comments
 (0)