// Copyright 2019 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:convert'; import 'dart:io'; import 'package:front_end/src/api_unstable/vm.dart' show FileSystem; import 'package:dev_compiler/dev_compiler.dart'; import 'package:kernel/ast.dart'; import 'package:kernel/class_hierarchy.dart'; import 'package:kernel/core_types.dart'; import 'package:path/path.dart' as p; import 'package:package_config/package_config.dart'; 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. /// /// Ths 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.soundNullSafety = false, String? moduleFormat, }) : _moduleFormat = parseModuleFormat(moduleFormat ?? 'amd'); final bool useDebuggerModuleNames; final bool emitDebugMetadata; final bool emitDebugSymbols; final ModuleFormat _moduleFormat; final bool soundNullSafety; final FileSystem? _fileSystem; final Set _loadedLibraries; final Map _uriToComponent = {}; final _importToSummary = Map.identity(); final _summaryToModule = Map.identity(); final Map _moduleImportForSummary = {}; final Map _moduleImportNameForSummary = {}; final String _fileSystemScheme; late Component _lastFullComponent; late Component _currentComponent; late StrongComponents _strongComponents; /// Initialize the incremental bundler from a full component. Future initialize( Component fullComponent, Uri mainUri, PackageConfig packageConfig) async { _lastFullComponent = fullComponent; _currentComponent = fullComponent; _strongComponents = 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 invalidate( Component partialComponent, Component lastFullComponent, Uri mainUri, PackageConfig packageConfig) async { _currentComponent = partialComponent; _updateFullComponent(lastFullComponent, partialComponent); _strongComponents = StrongComponents( _lastFullComponent, _loadedLibraries, mainUri, _fileSystem, ); await _strongComponents.computeModules({ for (Library library in partialComponent.libraries) library.importUri: library, }); var invalidated = { for (Library library in partialComponent.libraries) _strongComponents.moduleAssignment[library.importUri]!, }; _updateSummaries(invalidated, packageConfig); } void _updateFullComponent(Component lastKnownGood, Component candidate) { Map combined = {}; Map uriToSource = {}; 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 = Component( libraries: combined.values.toList(), uriToSource: uriToSource, )..setMainMethodAndMode( candidate.mainMethod?.reference, true, candidate.mode); for (final repo in candidate.metadata.values) { _lastFullComponent.addMetadataRepository(repo); } } /// Update the summaries [moduleKeys]. void _updateSummaries(Iterable moduleKeys, PackageConfig packageConfig) { for (Uri uri in moduleKeys) { final List libraries = _strongComponents.modules[uri]!.toList(); final Component summaryComponent = Component( libraries: libraries, nameRoot: _lastFullComponent.root, uriToSource: _lastFullComponent.uriToSource, ); summaryComponent.setMainMethodAndMode( null, false, _currentComponent.mode); var 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 var moduleImport = _moduleImportNameForSummary[uri]!; var 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> compile( ClassHierarchy classHierarchy, CoreTypes coreTypes, PackageConfig packageConfig, IOSink codeSink, IOSink manifestSink, IOSink sourceMapsSink, IOSink? metadataSink, IOSink? symbolsSink, ) async { var codeOffset = 0; var sourceMapOffset = 0; var metadataOffset = 0; var symbolsOffset = 0; final manifest = >>{}; final Set visited = {}; final Map 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 summaryComponent = _uriToComponent[moduleUri]!; // module name to use in trackLibraries // use full path for tracking if module uri is not a package uri. final moduleUrl = urlForComponentUri(moduleUri, packageConfig); final moduleName = makeModuleName(moduleUrl); var compiler = ProgramCompiler( _currentComponent, classHierarchy, SharedCompilerOptions( sourceMap: true, summarizeApi: false, emitDebugMetadata: emitDebugMetadata, emitDebugSymbols: emitDebugSymbols, moduleName: moduleName, soundNullSafety: soundNullSafety, ), _importToSummary, _summaryToModule, coreTypes: coreTypes, ); final 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 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 codeBytes = utf8.encode(code.code); final sourceMapBytes = utf8.encode(json.encode(code.sourceMap)); final metadataBytes = emitDebugMetadata ? utf8.encode(json.encode(code.metadata)) : null; final 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': [codeOffset, codeOffset += codeBytes.length], 'sourcemap': [ sourceMapOffset, sourceMapOffset += sourceMapBytes.length ], if (emitDebugMetadata) 'metadata': [ metadataOffset, metadataOffset += metadataBytes!.length ], if (emitDebugSymbols) 'symbols': [ 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 resolvedUri = packageConfig.resolve(componentUri)!; final package = packageConfig.packageOf(resolvedUri)!; final root = package.root; final relativeRoot = root.pathSegments .lastWhere((segment) => segment.isNotEmpty, orElse: null); final relativeUrl = resolvedUri.toString().replaceFirst('$root', ''); // Relative component url (used as server path in the browser): // `packages//` return 'packages/$relativeRoot/$relativeUrl'; } }