Skip to content
Merged
9 changes: 1 addition & 8 deletions packages/flutter_tools/lib/src/commands/build_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,7 @@ class BuildWebCommand extends BuildSubCommand {
// Flutter web-specific options
//
argParser.addSeparator('Flutter web options');
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',
);
usesBaseHrefOption();
argParser.addOption(
'static-assets-url',
help:
Expand Down
9 changes: 9 additions & 0 deletions packages/flutter_tools/lib/src/commands/run.dart
Original file line number Diff line number Diff line change
Expand Up @@ -417,11 +417,20 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
stringArg('web-tls-cert-key-path') ?? fileConfig.https?.certKeyPath,
);

final String? baseHref = stringArg('base-href') ?? fileConfig.baseHref;
if (baseHref != null && !(baseHref.startsWith('/') && baseHref.endsWith('/'))) {
throwToolExit(
'Received a --base-href value of "$baseHref"\n'
'--base-href should start and end with /',
);
}

final WebDevServerConfig webDevServerConfig = fileConfig.copyWith(
host: stringArg('web-hostname'),
port: webPort,
https: httpsConfig,
headers: extractWebHeaders(),
baseHref: baseHref,
);
return webDevServerConfig;
}
Expand Down
12 changes: 9 additions & 3 deletions packages/flutter_tools/lib/src/isolated/web_asset_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ class WebAssetServer implements AssetReader {
required this.useLocalCanvasKit,
required this.fileSystem,
required this.logger,
String? baseHref,
Map<String, String> webDefines = const <String, String>{},
}) : basePath = WebTemplate.baseHref(htmlTemplate(fileSystem, 'index.html', _kDefaultIndex)),
_baseHref = baseHref,
_webDefines = webDefines {
// TODO(srujzs): Remove this assertion when the library bundle format is
// supported without canary mode.
Expand Down Expand Up @@ -268,8 +270,12 @@ class WebAssetServer implements AssetReader {
useLocalCanvasKit: useLocalCanvasKit,
fileSystem: fileSystem,
logger: logger,
baseHref: webDevServerConfig.baseHref,
webDefines: webDefines,
);
if (webDevServerConfig.baseHref case final String baseHref?) {
server.basePath = stripLeadingSlash(baseHref.substring(0, baseHref.length - 1));
}
final int selectedPort = server.selectedPort;

final cleanHost = hostname == webDevAnyHostDefault ? 'localhost' : hostname;
Expand Down Expand Up @@ -407,6 +413,7 @@ class WebAssetServer implements AssetReader {
final bool _ddcModuleSystem;
final bool _canaryFeatures;
final Map<String, String> _webDefines;
final String? _baseHref;
final HttpServer _httpServer;
final _webMemoryFS = WebMemoryFS();
final PackageConfig _packages;
Expand Down Expand Up @@ -633,7 +640,7 @@ _flutter.buildConfig = ${jsonEncode(buildConfig)};
generateDefaultFlutterBootstrapScript(includeServiceWorkerSettings: false),
);
return bootstrapTemplate.withSubstitutions(
baseHref: '/',
baseHref: _baseHref ?? '/',
serviceWorkerVersion: null,
buildConfig: _buildConfigString,
flutterJsFile: _flutterJsFile,
Expand All @@ -653,8 +660,7 @@ _flutter.buildConfig = ${jsonEncode(buildConfig)};
final WebTemplate indexHtml = getWebTemplate(fileSystem, 'index.html', _kDefaultIndex);
return shelf.Response.ok(
indexHtml.withSubstitutions(
// Currently, we don't support --base-href for the "run" command.
baseHref: '/',
baseHref: _baseHref ?? '/',
// Currently, we don't support --static-assets-url for the "run" command.
staticAssetsUrl: '/',
serviceWorkerVersion: null,
Expand Down
12 changes: 12 additions & 0 deletions packages/flutter_tools/lib/src/runner/flutter_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,18 @@ abstract class FlutterCommand extends Command<void> {
'skwasm renderer, use "--no-cross-origin-isolation".',
hide: !verboseHelp,
);
usesBaseHrefOption();
}

void usesBaseHrefOption() {
argParser.addOption(
'base-href',
help:
'Overrides the href attribute of the <base> tag in web/index.html. '
'No change is made to web/index.html file if this flag is not provided. '
'The value must start and end with "/". '
'For more information: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base',
);
Comment on lines +413 to +420

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs its own usesWebBaseHrefFlag() that is shared between run and build.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, since --base-href flag is duplicated; defined separately across both

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 3e2148e

}

void usesTargetOption() {
Expand Down
11 changes: 10 additions & 1 deletion packages/flutter_tools/lib/src/web/devfs_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const _kProxy = 'proxy';
const _kHeaders = 'headers';
const _kCertKeyPath = 'cert-key-path';
const _kCertPath = 'cert-path';
const _kBaseHref = 'base-href';
Comment thread
Mastersam07 marked this conversation as resolved.

/// Checks if a given [value] has the expected type [T].
///
Expand All @@ -50,6 +51,7 @@ class WebDevServerConfig {
this.port = 0,
this.https,
this.proxy = const <ProxyRule>[],
this.baseHref,
});

factory WebDevServerConfig.fromYaml(YamlMap yaml, Logger logger) {
Expand Down Expand Up @@ -100,12 +102,15 @@ class WebDevServerConfig {
...?proxyList?.whereType<YamlMap>().map((e) => ProxyRule.fromYaml(e, logger)).nonNulls,
];

final String? baseHref = _validateType<String>(value: yaml[_kBaseHref], fieldName: _kBaseHref);

return WebDevServerConfig(
headers: headers,
host: host ?? webDevAnyHostDefault,
port: port ?? 0,
https: https == null ? null : HttpsConfig.fromYaml(https),
proxy: proxyRules,
baseHref: baseHref,
);
}

Expand Down Expand Up @@ -162,13 +167,15 @@ class WebDevServerConfig {
HttpsConfig? https,
Map<String, String>? headers,
List<ProxyRule>? proxy,
String? baseHref,
}) {
return WebDevServerConfig(
host: host ?? this.host,
port: port ?? this.port,
https: https ?? this.https,
headers: {...this.headers, ...?headers},
proxy: proxy ?? this.proxy,
baseHref: baseHref ?? this.baseHref,
);
}

Expand All @@ -177,6 +184,7 @@ class WebDevServerConfig {
final int port;
final HttpsConfig? https;
final List<ProxyRule> proxy;
final String? baseHref;

@override
String toString() {
Expand All @@ -186,7 +194,8 @@ WebDevServerConfig:
$_kHost: $host
$_kPort: $port
$_kHttps: $https
$_kProxy: $proxy''';
$_kProxy: $proxy
$_kBaseHref: $baseHref''';
}
}

Expand Down
115 changes: 115 additions & 0 deletions packages/flutter_tools/test/commands.shard/hermetic/run_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1399,6 +1399,121 @@ server:
},
);
});

