diff --git a/build_modules/CHANGELOG.md b/build_modules/CHANGELOG.md index cde4e243e..c3f796cf7 100644 --- a/build_modules/CHANGELOG.md +++ b/build_modules/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.1.5 + +- Add support for DDC's Library Bundle module system, which is compatible with web hot reload. This is not yet enabled by default. + ## 5.1.4 - Fix module_builder reading DDC modules for non-primary dart libraries. diff --git a/build_modules/lib/src/module_builder.dart b/build_modules/lib/src/module_builder.dart index 380c9ab5f..8b7285f36 100644 --- a/build_modules/lib/src/module_builder.dart +++ b/build_modules/lib/src/module_builder.dart @@ -24,12 +24,12 @@ String moduleExtension(DartPlatform platform) => '.${platform.name}.module'; class ModuleBuilder implements Builder { final DartPlatform _platform; - /// Emits DDC code with the Library Bundle module system, which supports hot - /// reload. - /// /// If set, this builder will consume raw meta modules (instead of clean). - /// Clean meta modules are only used for DDC's AMD module system due its - /// requirement that self-referential libraries be bundled. + /// + /// Clean meta modules cannot be used when compiling with the Frontend Server + /// due to potentially divergent bundling strategies between it and + /// build_runner. Additionally, bundling isn't required in DDC's Library + /// Bundle module system. final bool usesWebHotReload; ModuleBuilder(this._platform, {this.usesWebHotReload = false}) diff --git a/build_modules/pubspec.yaml b/build_modules/pubspec.yaml index b606c2af8..81e3b59be 100644 --- a/build_modules/pubspec.yaml +++ b/build_modules/pubspec.yaml @@ -1,5 +1,5 @@ name: build_modules -version: 5.1.4 +version: 5.1.5 description: >- Builders to analyze and split Dart code into individually compilable modules based on imports. diff --git a/build_web_compilers/CHANGELOG.md b/build_web_compilers/CHANGELOG.md index 91f27cb8e..30aa5f2e1 100644 --- a/build_web_compilers/CHANGELOG.md +++ b/build_web_compilers/CHANGELOG.md @@ -1,5 +1,7 @@ -## 4.4.6 +## 4.4.7 +- Add support for DDC's Library Bundle module system, which is compatible with web hot reload. This is not yet enabled by default. +## 4.4.6 - Add build options to customize the SDK used for compiling to js and wasm. ## 4.4.5 diff --git a/build_web_compilers/lib/builders.dart b/build_web_compilers/lib/builders.dart index 0a369fbc6..6a41ce7df 100644 --- a/build_web_compilers/lib/builders.dart +++ b/build_web_compilers/lib/builders.dart @@ -64,8 +64,11 @@ Builder ddcBuilder(BuilderOptions options) { useIncrementalCompiler: _readUseIncrementalCompilerOption(options), generateFullDill: _readGenerateFullDillOption(options), emitDebugSymbols: _readEmitDebugSymbolsOption(options), - canaryFeatures: _readCanaryOption(options), - ddcModules: _readWebHotReloadOption(options), + canaryFeatures: + _readCanaryOption(options) || _readWebHotReloadOption(options), + ddcLibraryBundle: + _readDdcLibraryBundleOption(options) || + _readWebHotReloadOption(options), trackUnusedInputs: _readTrackInputsCompilerOption(options), platform: ddcPlatform, sdkKernelPath: _readDdcKernelPathOption(options), @@ -102,8 +105,10 @@ Builder sdkJsCompile(BuilderOptions options) { sdkKernelPath: 'lib/_internal/ddc_platform.dill', outputPath: 'lib/src/dev_compiler/dart_sdk.js', canaryFeatures: - _readWebHotReloadOption(options) || _readCanaryOption(options), - usesWebHotReload: _readWebHotReloadOption(options), + _readCanaryOption(options) || _readWebHotReloadOption(options), + ddcLibraryBundle: + _readDdcLibraryBundleOption(options) || + _readWebHotReloadOption(options), usePrebuiltSdkFromPath: _readUsePrebuiltSdkFromPathOption(options), ); } @@ -244,6 +249,10 @@ bool _readWebHotReloadOption(BuilderOptions options) { return options.config[_webHotReloadOption] as bool? ?? false; } +bool _readDdcLibraryBundleOption(BuilderOptions options) { + return options.config[_ddcLibraryBundleOption] as bool? ?? false; +} + bool _readUseUiLibrariesOption(BuilderOptions options) { return options.config[_useUiLibrariesOption] as bool? ?? false; } @@ -279,6 +288,7 @@ const _canaryOption = 'canary'; const _trackUnusedInputsCompilerOption = 'track-unused-inputs'; const _environmentOption = 'environment'; const _webHotReloadOption = 'web-hot-reload'; +const _ddcLibraryBundleOption = 'ddc-library-bundle'; const _useUiLibrariesOption = 'use-ui-libraries'; const _ddcKernelPathOption = 'ddc-kernel-path'; const _librariesPathOption = 'libraries-path'; @@ -293,8 +303,10 @@ const _supportedOptions = [ _canaryOption, _trackUnusedInputsCompilerOption, _webHotReloadOption, + _ddcLibraryBundleOption, _useUiLibrariesOption, _ddcKernelPathOption, _librariesPathOption, _platformSdkOption, + _usePrebuiltSdkFromPathOption, ]; diff --git a/build_web_compilers/lib/src/dev_compiler_bootstrap.dart b/build_web_compilers/lib/src/dev_compiler_bootstrap.dart index 6d5af8a8b..6ded491fe 100644 --- a/build_web_compilers/lib/src/dev_compiler_bootstrap.dart +++ b/build_web_compilers/lib/src/dev_compiler_bootstrap.dart @@ -37,6 +37,7 @@ Future bootstrapDdc( String entrypointExtension = jsEntrypointExtension, required bool? nativeNullAssertions, bool usesWebHotReload = false, + bool ddcLibraryBundle = false, bool unsafeAllowUnsupportedModules = false, }) async { // Ensures that the sdk resources are built and available. @@ -112,7 +113,7 @@ $librariesString final dartEntrypointParts = _context.split(dartEntrypointId.path); final packageName = module.primarySource.package; final entrypointLibraryName = - usesWebHotReload + ddcLibraryBundle ? _context.joinAll([ // Convert to a package: uri for files under lib. if (dartEntrypointParts.first == 'lib') 'package:$packageName', @@ -132,7 +133,7 @@ $librariesString String entrypointJsContent; String bootstrapContent; String bootstrapEndContent; - if (usesWebHotReload) { + if (ddcLibraryBundle) { final ddcSdkUrl = r'packages/build_web_compilers/src/dev_compiler/dart_sdk.js'; modulePaths['dart_sdk'] = ddcSdkUrl; @@ -148,8 +149,8 @@ $librariesString : _context.joinAll(_context.split(jsId.path).skip(1)); } final bootstrapEndModuleName = _context.relative( - bootstrapId.path, - from: _context.dirname(bootstrapEndId.path), + bootstrapEndId.path, + from: _context.dirname(bootstrapId.path), ); bootstrapContent = generateDDCLibraryBundleMainModule( entrypoint: entrypointLibraryName, diff --git a/build_web_compilers/lib/src/dev_compiler_builder.dart b/build_web_compilers/lib/src/dev_compiler_builder.dart index 66cbe235c..3ca325b73 100644 --- a/build_web_compilers/lib/src/dev_compiler_builder.dart +++ b/build_web_compilers/lib/src/dev_compiler_builder.dart @@ -42,8 +42,8 @@ class DevCompilerBuilder implements Builder { /// Enables canary features in DDC. final bool canaryFeatures; - /// Emits code with the DDC module system. - final bool ddcModules; + /// Emits code with the DDC Library Bundle module system. + final bool ddcLibraryBundle; final bool trackUnusedInputs; @@ -74,7 +74,7 @@ class DevCompilerBuilder implements Builder { this.generateFullDill = false, this.emitDebugSymbols = false, this.canaryFeatures = false, - this.ddcModules = false, + this.ddcLibraryBundle = false, this.trackUnusedInputs = false, required this.platform, String? sdkKernelPath, @@ -125,16 +125,16 @@ class DevCompilerBuilder implements Builder { await _createDevCompilerModule( module, buildStep, - useIncrementalCompiler, - generateFullDill, - emitDebugSymbols, - canaryFeatures, - ddcModules, - trackUnusedInputs, - platformSdk, - sdkKernelPath, - librariesPath, environment, + useIncrementalCompiler: useIncrementalCompiler, + generateFullDill: generateFullDill, + emitDebugSymbols: emitDebugSymbols, + canaryFeatures: canaryFeatures, + ddcLibraryBundle: ddcLibraryBundle, + trackUnusedInputs: trackUnusedInputs, + dartSdk: platformSdk, + sdkKernelPath: sdkKernelPath, + librariesPath: librariesPath, ); } on DartDevcCompilationException catch (e) { await handleError(e); @@ -148,16 +148,16 @@ class DevCompilerBuilder implements Builder { Future _createDevCompilerModule( Module module, BuildStep buildStep, - bool useIncrementalCompiler, - bool generateFullDill, - bool emitDebugSymbols, - bool canaryFeatures, - bool ddcModules, - bool trackUnusedInputs, - String dartSdk, - String sdkKernelPath, - String librariesPath, Map environment, { + required bool useIncrementalCompiler, + required bool generateFullDill, + required bool emitDebugSymbols, + required bool canaryFeatures, + required bool ddcLibraryBundle, + required bool trackUnusedInputs, + required String dartSdk, + required String sdkKernelPath, + required String librariesPath, bool debugMode = true, }) async { final transitiveDeps = await buildStep.trackStage( @@ -202,11 +202,11 @@ Future _createDevCompilerModule( WorkRequest() ..arguments.addAll([ '--dart-sdk-summary=$sdkSummary', - '--modules=${ddcModules ? 'ddc' : 'amd'}', + '--modules=${ddcLibraryBundle ? 'ddc' : 'amd'}', '--no-summarize', if (generateFullDill) '--experimental-output-compiled-kernel', if (emitDebugSymbols) '--emit-debug-symbols', - if (canaryFeatures) '--canary', + if (canaryFeatures || ddcLibraryBundle) '--canary', '-o', jsOutputFile.path, debugMode ? '--source-map' : '--no-source-map', diff --git a/build_web_compilers/lib/src/sdk_js_compile_builder.dart b/build_web_compilers/lib/src/sdk_js_compile_builder.dart index fe2b9e037..e5b71f64e 100644 --- a/build_web_compilers/lib/src/sdk_js_compile_builder.dart +++ b/build_web_compilers/lib/src/sdk_js_compile_builder.dart @@ -42,9 +42,8 @@ class SdkJsCompileBuilder implements Builder { /// Enables canary features in DDC. final bool canaryFeatures; - /// Emits DDC code with the Library Bundle module system, which supports hot - /// reload. - final bool usesWebHotReload; + /// Emits DDC code using its Library Bundle module system. + final bool ddcLibraryBundle; /// An optional directory path that contains prebuilt sdk files. /// @@ -58,7 +57,7 @@ class SdkJsCompileBuilder implements Builder { String? librariesPath, String? platformSdk, required this.canaryFeatures, - required this.usesWebHotReload, + required this.ddcLibraryBundle, this.usePrebuiltSdkFromPath, }) : platformSdk = platformSdk ?? sdkDir, librariesPath = @@ -82,12 +81,12 @@ class SdkJsCompileBuilder implements Builder { } else { await _createDevCompilerModule( buildStep, - platformSdk, - sdkKernelPath, - librariesPath, jsOutputId, - canaryFeatures, - usesWebHotReload, + dartSdk: platformSdk, + sdkKernelPath: sdkKernelPath, + librariesPath: librariesPath, + canaryFeatures: canaryFeatures, + ddcLibraryBundle: ddcLibraryBundle, ); } } @@ -96,13 +95,13 @@ class SdkJsCompileBuilder implements Builder { /// Compile the sdk module with the dev compiler. Future _createDevCompilerModule( BuildStep buildStep, - String dartSdk, - String sdkKernelPath, - String librariesPath, - AssetId jsOutputId, - bool canaryFeatures, - bool usesWebHotReload, -) async { + AssetId jsOutputId, { + required String dartSdk, + required String sdkKernelPath, + required String librariesPath, + required bool canaryFeatures, + required bool ddcLibraryBundle, +}) async { final scratchSpace = await buildStep.fetchResource(scratchSpaceResource); final jsOutputFile = scratchSpace.fileFor(jsOutputId); @@ -130,8 +129,8 @@ Future _createDevCompilerModule( result = await Process.run(dartPath, [ snapshotPath, '--multi-root-scheme=org-dartlang-sdk', - '--modules=${usesWebHotReload ? 'ddc' : 'amd'}', - if (canaryFeatures || usesWebHotReload) '--canary', + '--modules=${ddcLibraryBundle ? 'ddc' : 'amd'}', + if (canaryFeatures || ddcLibraryBundle) '--canary', '--module-name=dart_sdk', '-o', jsOutputFile.path, diff --git a/build_web_compilers/lib/src/web_entrypoint_builder.dart b/build_web_compilers/lib/src/web_entrypoint_builder.dart index 3fd13cf8a..0c6d1f396 100644 --- a/build_web_compilers/lib/src/web_entrypoint_builder.dart +++ b/build_web_compilers/lib/src/web_entrypoint_builder.dart @@ -126,9 +126,13 @@ final class EntrypointBuilderOptions { /// Whether or not to emit DDC entrypoints that support web hot reload. /// - /// Web hot reload is only supported for DDC's Library Bundle module system. + /// Only supported for DDC's Library Bundle module system. final bool usesWebHotReload; + /// Whether or not to emit DDC entrypoints that target the DDC Library Bundle + /// module system. + final bool ddcLibraryBundle; + /// The absolute path to the libraries file for the current platform. /// /// If not provided, defaults to "lib/libraries.json" in the sdk directory. @@ -145,6 +149,7 @@ final class EntrypointBuilderOptions { this.nativeNullAssertions, this.loaderExtension, this.usesWebHotReload = false, + this.ddcLibraryBundle = false, this.librariesPath, this.unsafeAllowUnsupportedModules = false, }); @@ -159,9 +164,11 @@ final class EntrypointBuilderOptions { const nativeNullAssertionsOption = 'native_null_assertions'; const loaderOption = 'loader'; const webHotReloadOption = 'web-hot-reload'; + const ddcLibraryBundleOption = 'ddc-library-bundle'; const librariesPathOption = 'libraries-path'; const unsafeAllowUnsupportedModulesOption = 'unsafe-allow-unsupported-modules'; + String? defaultLoaderOption; const supportedOptions = [ @@ -172,6 +179,7 @@ final class EntrypointBuilderOptions { dart2wasmArgsOption, loaderOption, webHotReloadOption, + ddcLibraryBundleOption, librariesPathOption, unsafeAllowUnsupportedModulesOption, 'use-ui-libraries', @@ -181,9 +189,12 @@ final class EntrypointBuilderOptions { final nativeNullAssertions = options.config[nativeNullAssertionsOption] as bool?; final usesWebHotReload = options.config[webHotReloadOption] as bool?; + final usesDdcLibraryBundle = + usesWebHotReload ?? options.config[ddcLibraryBundleOption] as bool?; final librariesPath = options.config[librariesPathOption] as String?; final unsafeAllowUnsupportedModules = options.config[unsafeAllowUnsupportedModulesOption] as bool?; + final compilers = []; validateOptions( @@ -271,6 +282,7 @@ final class EntrypointBuilderOptions { ? config[loaderOption] as String? : defaultLoaderOption, usesWebHotReload: usesWebHotReload ?? false, + ddcLibraryBundle: usesDdcLibraryBundle ?? false, librariesPath: librariesPath, unsafeAllowUnsupportedModules: unsafeAllowUnsupportedModules ?? false, ); @@ -354,14 +366,17 @@ class WebEntrypointBuilder implements Builder { Future(() async { try { final usesWebHotReload = options.usesWebHotReload; + final usesDdcLibraryBundle = + options.ddcLibraryBundle || usesWebHotReload; await bootstrapDdc( buildStep, nativeNullAssertions: options.nativeNullAssertions, requiredAssets: - usesWebHotReload + usesDdcLibraryBundle ? _ddcLibraryBundleSdkResources : _ddcSdkResources, usesWebHotReload: usesWebHotReload, + ddcLibraryBundle: usesDdcLibraryBundle, unsafeAllowUnsupportedModules: options.unsafeAllowUnsupportedModules, ); diff --git a/build_web_compilers/lib/src/web_entrypoint_marker_builder.dart b/build_web_compilers/lib/src/web_entrypoint_marker_builder.dart index cc2dda9ad..90ac8fee4 100644 --- a/build_web_compilers/lib/src/web_entrypoint_marker_builder.dart +++ b/build_web_compilers/lib/src/web_entrypoint_marker_builder.dart @@ -11,7 +11,7 @@ import 'package:glob/glob.dart'; /// A builder that gathers information about a web target's 'main' entrypoint. class WebEntrypointMarkerBuilder implements Builder { /// Records state (such as the web entrypoint) required when compiling DDC - /// with the Library Bundle module system. + /// with the Frontend Server, which supports hot reload. /// /// A no-op if [usesWebHotReload] is not set. final bool usesWebHotReload; diff --git a/build_web_compilers/test/ddc_library_bundle_bootstrap_test.dart b/build_web_compilers/test/ddc_library_bundle_bootstrap_test.dart new file mode 100644 index 000000000..c0274305a --- /dev/null +++ b/build_web_compilers/test/ddc_library_bundle_bootstrap_test.dart @@ -0,0 +1,278 @@ +// Copyright (c) 2025, 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 'package:build/build.dart'; +import 'package:build_modules/build_modules.dart'; +import 'package:build_test/build_test.dart'; +import 'package:build_web_compilers/build_web_compilers.dart'; +import 'package:build_web_compilers/builders.dart'; +import 'package:test/test.dart'; + +final defaultBuilderOptions = const BuilderOptions({ + 'compiler': 'dartdevc', + 'ddc-library-bundle': true, + 'native_null_assertions': false, +}); + +void main() { + initializePlatforms(); + + final startingBuilders = { + // Uses the real sdk copy builder to copy required files from the SDK. + sdkJsCopyRequirejs(const BuilderOptions({})), + sdkJsCompile(defaultBuilderOptions), + const ModuleLibraryBuilder(), + MetaModuleBuilder(ddcPlatform), + MetaModuleCleanBuilder(ddcPlatform), + ModuleBuilder(ddcPlatform), + ddcKernelBuilder(const BuilderOptions({})), + DevCompilerBuilder(platform: ddcPlatform, ddcLibraryBundle: true), + }; + group('DDC Library Bundle:', () { + group('simple project', () { + final startingAssets = { + 'a|lib/a.dart': ''' + import 'package:b/b.dart'; + final hello = world; + ''', + 'a|web/index.dart': ''' + import "package:a/a.dart"; + main() { + print(hello); + } + ''', + 'b|lib/b.dart': '''final world = 'world';''', + // Add a fake asset so that the build_web_compilers package exists. + 'build_web_compilers|fake.txt': '', + }; + final startingExpectedOutputs = { + 'a|lib/.ddc.meta_module.clean': isNotNull, + 'a|lib/.ddc.meta_module.raw': isNotNull, + 'a|lib/a.ddc.dill': isNotNull, + 'a|lib/a.ddc.js.map': isNotNull, + 'a|lib/a.ddc.js.metadata': isNotNull, + 'a|lib/a.ddc.js': isNotNull, + 'a|lib/a.ddc.module': isNotNull, + 'a|lib/a.module.library': isNotNull, + 'a|web/index.ddc.dill': isNotNull, + 'a|web/index.ddc.js.map': isNotNull, + 'a|web/index.ddc.js.metadata': isNotNull, + 'a|web/index.ddc.js': isNotNull, + 'a|web/index.ddc.module': isNotNull, + 'a|web/index.module.library': isNotNull, + 'b|lib/.ddc.meta_module.clean': isNotNull, + 'b|lib/.ddc.meta_module.raw': isNotNull, + 'b|lib/b.ddc.dill': isNotNull, + 'b|lib/b.ddc.js.map': isNotNull, + 'b|lib/b.ddc.js.metadata': isNotNull, + 'b|lib/b.ddc.js': isNotNull, + 'b|lib/b.ddc.module': isNotNull, + 'b|lib/b.module.library': isNotNull, + 'build_web_compilers|lib/.ddc.meta_module.clean': isNotNull, + 'build_web_compilers|lib/.ddc.meta_module.raw': isNotNull, + 'build_web_compilers|lib/src/dev_compiler/dart_sdk.js.map': isNotNull, + 'build_web_compilers|lib/src/dev_compiler/dart_sdk.js': isNotNull, + 'build_web_compilers|lib/src/dev_compiler/ddc_module_loader.js': + isNotNull, + 'build_web_compilers|lib/src/dev_compiler/require.js': isNotNull, + }; + + test('base build', () async { + await testBuilders( + startingBuilders, + startingAssets, + outputs: startingExpectedOutputs, + ); + }); + + test('can bootstrap dart entrypoints', () async { + // Just do some basic sanity checking, integration tests will validate + // things actually work. + final builder = WebEntrypointBuilder.fromOptions(defaultBuilderOptions); + final expectedOutputs = Map.of(startingExpectedOutputs)..addAll({ + 'a|web/index.dart.bootstrap.js': decodedMatches( + allOf([ + // Calls 'main' via the embedder. + contains('dartDevEmbedder.runMain'), + ]), + ), + 'a|web/index.dart.bootstrap.end.js': isNotEmpty, + 'a|web/index.dart.ddc_merged_metadata': isNotEmpty, + 'a|web/index.ddc.js': decodedMatches( + // Contains the library declaration of the entrypoint library. + contains( + 'dartDevEmbedder.defineLibrary("org-dartlang-app:///web/index.dart"', + ), + ), + 'a|web/index.dart.js': decodedMatches( + allOf([ + // Contains a script pointer to main's bootstrap.js file. + contains('"src": "index.dart.bootstrap.js", "id": "data-main"'), + // Maps non-lib modules to remove the top level dir. + contains( + '"src": "index.ddc.js", "id": "org-dartlang-app:///web/index.dart"', + ), + // Maps lib modules to packages path + contains( + '"src": "packages/a/a.ddc.js", "id": "package:a/a.dart"', + ), + contains( + '"src": "packages/b/b.ddc.js", "id": "package:b/b.dart"', + ), + // Imports the dart sdk. + contains('"id": "dart_sdk"'), + isNot(contains('lib/a')), + ]), + ), + 'a|web/index.digests': decodedMatches(contains('packages/')), + }); + await testBuilders( + [...startingBuilders, builder], + startingAssets, + outputs: expectedOutputs, + ); + }); + }); + + group('regression tests', () { + test('root dart file is not the primary source, #2269', () async { + final builder = WebEntrypointBuilder.fromOptions(defaultBuilderOptions); + final assets = { + // Becomes the primary source for the module, since it we alpha-sort. + 'a|web/a.dart': ''' + final hello = 'hello'; + ''', + // Rolled into the module for `a.dart`, as a normal source. + 'a|web/b.dart': ''' + import 'a.dart'; + main() { + print(hello); + } + ''', + // Add a fake asset so that the build_web_compilers package exists. + 'build_web_compilers|fake.txt': '', + }; + // Check that we are invoking the correct + final expectedOutputs = { + 'a|lib/.ddc.meta_module.clean': isNotNull, + 'a|lib/.ddc.meta_module.raw': isNotNull, + 'a|web/a.ddc.dill': isNotNull, + 'a|web/a.ddc.js.map': isNotNull, + 'a|web/a.ddc.js.metadata': isNotNull, + 'a|web/a.ddc.js': isNotNull, + 'a|web/a.ddc.module': isNotNull, + 'a|web/a.module.library': isNotNull, + 'a|web/b.dart.bootstrap.js': isNotEmpty, + 'a|web/b.dart.bootstrap.end.js': isNotEmpty, + 'a|web/b.dart.ddc_merged_metadata': isNotNull, + 'a|web/b.dart.js': decodedMatches( + allOf([ + // Confirm that `a.dart` is the actual primary source. + contains( + '"src": "a.ddc.js", "id": "org-dartlang-app:///web/a.dart"', + ), + // And `b.dart` is the application whose 'main' is being invoked. + contains('"src": "b.dart.bootstrap.js", "id": "data-main'), + ]), + ), + 'a|web/b.ddc.module': isNotNull, + 'a|web/b.digests': isNotNull, + 'a|web/b.module.library': isNotNull, + 'build_web_compilers|lib/.ddc.meta_module.clean': isNotNull, + 'build_web_compilers|lib/.ddc.meta_module.raw': isNotNull, + 'build_web_compilers|lib/src/dev_compiler/dart_sdk.js.map': isNotNull, + 'build_web_compilers|lib/src/dev_compiler/dart_sdk.js': isNotNull, + 'build_web_compilers|lib/src/dev_compiler/ddc_module_loader.js': + isNotNull, + 'build_web_compilers|lib/src/dev_compiler/require.js': isNotNull, + }; + + await testBuilders( + [...startingBuilders, builder], + assets, + outputs: expectedOutputs, + ); + }); + + test('root dart file is under lib', () async { + final builder = WebEntrypointBuilder.fromOptions(defaultBuilderOptions); + final assets = { + 'a|lib/app.dart': 'void main() {}', + // Add a fake asset so that the build_web_compilers package exists. + 'build_web_compilers|fake.txt': '', + }; + final expectedOutputs = { + 'a|lib/.ddc.meta_module.clean': isNotNull, + 'a|lib/.ddc.meta_module.raw': isNotNull, + 'a|lib/app.dart.bootstrap.js': isNotNull, + 'a|lib/app.dart.bootstrap.end.js': isNotEmpty, + 'a|lib/app.dart.ddc_merged_metadata': isNotEmpty, + 'a|lib/app.dart.js': decodedMatches( + // Confirm that the child name is referenced via a package: uri + // and not relative path to the root dir being served. + contains( + '"src": "packages/a/app.ddc.js", "id": "package:a/app.dart"', + ), + ), + 'a|lib/app.ddc.dill': isNotNull, + 'a|lib/app.ddc.js.map': isNotNull, + 'a|lib/app.ddc.js.metadata': isNotNull, + 'a|lib/app.ddc.js': isNotNull, + 'a|lib/app.ddc.module': isNotNull, + 'a|lib/app.digests': isNotEmpty, + 'a|lib/app.module.library': isNotNull, + 'build_web_compilers|lib/.ddc.meta_module.clean': isNotNull, + 'build_web_compilers|lib/.ddc.meta_module.raw': isNotNull, + 'build_web_compilers|lib/src/dev_compiler/dart_sdk.js.map': isNotNull, + 'build_web_compilers|lib/src/dev_compiler/dart_sdk.js': isNotNull, + 'build_web_compilers|lib/src/dev_compiler/ddc_module_loader.js': + isNotNull, + 'build_web_compilers|lib/src/dev_compiler/require.js': isNotNull, + }; + + await testBuilders( + [...startingBuilders, builder], + assets, + outputs: expectedOutputs, + ); + }); + + test('can enable canary features for SDK', () async { + final builder = sdkJsCompile( + const BuilderOptions({'canary': true, 'ddc-library-bundle': true}), + ); + final sdkAssets = {'build_web_compilers|fake.txt': ''}; + final expectedOutputs = { + 'build_web_compilers|lib/src/dev_compiler/dart_sdk.js': + decodedMatches(contains('canary')), + 'build_web_compilers|lib/src/dev_compiler/dart_sdk.js.map': + isNotEmpty, + }; + await testBuilder(builder, sdkAssets, outputs: expectedOutputs); + }); + + test( + 'does not enable canary features for SDK by default', + () async { + final builder = sdkJsCompile( + const BuilderOptions({'ddc-library-bundle': true}), + ); + final sdkAssets = { + 'build_web_compilers|fake.txt': '', + }; + final expectedOutputs = { + 'build_web_compilers|lib/src/dev_compiler/dart_sdk.js': + decodedMatches(isNot(contains('canary'))), + 'build_web_compilers|lib/src/dev_compiler/dart_sdk.js.map': + isNotEmpty, + }; + await testBuilder(builder, sdkAssets, outputs: expectedOutputs); + }, + skip: + 'Enable this test when the library bundle module system is no ' + 'longer locked behind the --canary flag', + ); + }); + }); +} diff --git a/build_web_compilers/test/ddc_library_bundle_builder_test.dart b/build_web_compilers/test/ddc_library_bundle_builder_test.dart new file mode 100644 index 000000000..b6b16fde4 --- /dev/null +++ b/build_web_compilers/test/ddc_library_bundle_builder_test.dart @@ -0,0 +1,437 @@ +// Copyright (c) 2025, 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 'package:build/build.dart'; +import 'package:build_modules/build_modules.dart'; +import 'package:build_test/build_test.dart'; +import 'package:build_web_compilers/build_web_compilers.dart'; +import 'package:build_web_compilers/builders.dart'; +import 'package:logging/logging.dart'; +import 'package:test/test.dart'; + +final builderOptions = const BuilderOptions({ + 'track-unused-inputs': false, + 'ddc-library-bundle': true, +}); + +void main() { + initializePlatforms(); + + group('DDC Library Bundle:', () { + group('error free project', () { + final startingAssets = { + 'a|lib/a.dart': r''' + // @dart=2.12 + import 'package:b/b.dart'; + final hello = 'hello $world'; + ''', + 'a|web/index.dart': ''' + // @dart=2.12 + import "package:a/a.dart"; + void main() { + print(hello); + print(const String.fromEnvironment('foo', defaultValue: 'bar')); + } + ''', + 'b|lib/b.dart': ''' + // @dart=2.12 + final world = 'world';''', + }; + final startingBuilders = [ + const ModuleLibraryBuilder(), + MetaModuleBuilder(ddcPlatform), + MetaModuleCleanBuilder(ddcPlatform), + ModuleBuilder(ddcPlatform), + ddcKernelBuilder(builderOptions), + ]; + final startingExpectedOutputs = { + 'a|lib/.ddc.meta_module.clean': isNotNull, + 'a|lib/.ddc.meta_module.raw': isNotNull, + 'a|lib/a.ddc.dill': isNotNull, + 'a|lib/a.ddc.module': isNotNull, + 'a|lib/a.module.library': isNotNull, + 'a|web/index.ddc.dill': isNotNull, + 'a|web/index.ddc.module': isNotNull, + 'a|web/index.module.library': isNotNull, + 'b|lib/.ddc.meta_module.clean': isNotNull, + 'b|lib/.ddc.meta_module.raw': isNotNull, + 'b|lib/b.ddc.dill': isNotNull, + 'b|lib/b.ddc.module': isNotNull, + 'b|lib/b.module.library': isNotNull, + }; + + setUp(() async { + final listener = Logger.root.onRecord.listen( + (r) => printOnFailure('$r\n${r.error}\n${r.stackTrace}'), + ); + addTearDown(listener.cancel); + }); + + test('base build', () async { + await testBuilders( + startingBuilders, + startingAssets, + outputs: startingExpectedOutputs, + ); + }); + + for (final trackUnusedInputs in [true, false]) { + test('can compile ddc modules under lib and web and ' + '${trackUnusedInputs ? 'track' : 'not track'} ' + 'unused inputs', () async { + final builder = DevCompilerBuilder( + platform: ddcPlatform, + useIncrementalCompiler: trackUnusedInputs, + trackUnusedInputs: trackUnusedInputs, + ddcLibraryBundle: true, + ); + + final expectedOutputs = Map.of(startingExpectedOutputs)..addAll({ + 'a|lib/a$jsModuleExtension': decodedMatches(contains('hello')), + 'a|lib/a$jsSourceMapExtension': decodedMatches(contains('a.dart')), + 'a|lib/a$metadataExtension': isNotNull, + 'a|web/index$jsModuleExtension': decodedMatches(contains('main')), + 'a|web/index$jsSourceMapExtension': decodedMatches( + contains('index.dart'), + ), + 'a|web/index$metadataExtension': isNotNull, + 'b|lib/b$jsModuleExtension': decodedMatches(contains('world')), + 'b|lib/b$jsSourceMapExtension': decodedMatches(contains('b.dart')), + 'b|lib/b$metadataExtension': isNotNull, + }); + + final reportedUnused = >{}; + await testBuilders( + [...startingBuilders, builder], + startingAssets, + outputs: expectedOutputs, + reportUnusedAssetsForInput: (input, unused) { + reportedUnused[input] = unused; + }, + ); + + expect( + reportedUnused[AssetId( + 'a', + 'web/index${moduleExtension(ddcPlatform)}', + )], + equals( + trackUnusedInputs + ? [AssetId('b', 'lib/b$ddcKernelExtension')] + : null, + ), + reason: + 'Should${trackUnusedInputs ? '' : ' not'} report unused ' + 'transitive deps.', + ); + }); + } + + test('allows a custom environment', () async { + final builder = DevCompilerBuilder( + platform: ddcPlatform, + environment: {'foo': 'zap'}, + ddcLibraryBundle: true, + ); + final expectedOutputs = Map.of(startingExpectedOutputs)..addAll({ + 'a|lib/a$jsModuleExtension': isNotNull, + 'a|lib/a$jsSourceMapExtension': isNotNull, + 'a|lib/a$metadataExtension': isNotNull, + 'a|web/index$jsModuleExtension': decodedMatches( + contains('print("zap")'), + ), + 'a|web/index$jsSourceMapExtension': isNotNull, + 'a|web/index$metadataExtension': isNotNull, + 'b|lib/b$jsModuleExtension': isNotNull, + 'b|lib/b$jsSourceMapExtension': isNotNull, + 'b|lib/b$metadataExtension': isNotNull, + }); + await testBuilders( + [...startingBuilders, builder], + startingAssets, + outputs: expectedOutputs, + ); + }); + + test('can enable DDC canary features', () async { + final builder = DevCompilerBuilder( + platform: ddcPlatform, + canaryFeatures: true, + ddcLibraryBundle: true, + ); + final expectedOutputs = Map.of(startingExpectedOutputs)..addAll({ + 'a|lib/a$jsModuleExtension': decodedMatches(contains('canary')), + 'a|lib/a$jsSourceMapExtension': isNotNull, + 'a|lib/a$metadataExtension': isNotNull, + 'a|web/index$jsModuleExtension': isNotNull, + 'a|web/index$jsSourceMapExtension': isNotNull, + 'a|web/index$metadataExtension': isNotNull, + 'b|lib/b$jsModuleExtension': isNotNull, + 'b|lib/b$jsSourceMapExtension': isNotNull, + 'b|lib/b$metadataExtension': isNotNull, + }); + await testBuilders( + [...startingBuilders, builder], + startingAssets, + outputs: expectedOutputs, + ); + }); + + test( + 'does not enable DDC canary features by default', + () async { + final builder = DevCompilerBuilder( + platform: ddcPlatform, + ddcLibraryBundle: true, + ); + final expectedOutputs = Map.of(startingExpectedOutputs)..addAll({ + 'a|lib/a$jsModuleExtension': decodedMatches( + isNot(contains('canary')), + ), + 'a|lib/a$jsSourceMapExtension': isNotNull, + 'a|lib/a$metadataExtension': isNotNull, + 'a|web/index$jsModuleExtension': isNotNull, + 'a|web/index$jsSourceMapExtension': isNotNull, + 'a|web/index$metadataExtension': isNotNull, + 'b|lib/b$jsModuleExtension': isNotNull, + 'b|lib/b$jsSourceMapExtension': isNotNull, + 'b|lib/b$metadataExtension': isNotNull, + }); + await testBuilders( + [...startingBuilders, builder], + startingAssets, + outputs: expectedOutputs, + ); + }, + skip: + 'Enable this test when the library bundle module system is no ' + 'longer locked behind the --canary flag', + ); + + test('generates full dill when enabled', () async { + final builder = DevCompilerBuilder( + platform: ddcPlatform, + generateFullDill: true, + ddcLibraryBundle: true, + ); + final expectedOutputs = Map.of(startingExpectedOutputs)..addAll({ + 'a|lib/a$fullKernelExtension': isNotNull, + 'a|lib/a$jsModuleExtension': isNotNull, + 'a|lib/a$jsSourceMapExtension': isNotNull, + 'a|lib/a$metadataExtension': isNotNull, + 'a|web/index$fullKernelExtension': isNotNull, + 'a|web/index$jsModuleExtension': isNotNull, + 'a|web/index$jsSourceMapExtension': isNotNull, + 'a|web/index$metadataExtension': isNotNull, + 'b|lib/b$fullKernelExtension': isNotNull, + 'b|lib/b$jsModuleExtension': isNotNull, + 'b|lib/b$jsSourceMapExtension': isNotNull, + 'b|lib/b$metadataExtension': isNotNull, + }); + await testBuilders( + [...startingBuilders, builder], + startingAssets, + outputs: expectedOutputs, + ); + }); + + test('does not generate full dill by default', () async { + final builder = DevCompilerBuilder( + platform: ddcPlatform, + ddcLibraryBundle: true, + ); + final expectedOutputs = Map.of(startingExpectedOutputs)..addAll({ + 'a|lib/a$jsModuleExtension': isNotNull, + 'a|lib/a$jsSourceMapExtension': isNotNull, + 'a|lib/a$metadataExtension': isNotNull, + 'a|web/index$jsModuleExtension': isNotNull, + 'a|web/index$jsSourceMapExtension': isNotNull, + 'a|web/index$metadataExtension': isNotNull, + 'b|lib/b$jsModuleExtension': isNotNull, + 'b|lib/b$jsSourceMapExtension': isNotNull, + 'b|lib/b$metadataExtension': isNotNull, + }); + await testBuilders( + [...startingBuilders, builder], + startingAssets, + outputs: expectedOutputs, + ); + }); + + test('emits debug symbols when enabled', () async { + final builder = DevCompilerBuilder( + platform: ddcPlatform, + emitDebugSymbols: true, + ddcLibraryBundle: true, + ); + final expectedOutputs = Map.of(startingExpectedOutputs)..addAll({ + 'a|lib/a$jsModuleExtension': isNotNull, + 'a|lib/a$jsSourceMapExtension': isNotNull, + 'a|lib/a$metadataExtension': isNotNull, + 'a|lib/a$symbolsExtension': isNotNull, + 'a|web/index$jsModuleExtension': isNotNull, + 'a|web/index$jsSourceMapExtension': isNotNull, + 'a|web/index$metadataExtension': isNotNull, + 'a|web/index$symbolsExtension': isNotNull, + 'b|lib/b$jsModuleExtension': isNotNull, + 'b|lib/b$jsSourceMapExtension': isNotNull, + 'b|lib/b$metadataExtension': isNotNull, + 'b|lib/b$symbolsExtension': isNotNull, + }); + await testBuilders( + [...startingBuilders, builder], + startingAssets, + outputs: expectedOutputs, + ); + }); + + test('does not emit debug symbols by default', () async { + final builder = DevCompilerBuilder( + platform: ddcPlatform, + ddcLibraryBundle: true, + ); + final expectedOutputs = Map.of(startingExpectedOutputs)..addAll({ + 'b|lib/b$jsModuleExtension': isNotNull, + 'b|lib/b$jsSourceMapExtension': isNotNull, + 'b|lib/b$metadataExtension': isNotNull, + 'a|lib/a$jsModuleExtension': isNotNull, + 'a|lib/a$jsSourceMapExtension': isNotNull, + 'a|lib/a$metadataExtension': isNotNull, + 'a|web/index$jsModuleExtension': isNotNull, + 'a|web/index$jsSourceMapExtension': isNotNull, + 'a|web/index$metadataExtension': isNotNull, + }); + await testBuilders( + [...startingBuilders, builder], + startingAssets, + outputs: expectedOutputs, + ); + }); + + test('strips scratch paths from metadata', () async { + final builder = DevCompilerBuilder( + platform: ddcPlatform, + ddcLibraryBundle: true, + ); + final expectedOutputs = Map.of(startingExpectedOutputs)..addAll({ + 'a|lib/a$jsModuleExtension': isNotNull, + 'a|lib/a$jsSourceMapExtension': isNotNull, + 'a|lib/a$metadataExtension': decodedMatches( + isNot(contains('scratch')), + ), + 'a|web/index$jsModuleExtension': isNotNull, + 'a|web/index$jsSourceMapExtension': isNotNull, + 'a|web/index$metadataExtension': decodedMatches( + isNot(contains('scratch')), + ), + 'b|lib/b$jsModuleExtension': isNotNull, + 'b|lib/b$jsSourceMapExtension': isNotNull, + 'b|lib/b$metadataExtension': decodedMatches( + isNot(contains('scratch')), + ), + }); + await testBuilders( + [...startingBuilders, builder], + startingAssets, + outputs: expectedOutputs, + ); + }); + }); + + group('projects with errors due to', () { + group('invalid assignements', () { + test('reports useful messages', () async { + final assets = { + 'a|web/index.dart': 'int x = "hello";', + 'build_modules|lib/src/analysis_options.default.yaml': '', + }; + final expectedOutputs = { + 'a|lib/.ddc.meta_module.clean': isNotNull, + 'a|lib/.ddc.meta_module.raw': isNotNull, + 'a|web/index.ddc.dill': isNotNull, + 'a|web/index.ddc.module': isNotNull, + 'a|web/index$jsModuleErrorsExtension': decodedMatches( + allOf(contains('String'), contains('assigned'), contains('int')), + ), + 'a|web/index.module.library': isNotNull, + 'build_modules|lib/.ddc.meta_module.clean': isNotNull, + 'build_modules|lib/.ddc.meta_module.raw': isNotNull, + }; + final logs = []; + await testBuilders( + [ + const ModuleLibraryBuilder(), + MetaModuleBuilder(ddcPlatform), + MetaModuleCleanBuilder(ddcPlatform), + ModuleBuilder(ddcPlatform), + ddcKernelBuilder(builderOptions), + DevCompilerBuilder(platform: ddcPlatform, ddcLibraryBundle: true), + ], + assets, + outputs: expectedOutputs, + onLog: logs.add, + ); + expect( + logs, + contains( + predicate( + (record) => + record.level == Level.SEVERE && + record.message.contains('String') && + record.message.contains('assigned') && + record.message.contains('int'), + ), + ), + ); + }); + }); + + group('invalid imports', () { + test('reports useful messages', () async { + final assets = { + 'a|web/index.dart': "import 'package:a/a.dart'", + 'build_modules|lib/src/analysis_options.default.yaml': '', + }; + final expectedOutputs = { + 'a|lib/.ddc.meta_module.clean': isNotNull, + 'a|lib/.ddc.meta_module.raw': isNotNull, + 'a|web/index$jsModuleErrorsExtension': decodedMatches( + contains('Unable to find modules for some sources'), + ), + 'a|web/index.ddc.module': isNotNull, + 'a|web/index.module.library': isNotNull, + 'build_modules|lib/.ddc.meta_module.clean': isNotNull, + 'build_modules|lib/.ddc.meta_module.raw': isNotNull, + }; + final logs = []; + await testBuilders( + [ + const ModuleLibraryBuilder(), + MetaModuleBuilder(ddcPlatform), + MetaModuleCleanBuilder(ddcPlatform), + ModuleBuilder(ddcPlatform), + ddcKernelBuilder(builderOptions), + DevCompilerBuilder(platform: ddcPlatform, ddcLibraryBundle: true), + ], + assets, + outputs: expectedOutputs, + onLog: logs.add, + ); + expect( + logs, + contains( + predicate( + (record) => + record.level == Level.SEVERE && + record.message.contains( + 'Unable to find modules for some sources', + ), + ), + ), + ); + }); + }); + }); + }); +}