Skip to content

Commit 3be0441

Browse files
authored
[web] add --static-assets-url argument to build web (#171638)
Closes flutter/flutter#171637 As mentioned in the issue, I am proposing a new argument to the flutter build web command: `--static-assets-url`. This argument would accept a full URL string ending with `/` as its value. During the build process, the Flutter tool would use this value to replace a dedicated placeholder within the `web/index.html` file (inspired by `--base-href` approach). Example Implementation: A developer would modify their `web/index.html` to use a new placeholder, for instance, `$FLUTTER_STATIC_ASSETS_URL`: ```html ... <body> <script> {{flutter_js}} {{flutter_build_config}} _flutter.loader.load({ config: { entryPointBaseUrl: "$FLUTTER_STATIC_ASSETS_URL", }, onEntrypointLoaded: async function (engineInitializer) { const appRunner = await engineInitializer.initializeEngine({ assetBase: "$FLUTTER_STATIC_ASSETS_URL", }); await appRunner.runApp(); }, }); </script> </body> ... ``` The build command would be run with the new flag: `flutter build web --static-assets-url="https://static.company.com/some-webapp/“` - and the resulting `build/web/index.html` would have the placeholder replaced: ```html ... <body> <script> {{flutter_js}} {{flutter_build_config}} _flutter.loader.load({ config: { entryPointBaseUrl: "https://static.company.com/some-webapp/", }, onEntrypointLoaded: async function (engineInitializer) { const appRunner = await engineInitializer.initializeEngine({ assetBase: "https://static.company.com/some-webapp/", }); await appRunner.runApp(); }, }); </script> </body> ... ```
1 parent 18e7e64 commit 3be0441

8 files changed

Lines changed: 174 additions & 0 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,7 @@ _flutter.buildConfig = ${jsonEncode(buildConfig)};
661661

662662
final String indexHtmlContent = indexHtmlTemplate.withSubstitutions(
663663
baseHref: environment.defines[kBaseHref] ?? '/',
664+
staticAssetsUrl: environment.defines[kStaticAssetsUrl] ?? '/',
664665
serviceWorkerVersion: serviceWorkerVersion,
665666
flutterJsFile: flutterJsFile,
666667
buildConfig: buildConfig,

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ class BuildWebCommand extends BuildSubCommand {
4646
'The value has to start and end with a slash "/". '
4747
'For more information: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base',
4848
);
49+
argParser.addOption(
50+
'static-assets-url',
51+
help:
52+
'Used when serving the static assets from a different domain the application is hosted on. '
53+
'The value has to end with a slash "/". '
54+
'When this is set, it will replace all $kStaticAssetsUrlPlaceholder in web/index.html for the given value.',
55+
);
4956
argParser.addOption(
5057
'pwa-strategy',
5158
defaultsTo: ServiceWorkerStrategy.offlineFirst.cliName,
@@ -242,12 +249,19 @@ class BuildWebCommand extends BuildSubCommand {
242249

243250
final BuildInfo buildInfo = await getBuildInfo();
244251
final String? baseHref = stringArg('base-href');
252+
final String? staticAssetsUrl = stringArg('static-assets-url');
245253
if (baseHref != null && !(baseHref.startsWith('/') && baseHref.endsWith('/'))) {
246254
throwToolExit(
247255
'Received a --base-href value of "$baseHref"\n'
248256
'--base-href should start and end with /',
249257
);
250258
}
259+
if (staticAssetsUrl != null && !staticAssetsUrl.endsWith('/')) {
260+
throwToolExit(
261+
'Received a --static-assets-url value of "$staticAssetsUrl"\n'
262+
'--static-assets-url should end with /',
263+
);
264+
}
251265
if (!project.web.existsSync()) {
252266
throwToolExit(
253267
'This project is not configured for the web.\n'
@@ -285,6 +299,7 @@ class BuildWebCommand extends BuildSubCommand {
285299
ServiceWorkerStrategy.fromCliName(stringArg('pwa-strategy')),
286300
compilerConfigs: compilerConfigs,
287301
baseHref: baseHref,
302+
staticAssetsUrl: staticAssetsUrl,
288303
outputDirectoryPath: outputDirectoryPath,
289304
);
290305
return FlutterCommandResult.success();

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,8 @@ _flutter.buildConfig = ${jsonEncode(buildConfig)};
648648
indexHtml.withSubstitutions(
649649
// Currently, we don't support --base-href for the "run" command.
650650
baseHref: '/',
651+
// Currently, we don't support --static-assets-url for the "run" command.
652+
staticAssetsUrl: '/',
651653
serviceWorkerVersion: null,
652654
buildConfig: _buildConfigString,
653655
flutterJsFile: _flutterJsFile,

packages/flutter_tools/lib/src/web/compile.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ const kHasWebPlugins = 'HasWebPlugins';
3232
/// Base href to set in index.html in flutter build command
3333
const kBaseHref = 'baseHref';
3434

35+
/// Static assets url to set in index.html in flutter build command
36+
const kStaticAssetsUrl = 'staticAssetsUrl';
37+
3538
/// The caching strategy to use for service worker generation.
3639
const kServiceWorkerStrategy = 'ServiceWorkerStrategy';
3740

@@ -64,6 +67,7 @@ class WebBuilder {
6467
ServiceWorkerStrategy serviceWorkerStrategy, {
6568
required List<WebCompilerConfig> compilerConfigs,
6669
String? baseHref,
70+
String? staticAssetsUrl,
6771
String? outputDirectoryPath,
6872
}) async {
6973
final bool hasWebPlugins = (await findPlugins(
@@ -99,6 +103,7 @@ class WebBuilder {
99103
kTargetFile: target,
100104
kHasWebPlugins: hasWebPlugins.toString(),
101105
if (baseHref != null) kBaseHref: baseHref,
106+
if (staticAssetsUrl != null) kStaticAssetsUrl: staticAssetsUrl,
102107
kServiceWorkerStrategy: serviceWorkerStrategy.cliName,
103108
...buildInfo.toBuildSystemEnvironment(),
104109
},

packages/flutter_tools/lib/src/web_template.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'base/file_system.dart';
1111

1212
/// Placeholder for base href
1313
const kBaseHrefPlaceholder = r'$FLUTTER_BASE_HREF';
14+
const kStaticAssetsUrlPlaceholder = r'$FLUTTER_STATIC_ASSETS_URL';
1415

1516
class WebTemplateWarning {
1617
WebTemplateWarning(this.warningText, this.lineNumber);
@@ -101,13 +102,18 @@ class WebTemplate {
101102
required File flutterJsFile,
102103
String? buildConfig,
103104
String? flutterBootstrapJs,
105+
String? staticAssetsUrl,
104106
}) {
105107
String newContent = _content;
106108

107109
if (newContent.contains(kBaseHrefPlaceholder)) {
108110
newContent = newContent.replaceAll(kBaseHrefPlaceholder, baseHref);
109111
}
110112

113+
if (newContent.contains(kStaticAssetsUrlPlaceholder) && staticAssetsUrl != null) {
114+
newContent = newContent.replaceAll(kStaticAssetsUrlPlaceholder, staticAssetsUrl);
115+
}
116+
111117
if (serviceWorkerVersion != null) {
112118
newContent = newContent
113119
.replaceFirst(

packages/flutter_tools/test/commands.shard/hermetic/build_web_test.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,33 @@ void main() {
654654
},
655655
);
656656

657+
testUsingContext(
658+
'Rejects --static-assets-url value that does not end with /',
659+
() async {
660+
final buildCommand = TestWebBuildCommand(fileSystem: fileSystem);
661+
final CommandRunner<void> runner = createTestCommandRunner(buildCommand);
662+
663+
await expectLater(
664+
runner.run(<String>[
665+
'build',
666+
'web',
667+
'--no-pub',
668+
'--static-assets-url=i_dont_end_with_forward_slash',
669+
]),
670+
throwsToolExit(
671+
message:
672+
'Received a --static-assets-url value of "i_dont_end_with_forward_slash"\n'
673+
'--static-assets-url should end with /',
674+
),
675+
);
676+
},
677+
overrides: <Type, Generator>{
678+
Platform: () => fakePlatform,
679+
FileSystem: () => fileSystem,
680+
ProcessManager: () => processManager,
681+
},
682+
);
683+
657684
testUsingContext(
658685
'flutter build web option visibility',
659686
() async {

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,45 @@ name: foo
254254
}),
255255
);
256256

257+
group('--static-assets-url', () {
258+
test(
259+
'WebTemplatedFiles replaces placeholder with given value',
260+
() => testbed.run(() async {
261+
environment.defines[kStaticAssetsUrl] = 'https://static.example.com/example-app/';
262+
final Directory webResources = environment.projectDir.childDirectory('web');
263+
webResources.childFile('index.html').createSync(recursive: true);
264+
webResources.childFile('index.html').writeAsStringSync('''
265+
<!DOCTYPE html><html><body><script>const staticAssetsUrl = "$kStaticAssetsUrlPlaceholder";</script></body></html>
266+
''');
267+
environment.buildDir.childFile('main.dart.js').createSync();
268+
await WebTemplatedFiles(<Map<String, Object?>>[]).build(environment);
269+
270+
expect(
271+
environment.outputDir.childFile('index.html').readAsStringSync(),
272+
contains('https://static.example.com/example-app/'),
273+
);
274+
}),
275+
);
276+
277+
test(
278+
'WebTemplatedFiles replaces placeholder with / when not set',
279+
() => testbed.run(() async {
280+
final Directory webResources = environment.projectDir.childDirectory('web');
281+
webResources.childFile('index.html').createSync(recursive: true);
282+
webResources.childFile('index.html').writeAsStringSync('''
283+
<!DOCTYPE html><html><body><script>const staticAssetsUrl = "$kStaticAssetsUrlPlaceholder";</script></body></html>
284+
''');
285+
environment.buildDir.childFile('main.dart.js').createSync();
286+
await WebTemplatedFiles(<Map<String, Object?>>[]).build(environment);
287+
288+
expect(
289+
environment.outputDir.childFile('index.html').readAsStringSync(),
290+
contains('staticAssetsUrl = "/"'),
291+
);
292+
}),
293+
);
294+
});
295+
257296
test(
258297
'WebReleaseBundle copies dart2js output and resource files to output directory',
259298
() => testbed.run(() async {

packages/flutter_tools/test/general.shard/web_template_test.dart

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,70 @@ const htmlSample3 = '''
219219
</html>
220220
''';
221221

222+
const htmlSampleStaticAssetsUrl =
223+
'''
224+
<!DOCTYPE html>
225+
<html>
226+
<head>
227+
<title></title>
228+
<base href="/">
229+
<meta charset="utf-8">
230+
<link rel="icon" type="image/png" href="favicon.png"/>
231+
</head>
232+
<body>
233+
<div></div>
234+
<script>
235+
{{flutter_js}}
236+
{{flutter_build_config}}
237+
_flutter.loader.load({
238+
config: {
239+
entryPointBaseUrl: "$kStaticAssetsUrlPlaceholder",
240+
},
241+
onEntrypointLoaded: async function (engineInitializer) {
242+
const appRunner = await engineInitializer.initializeEngine({
243+
assetBase: "$kStaticAssetsUrlPlaceholder",
244+
});
245+
246+
await appRunner.runApp();
247+
},
248+
});
249+
</script>
250+
</body>
251+
</html>
252+
''';
253+
254+
String htmlSampleStaticAssetsUrlReplaced({required String staticAssetsUrl}) =>
255+
'''
256+
<!DOCTYPE html>
257+
<html>
258+
<head>
259+
<title></title>
260+
<base href="/">
261+
<meta charset="utf-8">
262+
<link rel="icon" type="image/png" href="favicon.png"/>
263+
</head>
264+
<body>
265+
<div></div>
266+
<script>
267+
(flutter.js content)
268+
{{flutter_build_config}}
269+
_flutter.loader.load({
270+
config: {
271+
entryPointBaseUrl: "$staticAssetsUrl",
272+
},
273+
onEntrypointLoaded: async function (engineInitializer) {
274+
const appRunner = await engineInitializer.initializeEngine({
275+
assetBase: "$staticAssetsUrl",
276+
});
277+
278+
await appRunner.runApp();
279+
},
280+
});
281+
</script>
282+
</body>
283+
</html>
284+
''';
285+
222286
void main() {
223287
final fs = MemoryFileSystem();
224288
final File flutterJs = fs.file('flutter.js');
@@ -300,6 +364,21 @@ void main() {
300364
);
301365
});
302366

367+
test('applies substitutions to static assets url', () {
368+
const indexHtml = WebTemplate(htmlSampleStaticAssetsUrl);
369+
const expectedStaticAssetsUrl = 'https://static.example.com/my-app/';
370+
371+
expect(
372+
indexHtml.withSubstitutions(
373+
baseHref: '/',
374+
serviceWorkerVersion: 'v123xyz',
375+
flutterJsFile: flutterJs,
376+
staticAssetsUrl: expectedStaticAssetsUrl,
377+
),
378+
htmlSampleStaticAssetsUrlReplaced(staticAssetsUrl: expectedStaticAssetsUrl),
379+
);
380+
});
381+
303382
test('re-parses after substitutions', () {
304383
const indexHtml = WebTemplate(htmlSample2);
305384
expect(WebTemplate.baseHref(htmlSample2), ''); // Placeholder base href.

0 commit comments

Comments
 (0)