group('--base-href', () {
late FakeWebRunnerFactory fakeWebRunnerFactory;

setUp(() {
fakeWebRunnerFactory = FakeWebRunnerFactory();

fileSystem.file('lib/main.dart').createSync(recursive: true);
fileSystem.file('pubspec.yaml').createSync();
fileSystem.file('.dart_tool/package_config.json')
..createSync(recursive: true)
..writeAsStringSync('''
{
"packages": [],
"configVersion": 2
}
''');
final device = FakeDevice(
isLocalEmulator: true,
platformType: PlatformType.web,
targetPlatform: TargetPlatform.web_javascript,
);
testDeviceManager.devices = <Device>[device];
});

testUsingContext(
'passes base-href to WebDevServerConfig',
() async {
final command = RunCommand();
await createTestCommandRunner(
command,
).run(<String>['run', '--no-pub', '--no-hot', '--base-href=/preview/']);

expect(fakeWebRunnerFactory.lastOptions, isNotNull);
expect(fakeWebRunnerFactory.lastOptions!.webDevServerConfig, isNotNull);
expect(fakeWebRunnerFactory.lastOptions!.webDevServerConfig!.baseHref, '/preview/');
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
DeviceManager: () => testDeviceManager,
FeatureFlags: () => FakeFeatureFlags(),
WebRunnerFactory: () => fakeWebRunnerFactory,
},
);

testUsingContext(
'throws ToolExit when base-href does not start with /',
() async {
final command = RunCommand();
await expectLater(
() => createTestCommandRunner(
command,
).run(<String>['run', '--no-pub', '--no-hot', '--base-href=preview/']),
throwsToolExit(
message: '--base-href should start and end with /',
),
);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
DeviceManager: () => testDeviceManager,
FeatureFlags: () => FakeFeatureFlags(),
WebRunnerFactory: () => fakeWebRunnerFactory,
},
);

testUsingContext(
'throws ToolExit when base-href does not end with /',
() async {
final command = RunCommand();
await expectLater(
() => createTestCommandRunner(
command,
).run(<String>['run', '--no-pub', '--no-hot', '--base-href=/preview']),
throwsToolExit(
message: '--base-href should start and end with /',
),
);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
DeviceManager: () => testDeviceManager,
FeatureFlags: () => FakeFeatureFlags(),
WebRunnerFactory: () => fakeWebRunnerFactory,
},
);

testUsingContext(
'base-href defaults to null when not provided',
() async {
final command = RunCommand();
await createTestCommandRunner(
command,
).run(<String>['run', '--no-pub', '--no-hot']);

expect(fakeWebRunnerFactory.lastOptions, isNotNull);
expect(fakeWebRunnerFactory.lastOptions!.webDevServerConfig, isNotNull);
expect(fakeWebRunnerFactory.lastOptions!.webDevServerConfig!.baseHref, isNull);
},
overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
Logger: () => logger,
DeviceManager: () => testDeviceManager,
FeatureFlags: () => FakeFeatureFlags(),
WebRunnerFactory: () => fakeWebRunnerFactory,
},
);
});
});

