Unverified Commit 4a33813b authored by Balvinder Singh Gambhir's avatar Balvinder Singh Gambhir Committed by GitHub

[flutter_tools] added base-href command in web (#80519)

parent 447b7f1d
...@@ -35,6 +35,12 @@ const String kDart2jsOptimization = 'Dart2jsOptimization'; ...@@ -35,6 +35,12 @@ const String kDart2jsOptimization = 'Dart2jsOptimization';
/// Whether to disable dynamic generation code to satisfy csp policies. /// Whether to disable dynamic generation code to satisfy csp policies.
const String kCspMode = 'cspMode'; const String kCspMode = 'cspMode';
/// Base href to set in index.html in flutter build command
const String kBaseHref = 'baseHref';
/// Placeholder for base href
const String kBaseHrefPlaceholder = r'$FLUTTER_BASE_HREF';
/// The caching strategy to use for service worker generation. /// The caching strategy to use for service worker generation.
const String kServiceWorkerStrategy = 'ServiceWorkerStrategy'; const String kServiceWorkerStrategy = 'ServiceWorkerStrategy';
...@@ -356,7 +362,7 @@ class WebReleaseBundle extends Target { ...@@ -356,7 +362,7 @@ class WebReleaseBundle extends Target {
// in question. // in question.
if (environment.fileSystem.path.basename(inputFile.path) == 'index.html') { if (environment.fileSystem.path.basename(inputFile.path) == 'index.html') {
final String randomHash = Random().nextInt(4294967296).toString(); final String randomHash = Random().nextInt(4294967296).toString();
final String resultString = inputFile.readAsStringSync() String resultString = inputFile.readAsStringSync()
.replaceFirst( .replaceFirst(
'var serviceWorkerVersion = null', 'var serviceWorkerVersion = null',
"var serviceWorkerVersion = '$randomHash'", "var serviceWorkerVersion = '$randomHash'",
...@@ -367,6 +373,13 @@ class WebReleaseBundle extends Target { ...@@ -367,6 +373,13 @@ class WebReleaseBundle extends Target {
"navigator.serviceWorker.register('flutter_service_worker.js')", "navigator.serviceWorker.register('flutter_service_worker.js')",
"navigator.serviceWorker.register('flutter_service_worker.js?v=$randomHash')", "navigator.serviceWorker.register('flutter_service_worker.js?v=$randomHash')",
); );
if (resultString.contains(kBaseHrefPlaceholder) &&
environment.defines[kBaseHref] == null) {
resultString = resultString.replaceAll(kBaseHrefPlaceholder, '/');
} else if (resultString.contains(kBaseHrefPlaceholder) &&
environment.defines[kBaseHref] != null) {
resultString = resultString.replaceAll(kBaseHrefPlaceholder, environment.defines[kBaseHref]);
}
outputFile.writeAsStringSync(resultString); outputFile.writeAsStringSync(resultString);
continue; continue;
} }
......
...@@ -10,6 +10,7 @@ import '../base/common.dart'; ...@@ -10,6 +10,7 @@ import '../base/common.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../build_system/targets/web.dart'; import '../build_system/targets/web.dart';
import '../features.dart'; import '../features.dart';
import '../globals.dart' as globals;
import '../project.dart'; import '../project.dart';
import '../runner/flutter_command.dart' import '../runner/flutter_command.dart'
show DevelopmentArtifact, FlutterCommandResult; show DevelopmentArtifact, FlutterCommandResult;
...@@ -42,6 +43,7 @@ class BuildWebCommand extends BuildSubCommand { ...@@ -42,6 +43,7 @@ class BuildWebCommand extends BuildSubCommand {
'to view and debug the original source code of a compiled and minified Dart ' 'to view and debug the original source code of a compiled and minified Dart '
'application.' 'application.'
); );
argParser.addOption('pwa-strategy', argParser.addOption('pwa-strategy',
defaultsTo: kOfflineFirst, defaultsTo: kOfflineFirst,
help: 'The caching strategy to be used by the PWA service worker.', help: 'The caching strategy to be used by the PWA service worker.',
...@@ -59,6 +61,13 @@ class BuildWebCommand extends BuildSubCommand { ...@@ -59,6 +61,13 @@ class BuildWebCommand extends BuildSubCommand {
'is not desirable', 'is not desirable',
}, },
); );
argParser.addOption('base-href',
help: 'Overrides the href attribute of the <base> tag in web/index.html. '
'No change is done to web/index.html file if this flag is not provided. '
'The value has to start and end with a slash "/". '
'For more information: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base'
);
} }
@override @override
...@@ -87,6 +96,20 @@ class BuildWebCommand extends BuildSubCommand { ...@@ -87,6 +96,20 @@ class BuildWebCommand extends BuildSubCommand {
if (buildInfo.isDebug) { if (buildInfo.isDebug) {
throwToolExit('debug builds cannot be built directly for the web. Try using "flutter run"'); throwToolExit('debug builds cannot be built directly for the web. Try using "flutter run"');
} }
if (stringArg('base-href') != null && !(stringArg('base-href').startsWith('/') && stringArg('base-href').endsWith('/'))) {
throwToolExit('base-href should start and end with /');
}
if (!globals.fs.currentDirectory
.childDirectory('web')
.childFile('index.html')
.readAsStringSync()
.contains(kBaseHrefPlaceholder) &&
stringArg('base-href') != null) {
throwToolExit(
"Couldn't find the placeholder for base href. "
r'Please add `<base href="$FLUTTER_BASE_HREF">` to web/index.html'
);
}
displayNullSafetyMode(buildInfo); displayNullSafetyMode(buildInfo);
await buildWeb( await buildWeb(
flutterProject, flutterProject,
...@@ -96,6 +119,7 @@ class BuildWebCommand extends BuildSubCommand { ...@@ -96,6 +119,7 @@ class BuildWebCommand extends BuildSubCommand {
stringArg('pwa-strategy'), stringArg('pwa-strategy'),
boolArg('source-maps'), boolArg('source-maps'),
boolArg('native-null-assertions'), boolArg('native-null-assertions'),
stringArg('base-href'),
); );
return FlutterCommandResult.success(); return FlutterCommandResult.success();
} }
......
...@@ -28,6 +28,7 @@ import '../base/logger.dart'; ...@@ -28,6 +28,7 @@ import '../base/logger.dart';
import '../base/net.dart'; import '../base/net.dart';
import '../base/platform.dart'; import '../base/platform.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../build_system/targets/web.dart';
import '../bundle.dart'; import '../bundle.dart';
import '../cache.dart'; import '../cache.dart';
import '../compile.dart'; import '../compile.dart';
...@@ -488,6 +489,12 @@ class WebAssetServer implements AssetReader { ...@@ -488,6 +489,12 @@ class WebAssetServer implements AssetReader {
.childFile('index.html'); .childFile('index.html');
if (indexFile.existsSync()) { if (indexFile.existsSync()) {
String indexFileContent = indexFile.readAsStringSync();
if (indexFileContent.contains(kBaseHrefPlaceholder)) {
indexFileContent = indexFileContent.replaceAll(kBaseHrefPlaceholder, '/');
headers[HttpHeaders.contentLengthHeader] = indexFileContent.length.toString();
return shelf.Response.ok(indexFileContent,headers: headers);
}
headers[HttpHeaders.contentLengthHeader] = headers[HttpHeaders.contentLengthHeader] =
indexFile.lengthSync().toString(); indexFile.lengthSync().toString();
return shelf.Response.ok(indexFile.openRead(), headers: headers); return shelf.Response.ok(indexFile.openRead(), headers: headers);
...@@ -1025,13 +1032,12 @@ String _stripTrailingSlashes(String path) { ...@@ -1025,13 +1032,12 @@ String _stripTrailingSlashes(String path) {
String _parseBasePathFromIndexHtml(File indexHtml) { String _parseBasePathFromIndexHtml(File indexHtml) {
final String htmlContent = final String htmlContent =
indexHtml.existsSync() ? indexHtml.readAsStringSync() : _kDefaultIndex; indexHtml.existsSync() ? indexHtml.readAsStringSync() : _kDefaultIndex;
final Document document = parse(htmlContent); final Document document = parse(htmlContent);
final Element baseElement = document.querySelector('base'); final Element baseElement = document.querySelector('base');
String baseHref = String baseHref =
baseElement?.attributes == null ? null : baseElement.attributes['href']; baseElement?.attributes == null ? null : baseElement.attributes['href'];
if (baseHref == null) { if (baseHref == null || baseHref == kBaseHrefPlaceholder) {
baseHref = ''; baseHref = '';
} else if (!baseHref.startsWith('/')) { } else if (!baseHref.startsWith('/')) {
throw ToolExit( throw ToolExit(
......
...@@ -297,6 +297,7 @@ class ResidentWebRunner extends ResidentRunner { ...@@ -297,6 +297,7 @@ class ResidentWebRunner extends ResidentRunner {
kNoneWorker, kNoneWorker,
true, true,
debuggingOptions.nativeNullAssertions, debuggingOptions.nativeNullAssertions,
null,
); );
} }
await device.device.startApp( await device.device.startApp(
...@@ -365,6 +366,7 @@ class ResidentWebRunner extends ResidentRunner { ...@@ -365,6 +366,7 @@ class ResidentWebRunner extends ResidentRunner {
kNoneWorker, kNoneWorker,
true, true,
debuggingOptions.nativeNullAssertions, debuggingOptions.nativeNullAssertions,
kBaseHref,
); );
} on ToolExit { } on ToolExit {
return OperationResult(1, 'Failed to recompile application.'); return OperationResult(1, 'Failed to recompile application.');
......
...@@ -26,6 +26,7 @@ Future<void> buildWeb( ...@@ -26,6 +26,7 @@ Future<void> buildWeb(
String serviceWorkerStrategy, String serviceWorkerStrategy,
bool sourceMaps, bool sourceMaps,
bool nativeNullAssertions, bool nativeNullAssertions,
String baseHref,
) async { ) async {
if (!flutterProject.web.existsSync()) { if (!flutterProject.web.existsSync()) {
throwToolExit('Missing index.html.'); throwToolExit('Missing index.html.');
...@@ -49,6 +50,7 @@ Future<void> buildWeb( ...@@ -49,6 +50,7 @@ Future<void> buildWeb(
kTargetFile: target, kTargetFile: target,
kHasWebPlugins: hasWebPlugins.toString(), kHasWebPlugins: hasWebPlugins.toString(),
kCspMode: csp.toString(), kCspMode: csp.toString(),
kBaseHref : baseHref,
kSourceMapsEnabled: sourceMaps.toString(), kSourceMapsEnabled: sourceMaps.toString(),
kNativeNullAssertions: nativeNullAssertions.toString(), kNativeNullAssertions: nativeNullAssertions.toString(),
if (serviceWorkerStrategy != null) if (serviceWorkerStrategy != null)
......
...@@ -10,8 +10,11 @@ ...@@ -10,8 +10,11 @@
For more details: For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
--> -->
<base href="/"> <base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible"> <meta content="IE=Edge" http-equiv="X-UA-Compatible">
......
...@@ -60,6 +60,7 @@ void main() { ...@@ -60,6 +60,7 @@ void main() {
null, null,
true, true,
true, true,
null,
), throwsToolExit()); ), throwsToolExit());
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Platform: () => fakePlatform, Platform: () => fakePlatform,
...@@ -119,6 +120,7 @@ void main() { ...@@ -119,6 +120,7 @@ void main() {
'DartObfuscation': 'false', 'DartObfuscation': 'false',
'TrackWidgetCreation': 'false', 'TrackWidgetCreation': 'false',
'TreeShakeIcons': 'false', 'TreeShakeIcons': 'false',
'baseHref': null,
}); });
}), }),
}); });
......
...@@ -101,6 +101,33 @@ void main() { ...@@ -101,6 +101,33 @@ void main() {
expect(environment.outputDir.childFile('version.json'), exists); expect(environment.outputDir.childFile('version.json'), exists);
})); }));
test('Base href is created in index.html with given base-href after release build', () => testbed.run(() async {
environment.defines[kBuildMode] = 'release';
environment.defines[kBaseHref] = '/basehreftest/';
final Directory webResources = environment.projectDir.childDirectory('web');
webResources.childFile('index.html').createSync(recursive: true);
webResources.childFile('index.html').writeAsStringSync('''
<!DOCTYPE html><html><base href="$kBaseHrefPlaceholder"><head></head></html>
''');
environment.buildDir.childFile('main.dart.js').createSync();
await const WebReleaseBundle().build(environment);
expect(environment.outputDir.childFile('index.html').readAsStringSync(), contains('/basehreftest/'));
}));
test('null base href does not override existing base href in index.html', () => testbed.run(() async {
environment.defines[kBuildMode] = 'release';
final Directory webResources = environment.projectDir.childDirectory('web');
webResources.childFile('index.html').createSync(recursive: true);
webResources.childFile('index.html').writeAsStringSync('''
<!DOCTYPE html><html><head><base href='/basehreftest/'></head></html>
''');
environment.buildDir.childFile('main.dart.js').createSync();
await const WebReleaseBundle().build(environment);
expect(environment.outputDir.childFile('index.html').readAsStringSync(), contains('/basehreftest/'));
}));
test('WebReleaseBundle copies dart2js output and resource files to output directory', () => testbed.run(() async { test('WebReleaseBundle copies dart2js output and resource files to output directory', () => testbed.run(() async {
environment.defines[kBuildMode] = 'release'; environment.defines[kBuildMode] = 'release';
final Directory webResources = environment.projectDir.childDirectory('web'); final Directory webResources = environment.projectDir.childDirectory('web');
......
...@@ -10,6 +10,7 @@ import 'package:flutter_tools/src/artifacts.dart'; ...@@ -10,6 +10,7 @@ import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/targets/web.dart';
import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/compile.dart';
import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/globals.dart' as globals;
...@@ -209,6 +210,19 @@ void main() { ...@@ -209,6 +210,19 @@ void main() {
expect(await response.readAsString(), htmlContent); expect(await response.readAsString(), htmlContent);
})); }));
test('serves index.html at / if href attribute is $kBaseHrefPlaceholder', () => testbed.run(() async {
const String htmlContent = '<html><head><base href ="$kBaseHrefPlaceholder"></head><body id="test"></body></html>';
final Directory webDir = globals.fs.currentDirectory.childDirectory('web')
..createSync();
webDir.childFile('index.html').writeAsStringSync(htmlContent);
final Response response = await webAssetServer
.handleRequest(Request('GET', Uri.parse('http://foobar/')));
expect(response.statusCode, HttpStatus.ok);
expect(await response.readAsString(), htmlContent.replaceAll(kBaseHrefPlaceholder, '/'));
}));
test('does not serve outside the base path', () => testbed.run(() async { test('does not serve outside the base path', () => testbed.run(() async {
webAssetServer.basePath = 'base/path'; webAssetServer.basePath = 'base/path';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment