blob: fc568b710d1e6458fcc6e1d08e9a09a9a3c7e055 [file] [log] [blame]
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// Emitter for resource identifiers embedded in the program.
///
/// See [documentation](../../../doc/resource_identifiers.md) for examples.
///
/// .../foo_resource.dart:
///
/// @pragma('dart2js:resource-identifer')
/// Resource getResource(String group, int index) { ... }
///
/// .../my_resources.dart:
///
/// ...
/// getResource('group1', 1);
/// ...
/// getResource('group2', 2);
/// ...
/// getResource('group1', 10); // optimized away.
/// ...
/// getResource('group1', 100);
/// ...
/// getResource('group1', 1000); // optimized away.
/// ...
/// getResource('group1', 1001);
///
///
/// Some of the calls above are tree-shaken. Some are placed in one 'part' file
/// and the others in a different 'part' file. The generated resources file
/// contains the constant arguments to the calls, arranged by resource identifer
/// and 'part' file.
///
/// `main.js.resources.json`:
/// ```json
/// {...
/// "environment": { // Command-line environment
/// "foo": "bar", // -Dfoo=bar
/// }
/// "identifiers": [
/// {"name": "getResource",
/// "uri": ".../foo_resource.dart",
/// "nonconstant": false, // No calls without a constant.
/// "files": [
/// {"filename": "main.js_13.part.js",
/// "references": [
/// {"1": "group1", "2": 1},
/// {"1": "group1", "2": 1001},
/// {"1": "group2", "2": 2}
/// ]}
/// {"filename": "main.js_282.part.js",
/// "references": [
/// {"1": "group1", "2": 100},
/// ]}
/// ]},
/// -- next identifer
/// ]
/// }
/// ```
///
/// To appear in the output, arguments must be primitive constants i.e. int,
/// double, String, bool, null. Other constants (e.g. enums, const objects) will
/// simply be missing as though they were not constants.
library;
import 'dart:convert' show jsonDecode;
import 'dart:io' show Platform;
// ignore: implementation_imports
import 'package:front_end/src/api_unstable/dart2js.dart' as fe;
import '../js/js.dart' as js;
import '../universe/resource_identifier.dart'
show ResourceIdentifier, ResourceIdentifierLocation;
class _AnnotationMonitor implements js.JavaScriptAnnotationMonitor {
final ResourceInfoCollector _collector;
final String _filename;
_AnnotationMonitor(this._collector, this._filename);
@override
void onAnnotations(List<Object> annotations) {
for (Object annotation in annotations) {
if (annotation is ResourceIdentifier) {
_collector._register(_filename, annotation);
}
}
}
}
class ResourceInfoCollector {
final Map<_ResourceIdentifierKey, _ResourceIdentifierInfo> _identifierMap =
{};
js.JavaScriptAnnotationMonitor monitorFor(String fileName) {
return _AnnotationMonitor(this, fileName);
}
void _register(String filename, ResourceIdentifier identifier) {
final key = _ResourceIdentifierKey(identifier.name, identifier.uri);
final info = _identifierMap[key] ??= _ResourceIdentifierInfo(key);
if (identifier.nonconstant) info.nonconstant = true;
(info._files[filename] ??= []).add(identifier);
}
Object finish(Map<String, String> environment) {
Map<String, Object> json = {
'_comment': r'Resources referenced by annotated resource identifers',
'AppTag': 'TBD',
'environment': environment,
'identifiers': _identifierMap.values.toList()
..sort(_ResourceIdentifierInfo.compare),
};
return json;
}
}
class _ResourceIdentifierKey {
final String name;
final Uri uri;
_ResourceIdentifierKey(this.name, this.uri);
@override
bool operator ==(Object other) =>
other is _ResourceIdentifierKey && name == other.name && uri == other.uri;
@override
late final int hashCode = Object.hash(name, uri);
}
class _ResourceIdentifierInfo {
final _ResourceIdentifierKey _key;
bool nonconstant = false;
final Map<String, List<ResourceIdentifier>> _files = {};
_ResourceIdentifierInfo(this._key);
static int compare(_ResourceIdentifierInfo a, _ResourceIdentifierInfo b) {
int r = a._key.name.compareTo(b._key.name);
if (r != 0) return r;
return a._key.uri.toString().compareTo(b._key.uri.toString());
}
Map<String, dynamic> toJson() {
final files = _files.entries.toList()
..sort((a, b) => a.key.compareTo(b.key));
return {
"name": _key.name,
"uri": _key.uri.toString(),
"nonconstant": nonconstant,
"files": [
for (final entry in files)
{
"filename": entry.key,
"references": [
for (final resourceIdentifier in entry.value)
{
if (resourceIdentifier.location != null)
'@': _locationToJson(resourceIdentifier.location!),
...jsonDecode(resourceIdentifier.arguments),
},
],
},
],
};
}
Map<String, dynamic> _locationToJson(ResourceIdentifierLocation location) {
return {
'uri': fe.relativizeUri(Uri.base, location.uri, Platform.isWindows),
if (location.line != null) 'line': location.line,
if (location.column != null) 'column': location.column,
};
}
}