group('terminal', () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -388,5 +388,61 @@ void main() {
expect(server.defaultResponseHeaders['Cross-Origin-Embedder-Policy'], isNull);
},
);

testWithoutContext('sets basePath from baseHref config', () async {
final WebAssetServer server = await WebAssetServer.start(
null,
null,
false,
false,
false,
BuildInfo.debug,
false,
const DartDevelopmentServiceConfiguration(enable: false),
Uri.base,
null,
crossOriginIsolation: false,
webDevServerConfig: const WebDevServerConfig(
host: 'localhost',
baseHref: '/preview/',
),
webRenderer: WebRendererMode.canvaskit,
isWasm: false,
useLocalCanvasKit: false,
testMode: true,
fileSystem: fileSystem,
logger: BufferLogger.test(),
platform: platform,
);

expect(server.basePath, 'preview');
});

testWithoutContext('basePath defaults to empty when baseHref is not provided', () async {
final WebAssetServer server = await WebAssetServer.start(
null,
null,
false,
false,
false,
BuildInfo.debug,
false,
const DartDevelopmentServiceConfiguration(enable: false),
Uri.base,
null,
crossOriginIsolation: false,
webDevServerConfig: const WebDevServerConfig(host: 'localhost'),
webRenderer: WebRendererMode.canvaskit,
isWasm: false,
useLocalCanvasKit: false,
testMode: true,
fileSystem: fileSystem,
logger: BufferLogger.test(),
platform: platform,
);

expect(server.basePath, isEmpty);
});

});
}