Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 39 additions & 20 deletions lib/web_ui/lib/src/engine/assets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,48 @@ const Map<String, String> testFontUrls = <String, String>{

/// This class downloads assets over the network.
///
/// The assets are resolved relative to [assetsDir] inside the directory
/// containing the currently executing JS script.
/// Assets are resolved relative to [assetsDir] inside the absolute base
/// specified by [assetBase] (optional).
///
/// By default, URLs are relative to the `<base>` of the current website.
class AssetManager {
/// Initializes [AssetManager] with path to assets relative to baseUrl.
const AssetManager({this.assetsDir = _defaultAssetsDir});
/// Initializes [AssetManager] with paths.
AssetManager({
this.assetsDir = _defaultAssetsDir,
String? assetBase,
}) : assert(
assetBase == null || assetBase.endsWith('/'),
'`assetBase` must end with a `/` character.',
),
_assetBase = assetBase;

static const String _defaultAssetsDir = 'assets';

/// The directory containing the assets.
final String assetsDir;

String? get _baseUrl {
return domWindow.document
.querySelectorAll('meta')
.where((DomElement domNode) => domInstanceOfString(domNode,
'HTMLMetaElement'))
.map((DomElement domNode) => domNode as DomHTMLMetaElement)
.firstWhereOrNull(
(DomHTMLMetaElement element) => element.name == 'assetBase')
?.content;
/// The absolute base URL for assets.
String? _assetBase;

// Cache a value for `_assetBase` so we don't hit the DOM multiple times.
String get _baseUrl => _assetBase ??= _deprecatedAssetBase ?? '';

// Retrieves the `assetBase` value from the DOM.
//
// This warns the user and points them to the new initializeEngine style.
String? get _deprecatedAssetBase {
final DomHTMLMetaElement? meta = domWindow.document
.querySelector('meta[name=assetBase]') as DomHTMLMetaElement?;

final String? fallbackBaseUrl = meta?.content;

if (fallbackBaseUrl != null) {
// Warn users that they're using a deprecated configuration style...
domWindow.console.warn('The `assetBase` meta tag is now deprecated.\n'
'Use engineInitializer.initializeEngine(config) instead.\n'
'See: https://docs.flutter.dev/development/platform-integration/web/initialization');
}
return fallbackBaseUrl;
}

/// Returns the URL to load the asset from, given the asset key.
Expand All @@ -67,7 +89,7 @@ class AssetManager {
if (Uri.parse(asset).hasScheme) {
return Uri.encodeFull(asset);
}
return Uri.encodeFull('${_baseUrl ?? ''}$assetsDir/$asset');
return Uri.encodeFull('$_baseUrl$assetsDir/$asset');
}

/// Loads an asset and returns the server response.
Expand All @@ -90,7 +112,7 @@ class AssetManager {
}

/// An asset manager that gives fake empty responses for assets.
class WebOnlyMockAssetManager implements AssetManager {
class WebOnlyMockAssetManager extends AssetManager {
/// Mock asset directory relative to base url.
String defaultAssetsDir = '';

Expand All @@ -113,9 +135,6 @@ class WebOnlyMockAssetManager implements AssetManager {
@override
String get assetsDir => defaultAssetsDir;

@override
String get _baseUrl => '';

@override
String getAssetUrl(String asset) => asset;

Expand All @@ -127,7 +146,7 @@ class WebOnlyMockAssetManager implements AssetManager {
status: 200,
payload: MockHttpFetchPayload(
byteBuffer: _toByteData(utf8.encode(defaultAssetManifest)).buffer,
)
),
);
}
if (asset == getAssetUrl('FontManifest.json')) {
Expand All @@ -136,7 +155,7 @@ class WebOnlyMockAssetManager implements AssetManager {
status: 200,
payload: MockHttpFetchPayload(
byteBuffer: _toByteData(utf8.encode(defaultFontManifest)).buffer,
)
),
);
}

Expand Down
30 changes: 30 additions & 0 deletions lib/web_ui/lib/src/engine/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,32 @@ class FlutterConfiguration {
// runtime. Runtime-supplied values take precedence over environment
// variables.

/// The absolute base URL of the location of the `assets` directory of the app.
///
/// This value is useful when Flutter web assets are deployed to a separate
/// domain (or subdirectory) from which the index.html is served, for example:
///
/// * Application: https://www.my-app.com/
/// * Flutter Assets: https://cdn.example.com/my-app/build-hash/assets/
///
/// The `assetBase` value would be set to:
///
/// * `'https://cdn.example.com/my-app/build-hash/'`
///
/// It is also useful in the case that a Flutter web application is embedded
/// into another web app, in a way that the `<base>` tag of the index.html
/// cannot be set (because it'd break the host app), for example:
///
/// * Application: https://www.my-app.com/
/// * Flutter Assets: https://www.my-app.com/static/companion/flutter/assets/
///
/// The `assetBase` would be set to:
///
/// * `'/static/companion/flutter/'`
///
/// Do not confuse this configuration value with [canvasKitBaseUrl].
String? get assetBase => _configuration?.assetBase;

/// The base URL to use when downloading the CanvasKit script and associated
/// wasm.
///
Expand Down Expand Up @@ -267,6 +293,10 @@ external JsFlutterConfiguration? get _jsConfiguration;
class JsFlutterConfiguration {}

extension JsFlutterConfigurationExtension on JsFlutterConfiguration {
@JS('assetBase')
external JSString? get _assetBase;
String? get assetBase => _assetBase?.toDart;

@JS('canvasKitBaseUrl')
external JSString? get _canvasKitBaseUrl;
String? get canvasKitBaseUrl => _canvasKitBaseUrl?.toDart;
Expand Down
2 changes: 1 addition & 1 deletion lib/web_ui/lib/src/engine/initialization.dart
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ Future<void> initializeEngineServices({
}
};

assetManager ??= const AssetManager();
assetManager ??= AssetManager(assetBase: configuration.assetBase);
_setAssetManager(assetManager);

Future<void> initializeRendererCallback () async => renderer.initialize();
Expand Down
142 changes: 142 additions & 0 deletions lib/web_ui/test/engine/assets_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

@TestOn('browser')
library;

import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';

import '../common/matchers.dart';

void main() {
internalBootstrapBrowserTest(() => testMain);
}

void testMain() {
group('AssetManager getAssetUrl', () {
setUp(() {
// Remove the meta-tag from the environment before each test.
removeAssetBaseMeta();
});

test('initializes with default values', () {
final AssetManager assets = AssetManager();

expect(
assets.getAssetUrl('asset.txt'),
'assets/asset.txt',
reason: 'Default `assetsDir` is "assets".',
);
});

test('assetsDir changes the directory where assets are stored', () {
final AssetManager assets = AssetManager(assetsDir: 'static');

expect(assets.getAssetUrl('asset.txt'), 'static/asset.txt');
});

test('assetBase must end with slash', () {
expect(() {
AssetManager(assetBase: '/deployment');
}, throwsAssertionError);
});

test('assetBase can be relative', () {
final AssetManager assets = AssetManager(assetBase: 'base/');

expect(assets.getAssetUrl('asset.txt'), 'base/assets/asset.txt');
});

test('assetBase can be absolute', () {
final AssetManager assets = AssetManager(
assetBase: 'https://www.gstatic.com/my-app/',
);

expect(
assets.getAssetUrl('asset.txt'),
'https://www.gstatic.com/my-app/assets/asset.txt',
);
});

test('assetBase in conjunction with assetsDir, fully custom paths', () {
final AssetManager assets = AssetManager(
assetBase: '/asset/base/',
assetsDir: 'static',
);

expect(assets.getAssetUrl('asset.txt'), '/asset/base/static/asset.txt');
});

test('Fully-qualified asset URLs are untouched', () {
final AssetManager assets = AssetManager();

expect(
assets.getAssetUrl('https://static.my-app.com/favicon.ico'),
'https://static.my-app.com/favicon.ico',
);
});

test('Fully-qualified asset URLs are untouched (even with assetBase)', () {
final AssetManager assets = AssetManager(
assetBase: 'https://static.my-app.com/',
);

expect(
assets.getAssetUrl('https://static.my-app.com/favicon.ico'),
'https://static.my-app.com/favicon.ico',
);
});
});

group('AssetManager getAssetUrl with <meta name=assetBase> tag', () {
setUp(() {
removeAssetBaseMeta();
addAssetBaseMeta('/dom/base/');
});

test('reads value from DOM', () {
final AssetManager assets = AssetManager();

expect(assets.getAssetUrl('asset.txt'), '/dom/base/assets/asset.txt');
});

test('reads value from DOM (only once!)', () {
final AssetManager firstManager = AssetManager();
expect(
firstManager.getAssetUrl('asset.txt'),
'/dom/base/assets/asset.txt',
);

removeAssetBaseMeta();
final AssetManager anotherManager = AssetManager();

expect(
firstManager.getAssetUrl('asset.txt'),
'/dom/base/assets/asset.txt',
reason: 'The old value of the assetBase meta should be cached.',
);
expect(anotherManager.getAssetUrl('asset.txt'), 'assets/asset.txt');
});
});
}

/// Removes all meta-tags with name=assetBase.
void removeAssetBaseMeta() {
domWindow.document
.querySelectorAll('meta[name=assetBase]')
.forEach((DomElement element) {
element.remove();
});
}

/// Adds a meta-tag with name=assetBase and the passed-in [value].
void addAssetBaseMeta(String value) {
final DomHTMLMetaElement meta = createDomHTMLMetaElement()
..name = 'assetBase'
..content = value;

domDocument.head!.append(meta);
}