// 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. import 'dart:convert'; import 'dart:io'; import 'package:dev_compiler/dev_compiler.dart' show Compiler; import 'package:frontend_server/src/javascript_bundle.dart'; 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:test/test.dart'; /// Additional indexed types required by the dev_compiler's NativeTypeSet. final Map>> additionalRequiredClasses = { 'dart:core': {'Comparable': []}, 'dart:async': { 'StreamIterator': ['get:current', 'moveNext', 'cancel', ''], '_SyncStarIterator': ['_current', '_datum', '_yieldStar'], '_IterationMarker': ['yieldSingle', 'yieldStar'], }, 'dart:_interceptors': { 'JSBool': [], 'JSNumber': [], 'JSArray': [], 'JSString': [], 'LegacyJavaScriptObject': [], }, 'dart:_native_typed_data': {}, 'dart:collection': { 'ListBase': [], 'MapBase': [], 'LinkedHashSet': [], '_HashSet': [], '_IdentityHashSet': [], }, 'dart:math': {'Rectangle': []}, 'dart:html': {}, 'dart:_rti': {'Rti': [], '_Universe': []}, 'dart:indexed_db': {}, 'dart:svg': {}, 'dart:web_audio': {}, 'dart:web_gl': {}, 'dart:_js_helper': { 'PrivateSymbol': [], 'LinkedMap': [], 'IdentityMap': [], 'LinkedSet': [], 'IdentitySet': [], 'SyncIterable': [], }, }; /// Additional indexed top level methods required by the dev_compiler. final Map> requiredTopLevels = { 'dart:_runtime': ['assertInterop'], 'dart:async': [ '_asyncAwait', '_asyncReturn', '_asyncRethrow', '_asyncStarHelper', '_asyncStartSync', '_makeAsyncAwaitCompleter', '_makeSyncStarIterable', '_makeAsyncStarStreamController', '_makeSyncStarIterable', '_streamOfController', '_wrapJsFunctionForAsync', ], }; void main() { Map> allRequiredTypes = _combineMaps( CoreTypes.requiredClasses, additionalRequiredClasses .map((k, v) => new MapEntry(k, v.keys.toList()))); List allRequiredLibraries = [ ...allRequiredTypes.keys, ...requiredTopLevels.keys, ]; List testCoreLibraries = []; for (String requiredLibrary in allRequiredLibraries) { Library library = new Library(Uri.parse(requiredLibrary), fileUri: Uri.parse(requiredLibrary)); for (String requiredClass in allRequiredTypes[requiredLibrary] ?? []) { library.addClass(new Class( name: requiredClass, fileUri: Uri.parse(requiredLibrary), procedures: [ for (String requiredMember in (additionalRequiredClasses[requiredLibrary] ?[requiredClass] ?? [])) new Procedure(new Name(requiredMember, library), ProcedureKind.Method, new FunctionNode(new EmptyStatement()), fileUri: Uri.parse(requiredLibrary)) ])); } for (String requiredMember in requiredTopLevels[requiredLibrary] ?? []) { library.addProcedure(new Procedure(new Name(requiredMember, library), ProcedureKind.Method, new FunctionNode(new EmptyStatement()), fileUri: Uri.parse(requiredLibrary))); } testCoreLibraries.add(library); } final PackageConfig packageConfig = PackageConfig.parseJson({ 'configVersion': 2, 'packages': [ { 'name': 'a', 'rootUri': 'file:///pkg/a', 'packageUri': 'lib/', } ], }, Uri.base); final String multiRootScheme = 'org-dartlang-app'; for (final bool debuggerNames in [true, false]) { group('Debugger module names: $debuggerNames |', () { test('Creates module uris for file paths', () async { final Uri fileUri = new Uri.file('/c.dart'); final IncrementalJavaScriptBundler javaScriptBundler = new IncrementalJavaScriptBundler( null, {}, multiRootScheme, useDebuggerModuleNames: debuggerNames, ); final String moduleUrl = javaScriptBundler.urlForComponentUri(fileUri, packageConfig); final String moduleName = javaScriptBundler.makeModuleName(moduleUrl); expect(moduleUrl, '/c.dart'); expect(moduleName, 'c.dart'); }); test('Creates module uris for package paths', () async { final Uri packageUri = Uri.parse('package:a/a.dart'); final IncrementalJavaScriptBundler javaScriptBundler = new IncrementalJavaScriptBundler( null, {}, multiRootScheme, useDebuggerModuleNames: debuggerNames, ); final String moduleUrl = javaScriptBundler.urlForComponentUri(packageUri, packageConfig); final String moduleName = javaScriptBundler.makeModuleName(moduleUrl); expect(moduleUrl, debuggerNames ? 'packages/a/lib/a.dart' : '/packages/a/a.dart'); expect(moduleName, debuggerNames ? 'packages/a/lib/a.dart' : 'packages/a/a.dart'); }); test('compiles JavaScript code', () async { final Uri uri = new Uri.file('/c.dart'); final Library library = new Library( uri, fileUri: uri, procedures: [ new Procedure(new Name('ArbitrarilyChosen'), ProcedureKind.Method, new FunctionNode(new Block([])), fileUri: uri) ], ); final Component testComponent = new Component(libraries: [library, ...testCoreLibraries]); final IncrementalJavaScriptBundler javaScriptBundler = new IncrementalJavaScriptBundler( null, {}, multiRootScheme, useDebuggerModuleNames: debuggerNames, ); await javaScriptBundler.initialize(testComponent, uri, packageConfig); final _MemorySink manifestSink = new _MemorySink(); final _MemorySink codeSink = new _MemorySink(); final _MemorySink sourcemapSink = new _MemorySink(); final _MemorySink metadataSink = new _MemorySink(); final _MemorySink symbolsSink = new _MemorySink(); final CoreTypes coreTypes = new CoreTypes(testComponent); final Map compilers = await javaScriptBundler.compile( new ClassHierarchy(testComponent, coreTypes), coreTypes, packageConfig, codeSink, manifestSink, sourcemapSink, metadataSink, symbolsSink, ); final Map manifest = json.decode(utf8.decode(manifestSink.buffer)); final String code = utf8.decode(codeSink.buffer); expect(manifest, { '/c.dart.lib.js': { 'code': [0, codeSink.buffer.length], 'sourcemap': [0, sourcemapSink.buffer.length], }, }); expect(code, contains('ArbitrarilyChosen')); // Verify source map url is correct. expect(code, contains('sourceMappingURL=c.dart.lib.js.map')); // Verify program compilers are created. final String moduleUrl = javaScriptBundler.urlForComponentUri( library.importUri, packageConfig); final String moduleName = javaScriptBundler.makeModuleName(moduleUrl); expect(compilers.keys, equals([moduleName])); }); test('converts package: uris into /packages/ uris', () async { Uri importUri = Uri.parse('package:a/a.dart'); Uri fileUri = packageConfig.resolve(importUri)!; final Library library = new Library( importUri, fileUri: fileUri, procedures: [ new Procedure(new Name('ArbitrarilyChosen'), ProcedureKind.Method, new FunctionNode(new Block([])), fileUri: fileUri) ], ); final Component testComponent = new Component(libraries: [library, ...testCoreLibraries]); final IncrementalJavaScriptBundler javaScriptBundler = new IncrementalJavaScriptBundler( null, {}, multiRootScheme, useDebuggerModuleNames: debuggerNames, ); await javaScriptBundler.initialize( testComponent, importUri, packageConfig); final _MemorySink manifestSink = new _MemorySink(); final _MemorySink codeSink = new _MemorySink(); final _MemorySink sourcemapSink = new _MemorySink(); final _MemorySink metadataSink = new _MemorySink(); final _MemorySink symbolsSink = new _MemorySink(); final CoreTypes coreTypes = new CoreTypes(testComponent); await javaScriptBundler.compile( new ClassHierarchy(testComponent, coreTypes), coreTypes, packageConfig, codeSink, manifestSink, sourcemapSink, metadataSink, symbolsSink, ); final Map manifest = json.decode(utf8.decode(manifestSink.buffer)); final String code = utf8.decode(codeSink.buffer); final String moduleUrl = javaScriptBundler.urlForComponentUri( library.importUri, packageConfig); expect(manifest, { '$moduleUrl.lib.js': { 'code': [0, codeSink.buffer.length], 'sourcemap': [0, sourcemapSink.buffer.length], }, }); expect(code, contains('ArbitrarilyChosen')); // Verify source map url is correct. expect(code, contains('sourceMappingURL=a.dart.lib.js.map')); }); test('multi-root uris create modules relative to the root', () async { Uri importUri = Uri.parse('$multiRootScheme:/web/main.dart'); Uri fileUri = importUri; final Library library = new Library( importUri, fileUri: fileUri, procedures: [ new Procedure(new Name('ArbitrarilyChosen'), ProcedureKind.Method, new FunctionNode(new Block([])), fileUri: fileUri) ], ); final Component testComponent = new Component(libraries: [library, ...testCoreLibraries]); final IncrementalJavaScriptBundler javaScriptBundler = new IncrementalJavaScriptBundler( null, {}, multiRootScheme, useDebuggerModuleNames: debuggerNames, ); await javaScriptBundler.initialize( testComponent, importUri, packageConfig); final _MemorySink manifestSink = new _MemorySink(); final _MemorySink codeSink = new _MemorySink(); final _MemorySink sourcemapSink = new _MemorySink(); final _MemorySink metadataSink = new _MemorySink(); final _MemorySink symbolsSink = new _MemorySink(); final CoreTypes coreTypes = new CoreTypes(testComponent); await javaScriptBundler.compile( new ClassHierarchy(testComponent, coreTypes), coreTypes, packageConfig, codeSink, manifestSink, sourcemapSink, metadataSink, symbolsSink, ); final Map manifest = json.decode(utf8.decode(manifestSink.buffer)); final String code = utf8.decode(codeSink.buffer); expect(manifest, { '${importUri.path}.lib.js': { 'code': [0, codeSink.buffer.length], 'sourcemap': [0, sourcemapSink.buffer.length], }, }); expect(code, contains('ArbitrarilyChosen')); // Verify source map url is correct. expect(code, contains('sourceMappingURL=main.dart.lib.js.map')); }); }); } test('can combine strongly connected components', () async { // Create three libraries A, B, C where A is the entrypoint and B & C // circularly import each other. final Library libraryC = new Library(new Uri.file('/c.dart'), fileUri: new Uri.file('/c.dart')); final Library libraryB = new Library(new Uri.file('/b.dart'), fileUri: new Uri.file('/b.dart')); libraryC.dependencies.add(new LibraryDependency.import(libraryB)); libraryB.dependencies.add(new LibraryDependency.import(libraryC)); final Uri uriA = new Uri.file('/a.dart'); final Library libraryA = new Library( uriA, fileUri: uriA, dependencies: [ new LibraryDependency.import(libraryB), ], procedures: [ new Procedure(new Name('ArbitrarilyChosen'), ProcedureKind.Method, new FunctionNode(new Block([])), fileUri: uriA) ], ); final Component testComponent = new Component( libraries: [libraryA, libraryB, libraryC, ...testCoreLibraries]); final IncrementalJavaScriptBundler javaScriptBundler = new IncrementalJavaScriptBundler( null, {}, multiRootScheme, ); await javaScriptBundler.initialize(testComponent, uriA, packageConfig); final _MemorySink manifestSink = new _MemorySink(); final _MemorySink codeSink = new _MemorySink(); final _MemorySink sourcemapSink = new _MemorySink(); final _MemorySink metadataSink = new _MemorySink(); final _MemorySink symbolsSink = new _MemorySink(); final CoreTypes coreTypes = new CoreTypes(testComponent); await javaScriptBundler.compile( new ClassHierarchy(testComponent, coreTypes), coreTypes, packageConfig, codeSink, manifestSink, sourcemapSink, metadataSink, symbolsSink, ); final String code = utf8.decode(codeSink.buffer); final Map manifest = json.decode(utf8.decode(manifestSink.buffer)); // There should only be two modules since C and B should be combined. final String moduleHeader = r"define(['dart_sdk'], (function load__"; expect(moduleHeader.allMatches(code), hasLength(2)); // Expected module headers. final String aModuleHeader = r"define(['dart_sdk'], (function load__a_dart(dart_sdk) {"; expect(code, contains(aModuleHeader)); final String cModuleHeader = r"define(['dart_sdk'], (function load__c_dart(dart_sdk) {"; expect(code, contains(cModuleHeader)); // Verify source map url is correct. expect(code, contains('sourceMappingURL=a.dart.lib.js.map')); final List offsets = manifest['/a.dart.lib.js']['sourcemap']; final Map sourcemapModuleA = json.decode( utf8.decode(sourcemapSink.buffer.sublist(offsets.first, offsets.last))); // Verify source maps are pointing at correct source files. expect(sourcemapModuleA['file'], 'a.dart.lib.js'); }); test('can invalidate changes to strongly connected components', () async { final Uri uriA = new Uri.file('/a.dart'); final Uri uriB = new Uri.file('/b.dart'); final Uri uriC = new Uri.file('/c.dart'); // Create three libraries A, B, C where A is the entrypoint and B & C // circularly import each other. final Library libraryC = new Library(uriC, fileUri: uriC); final Library libraryB = new Library(uriB, fileUri: uriB); libraryC.dependencies.add(new LibraryDependency.import(libraryB)); libraryB.dependencies.add(new LibraryDependency.import(libraryC)); final Library libraryA = new Library( uriA, fileUri: uriA, dependencies: [ new LibraryDependency.import(libraryB), ], procedures: [ new Procedure(new Name('ArbitrarilyChosen'), ProcedureKind.Method, new FunctionNode(new Block([])), fileUri: uriA) ], ); final Component testComponent = new Component( libraries: [libraryA, libraryB, libraryC, ...testCoreLibraries]); final IncrementalJavaScriptBundler javaScriptBundler = new IncrementalJavaScriptBundler( null, {}, multiRootScheme, ); await javaScriptBundler.initialize(testComponent, uriA, packageConfig); // Now change A and B so that they no longer import each other. final Library libraryC2 = new Library(uriC, fileUri: uriC); final Library libraryB2 = new Library(uriB, fileUri: uriB); libraryB2.dependencies.add(new LibraryDependency.import(libraryC2)); final Library libraryA2 = new Library( uriA, fileUri: uriA, dependencies: [ new LibraryDependency.import(libraryB2), ], procedures: [ new Procedure(new Name('ArbitrarilyChosen'), ProcedureKind.Method, new FunctionNode(new Block([])), fileUri: uriA) ], ); final Component partialComponent = new Component(libraries: [libraryA2, libraryB2, libraryC2]); await javaScriptBundler.invalidate( partialComponent, testComponent, uriA, packageConfig); final _MemorySink manifestSink = new _MemorySink(); final _MemorySink codeSink = new _MemorySink(); final _MemorySink sourcemapSink = new _MemorySink(); final _MemorySink metadataSink = new _MemorySink(); final _MemorySink symbolsSink = new _MemorySink(); final CoreTypes coreTypes = new CoreTypes(testComponent); await javaScriptBundler.compile( new ClassHierarchy(testComponent, coreTypes), coreTypes, packageConfig, codeSink, manifestSink, sourcemapSink, metadataSink, symbolsSink, ); final String code = utf8.decode(codeSink.buffer); final Map manifest = json.decode(utf8.decode(manifestSink.buffer)); // There should be 3 modules since A, B, C are now compiled separately final String moduleHeader = r"define(['dart_sdk'], (function load__"; expect(moduleHeader.allMatches(code), hasLength(3)); // Expected module headers. final String aModuleHeader = r"define(['dart_sdk'], (function load__a_dart(dart_sdk) {"; expect(code, contains(aModuleHeader)); final String bModuleHeader = r"define(['dart_sdk'], (function load__b_dart(dart_sdk) {"; expect(code, contains(bModuleHeader)); final String cModuleHeader = r"define(['dart_sdk'], (function load__c_dart(dart_sdk) {"; expect(code, contains(cModuleHeader)); // Verify source map url is correct. expect(code, contains('sourceMappingURL=a.dart.lib.js.map')); final List offsets = manifest['/a.dart.lib.js']['sourcemap']; final Map sourcemapModuleA = json.decode( utf8.decode(sourcemapSink.buffer.sublist(offsets.first, offsets.last))); // Verify source maps are pointing at correct source files. expect(sourcemapModuleA['file'], 'a.dart.lib.js'); }); test('can compile using the advanced invalidation', () async { final Uri uriC = new Uri.file('/c.dart'); // Given 3 libraries A -> B -> C final Library libraryC = new Library( uriC, fileUri: uriC, procedures: [ new Procedure(new Name('CheckForContents'), ProcedureKind.Method, new FunctionNode(new Block([])), fileUri: uriC) ], ); final Uri uriB = new Uri.file('/b.dart'); final Library libraryB = new Library(uriB, fileUri: uriB); libraryB.dependencies.add(new LibraryDependency.import(libraryC)); final Uri uriA = new Uri.file('/a.dart'); final Library libraryA = new Library( uriA, fileUri: uriA, dependencies: [ new LibraryDependency.import(libraryB), ], procedures: [ new Procedure(new Name('ArbitrarilyChosen'), ProcedureKind.Method, new FunctionNode(new Block([])), fileUri: uriA) ], ); final Component testComponent = new Component( libraries: [libraryA, libraryB, libraryC, ...testCoreLibraries]); final IncrementalJavaScriptBundler javaScriptBundler = new IncrementalJavaScriptBundler( null, {}, multiRootScheme, ); await javaScriptBundler.initialize(testComponent, uriA, packageConfig); // Create a new component that only contains C. final Library libraryC2 = new Library( uriC, fileUri: uriC, procedures: [ new Procedure(new Name('AlternativeContents'), ProcedureKind.Method, new FunctionNode(new Block([])), fileUri: uriC) ], ); final Component partialComponent = new Component(libraries: [libraryC2]); await javaScriptBundler.invalidate( partialComponent, testComponent, uriA, packageConfig); final _MemorySink manifestSink = new _MemorySink(); final _MemorySink codeSink = new _MemorySink(); final _MemorySink sourcemapSink = new _MemorySink(); final _MemorySink metadataSink = new _MemorySink(); final _MemorySink symbolsSink = new _MemorySink(); final CoreTypes coreTypes = new CoreTypes(testComponent); await javaScriptBundler.compile( new ClassHierarchy(testComponent, coreTypes), coreTypes, packageConfig, codeSink, manifestSink, sourcemapSink, metadataSink, symbolsSink, ); final String code = utf8.decode(codeSink.buffer); // There should be only a single module. final String moduleHeader = r"define(['dart_sdk'], (function load__"; expect(moduleHeader.allMatches(code), hasLength(1)); // C source code should be updated. expect(code, contains('AlternativeContents')); }); } class _MemorySink implements IOSink { final List buffer = []; @override void add(List data) { buffer.addAll(data); } @override Future close() => new Future.value(); @override void noSuchMethod(Invocation invocation) { throw new UnsupportedError(invocation.memberName.toString()); } } Map> _combineMaps( Map> left, Map> right, ) { final Map> result = new Map>.of(left); for (String key in right.keys) { (result[key] ??= []).addAll(right[key]!); } return result; }