| // 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 { | |
| new( | |
| this._fileSystem, | |
| this._loadedLibraries, | |
| this._fileSystemScheme, { | |
| Map<Library, Component> loadedLibraryToSummary = const {}, | |
| Map<Component, String> loadedSummaryToLibraryBundleName = const {}, | |
| this.useDebuggerModuleNames = false, | |
| this.emitDebugMetadata = false, | |
| this.emitDebugSymbols = false, | |
| this.useStronglyConnectedComponents = true, | |
| this.canaryFeatures = false, | |
| String? moduleFormat, | |
| this.extraDdcOptions = const [], | |
| }) : _moduleFormat = parseModuleFormat(moduleFormat ?? 'amd') { | |
| // Libraries already loaded from --import-dill | |
| _libraryToSummary.addAll(loadedLibraryToSummary); | |
| _summaryToLibraryBundleName.addAll(loadedSummaryToLibraryBundleName); | |
| } | |
| 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 | |
| .where((lib) => shouldCompileToJavaScript(lib, _loadedLibraries)) | |
| .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 = <Uri>[ | |
| for (Library library in partialComponent.libraries) | |
| if (shouldCompileToJavaScript(library, _loadedLibraries)) | |
| 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 (!shouldCompileToJavaScript(library, _loadedLibraries)) { | |
| continue; | |
| } | |
| final Uri? libraryOrLibraryBundleImportUri = | |
| useStronglyConnectedComponents | |
| ? _strongComponents.libraryImportToLibraryBundleImport[library | |
| .importUri] | |
| : library.importUri; | |
| if (libraryOrLibraryBundleImportUri == null) { | |
| continue; | |
| } | |
| 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( | |
| code.encodedSourceMap ?? 'null', | |
| ); | |
| 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'; | |
| } | |
| } |