blob: f72be19010a367a601b2e2c8172844f52c05bc32 [file] [log] [blame]
// 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:dev_compiler/dev_compiler.dart';
import 'package:dev_compiler/src/js_ast/nodes.dart';
import 'package:dev_compiler/src/kernel/command.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 modules concatenated together, and a second containing the byte
/// offsets by module name for each JavaScript module in JSON format.
///
/// 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.canaryFeatures = false,
String? moduleFormat,
}) : _moduleFormat = parseModuleFormat(moduleFormat ?? 'amd');
final bool useDebuggerModuleNames;
final bool emitDebugMetadata;
final bool emitDebugSymbols;
final ModuleFormat _moduleFormat;
final bool canaryFeatures;
final FileSystem? _fileSystem;
final Set<Library> _loadedLibraries;
final Map<Uri, Component> _uriToComponent = <Uri, Component>{};
final _importToSummary = new Map<Library, Component>.identity();
final _summaryToModule = new Map<Component, String>.identity();
final Map<Uri, String> _moduleImportForSummary = <Uri, String>{};
final Map<Uri, String> _moduleImportNameForSummary = <Uri, String>{};
final String _fileSystemScheme;
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;
_strongComponents = new StrongComponents(
fullComponent,
_loadedLibraries,
mainUri,
_fileSystem,
);
await _strongComponents.computeModules();
_updateSummaries(_strongComponents.modules.keys, 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) async {
_currentComponent = partialComponent;
_updateFullComponent(lastFullComponent, partialComponent);
_strongComponents = new StrongComponents(
_lastFullComponent,
_loadedLibraries,
mainUri,
_fileSystem,
);
await _strongComponents.computeModules(<Uri, Library>{
for (Library library in partialComponent.libraries)
library.importUri: library,
});
Set<Uri> invalidated = <Uri>{
for (Library library in partialComponent.libraries)
_strongComponents.moduleAssignment[library.importUri]!,
};
_updateSummaries(invalidated, 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, candidate.mode);
for (final MetadataRepository repo in candidate.metadata.values) {
_lastFullComponent.addMetadataRepository(repo);
}
}
/// Update the summaries [moduleKeys].
void _updateSummaries(Iterable<Uri> moduleKeys, PackageConfig packageConfig) {
for (Uri uri in moduleKeys) {
final List<Library> libraries = _strongComponents.modules[uri]!.toList();
final Component summaryComponent = new Component(
libraries: libraries,
nameRoot: _lastFullComponent.root,
uriToSource: _lastFullComponent.uriToSource,
);
summaryComponent.setMainMethodAndMode(
null, false, _currentComponent.mode);
String baseName = urlForComponentUri(uri, packageConfig);
_moduleImportForSummary[uri] = '$baseName.lib.js';
_moduleImportNameForSummary[uri] = makeModuleName(baseName);
_uriToComponent[uri] = summaryComponent;
// module loaders loads modules by modules names, not paths
String moduleImport = _moduleImportNameForSummary[uri]!;
List<Component> oldSummaries = [];
for (Component summary in _summaryToModule.keys) {
if (_summaryToModule[summary] == moduleImport) {
oldSummaries.add(summary);
}
}
for (Component summary in oldSummaries) {
_summaryToModule.remove(summary);
}
_importToSummary
.removeWhere((key, value) => oldSummaries.contains(value));
for (Library library in summaryComponent.libraries) {
assert(!_importToSummary.containsKey(library));
_importToSummary[library] = summaryComponent;
_summaryToModule[summaryComponent] = moduleImport;
}
}
}
/// Compile each component into a single JavaScript module.
Future<Map<String, ProgramCompiler>> 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 Set<Uri> visited = {};
final Map<String, ProgramCompiler> kernel2JsCompilers = {};
for (Library library in _currentComponent.libraries) {
if (_loadedLibraries.contains(library) ||
library.importUri.isScheme('dart')) {
continue;
}
final Uri moduleUri =
_strongComponents.moduleAssignment[library.importUri]!;
if (visited.contains(moduleUri)) {
continue;
}
visited.add(moduleUri);
final Component summaryComponent = _uriToComponent[moduleUri]!;
// module name to use in trackLibraries
// use full path for tracking if module uri is not a package uri.
final String moduleUrl = urlForComponentUri(moduleUri, packageConfig);
final String moduleName = makeModuleName(moduleUrl);
ProgramCompiler compiler = new ProgramCompiler(
_currentComponent,
classHierarchy,
new SharedCompilerOptions(
sourceMap: true,
summarizeApi: false,
emitDebugMetadata: emitDebugMetadata,
emitDebugSymbols: emitDebugSymbols,
moduleName: moduleName,
soundNullSafety: true,
canaryFeatures: canaryFeatures,
),
_importToSummary,
_summaryToModule,
coreTypes: coreTypes,
);
final Program jsModule = compiler.emitModule(summaryComponent);
// Save program compiler to reuse for expression evaluation.
kernel2JsCompilers[moduleName] = compiler;
String? sourceMapBase;
if (moduleUri.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 module and make them relative to that.
sourceMapBase = p.dirname((packageConfig.resolve(moduleUri))!.path);
}
final JSCode code = jsProgramToCode(
jsModule,
_moduleFormat,
inlineSourceMap: true,
buildSourceMap: true,
emitDebugMetadata: emitDebugMetadata,
emitDebugSymbols: emitDebugSymbols,
jsUrl: '$moduleUrl.lib.js',
mapUrl: '$moduleUrl.lib.js.map',
sourceMapBase: sourceMapBase,
customScheme: _fileSystemScheme,
compiler: compiler,
component: summaryComponent,
);
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 moduleKey = _moduleImportForSummary[moduleUri]!;
manifest[moduleKey] = {
'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;
}
/// Module name used in the browser to load modules.
///
/// Module names are used to load modules using module
/// paths 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 modules via module
/// path maps.
String makeModuleName(String name) {
return name.startsWith('/') ? name.substring(1) : name;
}
/// Create component url.
///
/// Used as a server path in the browser for the module 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';
}
}