| // 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<String, Map<String, List<String>>> 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<String, List<String>> requiredTopLevels = { |
| 'dart:_runtime': ['assertInterop'], |
| 'dart:async': [ |
| '_asyncAwait', |
| '_asyncReturn', |
| '_asyncRethrow', |
| '_asyncStarHelper', |
| '_asyncStartSync', |
| '_makeAsyncAwaitCompleter', |
| '_makeSyncStarIterable', |
| '_makeAsyncStarStreamController', |
| '_makeSyncStarIterable', |
| '_streamOfController', |
| '_wrapJsFunctionForAsync', |
| ], |
| }; |
| |
| void main() { |
| Map<String, List<String>> allRequiredTypes = _combineMaps( |
| CoreTypes.requiredClasses, |
| additionalRequiredClasses |
| .map((k, v) => new MapEntry(k, v.keys.toList()))); |
| List<String> allRequiredLibraries = [ |
| ...allRequiredTypes.keys, |
| ...requiredTopLevels.keys, |
| ]; |
| List<Library> 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 component uris for file paths', () async { |
| final Uri fileUri = new Uri.file('/c.dart'); |
| |
| final IncrementalJavaScriptBundler javaScriptBundler = |
| new IncrementalJavaScriptBundler( |
| null, |
| {}, |
| multiRootScheme, |
| useDebuggerModuleNames: debuggerNames, |
| ); |
| |
| final String componentUrl = |
| javaScriptBundler.urlForComponentUri(fileUri, packageConfig); |
| final String libraryBundleName = |
| javaScriptBundler.makeLibraryBundleName(componentUrl); |
| expect(componentUrl, '/c.dart'); |
| expect(libraryBundleName, 'c.dart'); |
| }); |
| |
| test('Creates component 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 componentUrl = |
| javaScriptBundler.urlForComponentUri(packageUri, packageConfig); |
| final String libraryBundleName = |
| javaScriptBundler.makeLibraryBundleName(componentUrl); |
| expect(componentUrl, |
| debuggerNames ? 'packages/a/lib/a.dart' : '/packages/a/a.dart'); |
| expect(libraryBundleName, |
| 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<String, Compiler> compilers = await javaScriptBundler.compile( |
| new ClassHierarchy(testComponent, coreTypes), |
| coreTypes, |
| packageConfig, |
| codeSink, |
| manifestSink, |
| sourcemapSink, |
| metadataSink, |
| symbolsSink, |
| ); |
| |
| final Map<String, dynamic> 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. |
| expect(compilers.keys, equals([library.importUri.toString()])); |
| }); |
| |
| 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<String, dynamic> manifest = |
| json.decode(utf8.decode(manifestSink.buffer)); |
| final String code = utf8.decode(codeSink.buffer); |
| |
| final String componentUrl = javaScriptBundler.urlForComponentUri( |
| library.importUri, packageConfig); |
| |
| expect(manifest, { |
| '$componentUrl.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 library bundles 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<String, dynamic> 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<dynamic> offsets = manifest['/a.dart.lib.js']['sourcemap']; |
| final Map<String, dynamic> 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, |
| recompileRestart: false); |
| |
| 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<String, dynamic> 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<dynamic> offsets = manifest['/a.dart.lib.js']['sourcemap']; |
| final Map<String, dynamic> 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, |
| recompileRestart: false); |
| |
| 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<int> buffer = <int>[]; |
| |
| @override |
| void add(List<int> data) { |
| buffer.addAll(data); |
| } |
| |
| @override |
| Future<void> close() => new Future.value(); |
| |
| @override |
| void noSuchMethod(Invocation invocation) { |
| throw new UnsupportedError(invocation.memberName.toString()); |
| } |
| } |
| |
| Map<String, List<String>> _combineMaps( |
| Map<String, List<String>> left, |
| Map<String, List<String>> right, |
| ) { |
| final Map<String, List<String>> result = |
| new Map<String, List<String>>.of(left); |
| for (String key in right.keys) { |
| (result[key] ??= []).addAll(right[key]!); |
| } |
| return result; |
| } |