blob: 111bb99c653f0f5e3762d48e4ef534776692dfea [file] [edit]
// Copyright (c) 2019, 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.
// ignore_for_file: implementation_imports
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:args/args.dart';
import 'package:dev_compiler/dev_compiler.dart';
import 'package:dev_compiler/src/command/command.dart';
import 'package:dev_compiler/src/kernel/hot_reload_delta_inspector.dart';
import 'package:dev_compiler/src/js_ast/nodes.dart';
import 'package:front_end/src/api_unstable/vm.dart' show FileSystem;
import 'package:kernel/ast.dart';
import 'package:kernel/class_hierarchy.dart';
import 'package:kernel/core_types.dart';
import 'package:package_config/package_config.dart';
import 'package:path/path.dart' as p;
import 'strong_components.dart';
/// Produce a special bundle format for compiled JavaScript.
///
/// The bundle format consists of two files: One containing all produced
/// JavaScript library bundles concatenated together, and a second containing
/// the byte offsets by the synthesized library bundle name for each JavaScript
/// library bundle in JSON format. The library bundle name is based off of a
/// library URI from the associated component.
///
/// The format is analogous to the dill and .incremental.dill in that during
/// an incremental build, a different file is written for each which contains
/// only the updated libraries.
class IncrementalJavaScriptBundler {
IncrementalJavaScriptBundler(
this._fileSystem,
this._loadedLibraries,
this._fileSystemScheme, {
this.useDebuggerModuleNames = false,
this.emitDebugMetadata = false,
this.emitDebugSymbols = false,
this.useStronglyConnectedComponents = true,
this.canaryFeatures = false,
String? moduleFormat,
this.extraDdcOptions = const [],
}) : _moduleFormat = parseModuleFormat(moduleFormat ?? 'amd');
final bool useDebuggerModuleNames;
final bool emitDebugMetadata;
final bool emitDebugSymbols;
final bool useStronglyConnectedComponents;
final ModuleFormat _moduleFormat;
final List<String> extraDdcOptions;
final bool canaryFeatures;
final FileSystem? _fileSystem;
final Set<Library> _loadedLibraries;
final Map<Uri, Component> _uriToComponent = <Uri, Component>{};
final _libraryToSummary = new Map<Library, Component>.identity();
final _summaryToLibraryBundleName = new Map<Component, String>.identity();
final Map<Uri, String> _summaryToLibraryBundleJSPath = <Uri, String>{};
final String _fileSystemScheme;
final HotReloadDeltaInspector _deltaInspector = new HotReloadDeltaInspector();
late HotReloadLibraryMetadataRepository _libraryMetadataRepository;
late Component _lastFullComponent;
late Component _currentComponent;
late StrongComponents _strongComponents;
/// Initialize the incremental bundler from a full component.
Future<void> initialize(
Component fullComponent,
Uri mainUri,
PackageConfig packageConfig,
) async {
_lastFullComponent = fullComponent;
_currentComponent = fullComponent;
// Initialize fresh hot reload metadata for this compile and throw out all
// information collected from any previous series of hot reloads compiles.
_libraryMetadataRepository = new HotReloadLibraryMetadataRepository();
Iterable<Uri> initialLibraryUris;
if (useStronglyConnectedComponents) {
_strongComponents = new StrongComponents(
fullComponent,
_loadedLibraries,
mainUri,
_fileSystem,
);
await _strongComponents.computeLibraryBundles();
initialLibraryUris =
_strongComponents.libraryBundleImportToLibraries.keys;
} else {
initialLibraryUris = fullComponent.libraries.map(
(library) => library.importUri,
);
}
_updateSummaries(initialLibraryUris, packageConfig);
}
/// Update the incremental bundler from a partial component and the last full
/// component.
Future<void> invalidate(
Component partialComponent,
Component lastFullComponent,
Uri mainUri,
PackageConfig packageConfig, {
required bool recompileRestart,
}) async {
if (canaryFeatures &&
_moduleFormat == ModuleFormat.ddc &&
!recompileRestart) {
// Attach the global metadata to the last full component. The delta
// inspector will add to it while comparing the two components.
lastFullComponent.addMetadataRepository(_libraryMetadataRepository);
// Find any potential hot reload rejections before updating the strongly
// connected component graph.
final List<String> errors = _deltaInspector.compareGenerations(
lastFullComponent,
partialComponent,
);
if (errors.isNotEmpty) {
throw new Exception(
errors.join('/n') +
'\nHot reload rejected due to unsupported changes. '
'Try performing a hot restart instead.',
);
}
}
_currentComponent = partialComponent;
_updateFullComponent(lastFullComponent, partialComponent);
Iterable<Uri> invalidatedLibraryUris;
if (useStronglyConnectedComponents) {
_strongComponents = new StrongComponents(
_lastFullComponent,
_loadedLibraries,
mainUri,
_fileSystem,
);
await _strongComponents.computeLibraryBundles(<Uri, Library>{
for (Library library in partialComponent.libraries)
library.importUri: library,
});
invalidatedLibraryUris = <Uri>{
for (Library library in partialComponent.libraries)
_strongComponents.libraryImportToLibraryBundleImport[library
.importUri]!,
};
} else {
invalidatedLibraryUris = partialComponent.libraries.map(
(library) => library.importUri,
);
}
_updateSummaries(invalidatedLibraryUris, packageConfig);
}
void _updateFullComponent(Component lastKnownGood, Component candidate) {
Map<Uri, Library> combined = <Uri, Library>{};
Map<Uri, Source> uriToSource = <Uri, Source>{};
for (Library library in lastKnownGood.libraries) {
combined[library.importUri] = library;
}
for (Library library in candidate.libraries) {
combined[library.importUri] = library;
}
uriToSource.addAll(lastKnownGood.uriToSource);
uriToSource.addAll(candidate.uriToSource);
_lastFullComponent = new Component(
libraries: combined.values.toList(),
uriToSource: uriToSource,
)..setMainMethodAndMode(candidate.mainMethod?.reference, true);
for (final MetadataRepository repo in candidate.metadata.values) {
_lastFullComponent.addMetadataRepository(repo);
}
}
/// Update the summaries using the [libraryBundleImports].
void _updateSummaries(
Iterable<Uri> libraryBundleImports,
PackageConfig packageConfig,
) {
final Map<Uri, Library> libraryUriToLibrary = {
for (Library library in _lastFullComponent.libraries)
library.importUri: library,
};
for (Uri uri in libraryBundleImports) {
final List<Library> libraries = useStronglyConnectedComponents
? _strongComponents.libraryBundleImportToLibraries[uri]!.toList()
: [libraryUriToLibrary[uri]!];
final Component summaryComponent = new Component(
libraries: libraries,
nameRoot: _lastFullComponent.root,
uriToSource: _lastFullComponent.uriToSource,
);
summaryComponent.setMainMethodAndMode(null, false);
String baseName = urlForComponentUri(uri, packageConfig);
_summaryToLibraryBundleJSPath[uri] = '$baseName.lib.js';
// Library bundle loaders loads bundles by bundle names, not paths
String libraryBundleName = makeLibraryBundleName(baseName);
_uriToComponent[uri] = summaryComponent;
List<Component> oldSummaries = [];
for (Component summary in _summaryToLibraryBundleName.keys) {
if (_summaryToLibraryBundleName[summary] == libraryBundleName) {
oldSummaries.add(summary);
}
}
for (Component summary in oldSummaries) {
_summaryToLibraryBundleName.remove(summary);
}
_libraryToSummary.removeWhere(
(key, value) => oldSummaries.contains(value),
);
for (Library library in summaryComponent.libraries) {
assert(!_libraryToSummary.containsKey(library));
_libraryToSummary[library] = summaryComponent;
_summaryToLibraryBundleName[summaryComponent] = libraryBundleName;
}
}
}
/// Reports if [option] in [extraDdcOptions] was overridden or extended
/// to [newValue] before being passed to DDC.
void _checkOverriddenExtraDdcOption(
ArgResults extraDdcOptionsResults,
String option,
dynamic newValue,
) {
if (extraDdcOptionsResults.wasParsed(option)) {
if (newValue is List) {
if (listEquals(extraDdcOptionsResults[option], newValue)) return;
} else if (newValue == extraDdcOptionsResults[option]) {
return;
}
print("Warning: DDC option '$option' was overridden to '$newValue'.");
}
}
/// Compile each component into a single JavaScript library bundle.
Future<Map<String, Compiler>> compile(
ClassHierarchy classHierarchy,
CoreTypes coreTypes,
PackageConfig packageConfig,
IOSink codeSink,
IOSink manifestSink,
IOSink sourceMapsSink,
IOSink? metadataSink,
IOSink? symbolsSink,
) async {
int codeOffset = 0;
int sourceMapOffset = 0;
int metadataOffset = 0;
int symbolsOffset = 0;
final Map<String, Map<String, List<int>>> manifest = {};
final Map<Uri, Compiler> visited = {};
final Map<String, Compiler> kernel2JsCompilers = {};
for (Library library in _currentComponent.libraries) {
if (_loadedLibraries.contains(library) ||
library.importUri.isScheme('dart')) {
continue;
}
final Uri libraryOrLibraryBundleImportUri = useStronglyConnectedComponents
? _strongComponents.libraryImportToLibraryBundleImport[library
.importUri]!
: library.importUri;
if (visited.containsKey(libraryOrLibraryBundleImportUri)) {
kernel2JsCompilers[library.importUri.toString()] =
visited[libraryOrLibraryBundleImportUri]!;
continue;
}
final Component summaryComponent =
_uriToComponent[libraryOrLibraryBundleImportUri]!;
final String componentUrl = urlForComponentUri(
libraryOrLibraryBundleImportUri,
packageConfig,
);
// Library bundle name to use in trackLibraries. Use the full path for
// tracking if library bundle uri is not a package uri.
final String libraryBundleName = makeLibraryBundleName(componentUrl);
// Issue a warning when provided [extraDdcOptions] were overridden.
final ArgResults extraDdcOptionsResults = Options.nonSdkArgParser().parse(
extraDdcOptions,
);
_checkOverriddenExtraDdcOption(
extraDdcOptionsResults,
'source-map',
true,
);
_checkOverriddenExtraDdcOption(
extraDdcOptionsResults,
'summarize',
false,
);
_checkOverriddenExtraDdcOption(
extraDdcOptionsResults,
'experimental-emit-debug-metadata',
emitDebugMetadata,
);
_checkOverriddenExtraDdcOption(
extraDdcOptionsResults,
'emit-debug-symbols',
emitDebugSymbols,
);
_checkOverriddenExtraDdcOption(
extraDdcOptionsResults,
'canary',
canaryFeatures,
);
_checkOverriddenExtraDdcOption(
extraDdcOptionsResults,
'module-name',
libraryBundleName,
);
_checkOverriddenExtraDdcOption(extraDdcOptionsResults, 'modules', [
libraryBundleName,
]);
// Apply existing Frontend Server flags over options selected in
// [extraDdcOptions].
final List<String> ddcArgs = [
...extraDdcOptions,
'--source-map',
'--no-summarize',
emitDebugMetadata
? '--experimental-emit-debug-metadata'
: '--no-experimental-emit-debug-metadata',
emitDebugSymbols ? '--emit-debug-symbols' : '--no-emit-debug-symbols',
canaryFeatures ? '--canary' : '--no-canary',
'--module-name=$libraryBundleName',
'--modules=${_moduleFormat.flagName}',
];
final Options ddcOptions = new Options.fromArguments(
Options.nonSdkArgParser().parse(ddcArgs),
);
Compiler compiler;
if (ddcOptions.emitLibraryBundle) {
compiler = new LibraryBundleCompiler(
_currentComponent,
classHierarchy,
ddcOptions,
_libraryToSummary,
_summaryToLibraryBundleName,
coreTypes: coreTypes,
);
// Attach all the hot reload metadata collected so far to the component
// that is about to be compiled.
summaryComponent.addMetadataRepository(_libraryMetadataRepository);
} else {
compiler = new ProgramCompiler(
_currentComponent,
classHierarchy,
ddcOptions,
_libraryToSummary,
_summaryToLibraryBundleName,
coreTypes: coreTypes,
);
}
final Program jsBundle = compiler.emitModule(summaryComponent);
// Save program compiler to reuse for expression evaluation.
kernel2JsCompilers[library.importUri.toString()] = compiler;
visited[libraryOrLibraryBundleImportUri] = compiler;
String? sourceMapBase;
if (libraryOrLibraryBundleImportUri.isScheme('package')) {
// Source locations come through as absolute file uris. In order to
// make relative paths in the source map we get the absolute uri for
// the library bundle and make them relative to that.
sourceMapBase = p.dirname(
(packageConfig.resolve(libraryOrLibraryBundleImportUri))!.path,
);
}
final JSCode code = jsProgramToCode(
jsBundle,
ddcOptions.emitLibraryBundle
? ModuleFormat.ddcLibraryBundle
: _moduleFormat,
inlineSourceMap: true,
buildSourceMap: true,
emitDebugMetadata: emitDebugMetadata,
emitDebugSymbols: emitDebugSymbols,
jsUrl: '$componentUrl.lib.js',
mapUrl: '$componentUrl.lib.js.map',
sourceMapBase: sourceMapBase,
customScheme: _fileSystemScheme,
compiler: compiler,
component: summaryComponent,
// Only pass a package configuration if the serve time package paths are
// not being enforced by the `useDebuggerModuleNames` option.
packageConfig: useDebuggerModuleNames ? null : packageConfig,
);
final Uint8List codeBytes = utf8.encode(code.code);
final Uint8List sourceMapBytes = utf8.encode(json.encode(code.sourceMap));
final Uint8List? metadataBytes = emitDebugMetadata
? utf8.encode(json.encode(code.metadata))
: null;
final Uint8List? symbolsBytes = emitDebugSymbols
? utf8.encode(json.encode(code.symbols))
: null;
codeSink.add(codeBytes);
sourceMapsSink.add(sourceMapBytes);
if (emitDebugMetadata) {
metadataSink!.add(metadataBytes!);
}
if (emitDebugSymbols) {
symbolsSink!.add(symbolsBytes!);
}
final String libraryBundleJSPath =
_summaryToLibraryBundleJSPath[libraryOrLibraryBundleImportUri]!;
manifest[libraryBundleJSPath] = {
'code': <int>[codeOffset, codeOffset += codeBytes.length],
'sourcemap': <int>[
sourceMapOffset,
sourceMapOffset += sourceMapBytes.length,
],
if (emitDebugMetadata)
'metadata': <int>[
metadataOffset,
metadataOffset += metadataBytes!.length,
],
if (emitDebugSymbols)
'symbols': <int>[
symbolsOffset,
symbolsOffset += symbolsBytes!.length,
],
};
}
manifestSink.add(utf8.encode(json.encode(manifest)));
return kernel2JsCompilers;
}
/// Library bundle name used in the browser to load library bundles.
///
/// Library bundle names are used to load library bundles using library bundle
/// path maps in RequireJS, which treats names with leading '/' or '.js'
/// extensions specially, and tries to load them without mapping. Skip the
/// leading '/' to always load library bundles via library bundle path maps.
String makeLibraryBundleName(String name) {
return name.startsWith('/') ? name.substring(1) : name;
}
/// Create component url.
///
/// Used as a server path in the browser for the library bundle created from
/// the component.
String urlForComponentUri(Uri componentUri, PackageConfig packageConfig) {
if (!componentUri.isScheme('package')) {
return componentUri.path;
}
if (!useDebuggerModuleNames) {
return '/packages/${componentUri.path}';
}
// Match relative directory structure of server paths to the
// actual directory structure, so the sourcemaps relative paths
// can be resolved by the browser.
final Uri resolvedUri = packageConfig.resolve(componentUri)!;
final Package package = packageConfig.packageOf(resolvedUri)!;
final Uri root = package.root;
final String relativeRoot = root.pathSegments.lastWhere(
(segment) => segment.isNotEmpty,
);
final String relativeUrl = resolvedUri.toString().replaceFirst('$root', '');
// Relative component url (used as server path in the browser):
// `packages/<package directory>/<path to file.dart>`
return 'packages/$relativeRoot/$relativeUrl';
}
}