From 79446027593d7f09ce74ae8f2e0632b4a5c94462 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 22 Aug 2024 00:32:17 +0200 Subject: [PATCH 01/14] Compile with dart2js and dart2wasm --- build_web_compilers/build.yaml | 2 +- .../lib/src/dart2js_bootstrap.dart | 22 ++++++++-- .../lib/src/dart2wasm_bootstrap.dart | 26 ++++++++---- build_web_compilers/lib/src/loader.js | 40 +++++++++++++++++++ build_web_compilers/lib/src/loader.min.js | 1 + .../lib/src/web_entrypoint_builder.dart | 29 +++++++++++++- 6 files changed, 106 insertions(+), 14 deletions(-) create mode 100644 build_web_compilers/lib/src/loader.js create mode 100644 build_web_compilers/lib/src/loader.min.js diff --git a/build_web_compilers/build.yaml b/build_web_compilers/build.yaml index a83675841..e9fe8bc41 100644 --- a/build_web_compilers/build.yaml +++ b/build_web_compilers/build.yaml @@ -141,7 +141,7 @@ builders: dart2js_args: - --enable-asserts release_options: - compiler: dart2js + compiler: both applies_builders: - build_web_compilers:dart2js_archive_extractor _stack_trace_mapper_copy: diff --git a/build_web_compilers/lib/src/dart2js_bootstrap.dart b/build_web_compilers/lib/src/dart2js_bootstrap.dart index 4c0b1eba4..8aee771d3 100644 --- a/build_web_compilers/lib/src/dart2js_bootstrap.dart +++ b/build_web_compilers/lib/src/dart2js_bootstrap.dart @@ -21,23 +21,37 @@ import 'platforms.dart'; import 'web_entrypoint_builder.dart'; /// Compiles an the primary input of [buildStep] with dart2js. +/// +/// When [generateEntrypoint] is `false` (it defaults to `true`), the generated +/// asset is named `.dart.bootstrap.js` instead of `.dart.js`. This is used by +/// the entrypoint builder to invoke both the dart2js and the dart2wasm +/// compilers and generate a custom entrypoint script detecting supported +/// browser features to load the relevant script. Future bootstrapDart2Js( BuildStep buildStep, List dart2JsArgs, { required bool? nativeNullAssertions, + bool generateEntrypoint = true, }) => - _resourcePool.withResource(() => _bootstrapDart2Js(buildStep, dart2JsArgs, - nativeNullAssertions: nativeNullAssertions)); + _resourcePool.withResource(() => _bootstrapDart2Js( + buildStep, + dart2JsArgs, + nativeNullAssertions: nativeNullAssertions, + generateEntrypoint: generateEntrypoint, + )); Future _bootstrapDart2Js( BuildStep buildStep, List dart2JsArgs, { required bool? nativeNullAssertions, + required bool generateEntrypoint, }) async { var dartEntrypointId = buildStep.inputId; var moduleId = dartEntrypointId.changeExtension(moduleExtension(dart2jsPlatform)); var args = []; + var outputExtension = + generateEntrypoint ? jsEntrypointExtension : ddcBootstrapExtension; { var module = Module.fromJson( json.decode(await buildStep.readAsString(moduleId)) @@ -74,7 +88,7 @@ https://github.com/dart-lang/build/blob/master/docs/faq.md#how-can-i-resolve-ski var jsOutputPath = p.withoutExtension(dartUri.scheme == 'package' ? 'packages/${dartUri.path}' : dartUri.path.substring(1)) + - jsEntrypointExtension; + outputExtension; var librariesSpec = p.joinAll([sdkDir, 'lib', 'libraries.json']); _validateUserArgs(dart2JsArgs); args = dart2JsArgs.toList() @@ -102,7 +116,7 @@ https://github.com/dart-lang/build/blob/master/docs/faq.md#how-can-i-resolve-ski ...args, ], workingDirectory: scratchSpace.tempDir.path); - var jsOutputId = dartEntrypointId.changeExtension(jsEntrypointExtension); + var jsOutputId = dartEntrypointId.changeExtension(outputExtension); var jsOutputFile = scratchSpace.fileFor(jsOutputId); if (result.exitCode == 0 && await jsOutputFile.exists()) { log.info('${result.stdout}\n${result.stderr}'); diff --git a/build_web_compilers/lib/src/dart2wasm_bootstrap.dart b/build_web_compilers/lib/src/dart2wasm_bootstrap.dart index 7f32202b3..3f8615282 100644 --- a/build_web_compilers/lib/src/dart2wasm_bootstrap.dart +++ b/build_web_compilers/lib/src/dart2wasm_bootstrap.dart @@ -19,15 +19,25 @@ final _resourcePool = Pool(maxWorkersPerTask); /// Invokes `dart compile wasm` to compile the primary input of [buildStep]. /// -/// Additionally, generates a `.js` entrypoint file invoking the entrypoint. +/// Additionally, generates a `.js` entrypoint file invoking the entrypoint +/// if [generateEntrypoint] is enabled. We generate a wasm-only entrypoint +/// (reflected by [generateEntrypoint] being enabled) when dart2wasm is the +/// only compiler. Otherwise, we generate a custom entrypoint loading either the +/// dart2js or dart2wasm-compiled program. Future bootstrapDart2Wasm( - BuildStep buildStep, List additionalArguments) async { - await _resourcePool - .withResource(() => _bootstrapDart2Wasm(buildStep, additionalArguments)); + BuildStep buildStep, + List additionalArguments, + bool generateEntrypoint, +) async { + await _resourcePool.withResource(() => + _bootstrapDart2Wasm(buildStep, additionalArguments, generateEntrypoint)); } Future _bootstrapDart2Wasm( - BuildStep buildStep, List additionalArguments) async { + BuildStep buildStep, + List additionalArguments, + bool generateEntrypoint, +) async { var dartEntrypointId = buildStep.inputId; var moduleId = dartEntrypointId.changeExtension(moduleExtension(dart2wasmPlatform)); @@ -105,8 +115,10 @@ https://github.com/dart-lang/build/blob/master/docs/faq.md#how-can-i-resolve-ski dartEntrypointId.changeExtension(extension), buildStep); } - final entrypoint = _entrypointRunner(buildStep.inputId); - await buildStep.writeAsString(entrypoint.$1, entrypoint.$2); + if (generateEntrypoint) { + final entrypoint = _entrypointRunner(buildStep.inputId); + await buildStep.writeAsString(entrypoint.$1, entrypoint.$2); + } } else { log.severe('ExitCode:${result.exitCode}\nStdOut:\n${result.stdout}\n' 'StdErr:\n${result.stderr}'); diff --git a/build_web_compilers/lib/src/loader.js b/build_web_compilers/lib/src/loader.js new file mode 100644 index 000000000..cdcd86871 --- /dev/null +++ b/build_web_compilers/lib/src/loader.js @@ -0,0 +1,40 @@ +// Copyright (c) 2024, 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. + +// For apps compiled both with dart2js and dart2wasm, this entrypoint script +// is responsible for detecting browser features and then loading either the +// wasm or the JS bundle depending on which features are supported. +// +// Note: build_web_compiler loads a minified version of this file. To re-generate +// it, run: +// +// esbuild lib/src/loader.js --bundle "--external:*.mjs" --format=esm --minify > lib/src/loader.min.js + +function supportsWasmGC() { + // This attempts to instantiate a wasm module that only will validate if the + // final WasmGC spec is implemented in the browser. + // + // Copied from https://github.com/GoogleChromeLabs/wasm-feature-detect/blob/main/src/detectors/gc/index.js + const bytes = [0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 95, 1, 120, 0]; + return WebAssembly && WebAssembly.validate(new Uint8Array(bytes)); +} + +function resolveUrlWithSegments(...segments) { + return new URL(joinPathSegments(...segments), document.baseURI).toString() +} + +(async () => { + if (supportsWasmGC()) { + let { instantiate, invoke } = await import("./$basename.mjs"); + + let modulePromise = WebAssembly.compileStreaming(fetch("$basename.wasm")); + let instantiated = await instantiate(modulePromise, {}); + invoke(instantiated, []); + } else { + const scriptTag = document.createElement("script"); + scriptTag.type = "application/javascript"; + scriptTag.src = resolveUrlWithSegments("./$basename.bootstrap.js"); + document.head.append(scriptTag); + } +})(); diff --git a/build_web_compilers/lib/src/loader.min.js b/build_web_compilers/lib/src/loader.min.js new file mode 100644 index 000000000..707bc7012 --- /dev/null +++ b/build_web_compilers/lib/src/loader.min.js @@ -0,0 +1 @@ +function s(){let e=[0,97,115,109,1,0,0,0,1,5,1,95,1,120,0];return WebAssembly&&WebAssembly.validate(new Uint8Array(e))}function i(...e){return new URL(joinPathSegments(...e),document.baseURI).toString()}(async()=>{if(s()){let{instantiate:e,invoke:t}=await import("./$basename.mjs"),a=WebAssembly.compileStreaming(fetch("$basename.wasm")),n=await e(a,{});t(n,[])}else{let e=document.createElement("script");e.type="application/javascript",e.src=i("./$basename.bootstrap.js"),document.head.append(e)}})(); diff --git a/build_web_compilers/lib/src/web_entrypoint_builder.dart b/build_web_compilers/lib/src/web_entrypoint_builder.dart index 671f8b888..bb0793363 100644 --- a/build_web_compilers/lib/src/web_entrypoint_builder.dart +++ b/build_web_compilers/lib/src/web_entrypoint_builder.dart @@ -8,6 +8,7 @@ import 'package:analyzer/dart/analysis/utilities.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:build/build.dart'; import 'package:build_modules/build_modules.dart'; +import 'package:path/path.dart' as p; import 'common.dart'; import 'dart2js_bootstrap.dart'; @@ -30,7 +31,9 @@ enum WebCompiler { // ignore: constant_identifier_names DartDevc('dartdevc'), // ignore: constant_identifier_names - Dart2Wasm('dart2wasm'); + Dart2Wasm('dart2wasm'), + // ignore: constant_identifier_names + Dart2JsAndDart2Wasm('both'); /// The name of this compiler used when identifying it in builder options. final String optionName; @@ -145,7 +148,29 @@ class WebEntrypointBuilder implements Builder { await bootstrapDart2Js(buildStep, dart2JsArgs, nativeNullAssertions: nativeNullAssertions); case WebCompiler.Dart2Wasm: - await bootstrapDart2Wasm(buildStep, dart2WasmArgs); + await bootstrapDart2Wasm(buildStep, dart2WasmArgs, true); + case WebCompiler.Dart2JsAndDart2Wasm: + await Future.wait( + [ + bootstrapDart2Js( + buildStep, + dart2JsArgs, + nativeNullAssertions: nativeNullAssertions, + generateEntrypoint: false, + ), + bootstrapDart2Wasm(buildStep, dart2WasmArgs, false), + ], + ); + final basename = p.url.basenameWithoutExtension(buildStep.inputId.path); + + final entrypointTemplate = await buildStep + .readAsString(AssetId('build_web_compilers', 'lib/src/loader.min.js')); + final entrypoint = + entrypointTemplate.replaceAll(r'$basename', basename); + await buildStep.writeAsString( + buildStep.inputId.changeExtension(jsEntrypointExtension), + entrypoint, + ); } } From a798540cf35fae2de1747d7e867d4b85cc55f298 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 22 Aug 2024 11:43:10 +0200 Subject: [PATCH 02/14] Support custom entrypoints --- build_web_compilers/README.md | 42 ++++++++++++++++++- build_web_compilers/lib/src/loader.js | 6 +-- build_web_compilers/lib/src/loader.min.js | 2 +- .../lib/src/web_entrypoint_builder.dart | 19 +++++++-- 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/build_web_compilers/README.md b/build_web_compilers/README.md index 1606e7d1f..401021116 100644 --- a/build_web_compilers/README.md +++ b/build_web_compilers/README.md @@ -43,7 +43,9 @@ then all you need is the `dev_dependency` listed above. By default, this package uses the [Dart development compiler][] (_dartdevc_, also known as _DDC_) to compile Dart to JavaScript. In release builds (running -the build tool with `--release`, the default compiler is `dart2js`). +the build tool with `--release`, this package uses both `dart2js` and +`dart2wasm` with a custom entrypoint loading the appropriate module depending +on browser features). If you would like to opt into dart2js for all builds, you will need to add a `build.yaml` file, which should look roughly like the following: @@ -80,6 +82,44 @@ targets: - -O2 ``` +### Compiling to WebAssembly and JavaScript + +In addition to either using `dart2wasm` or `dart2js`, this package can also +compile your application with both compilers and emit an entrypoint loader +that will fetch the WebAssembly module or the compiled JavaScript bundle +depending on whether WebAssembly with the GC extension is supported by the +browser. +This feature is enabled by default for release builds, but can also be +requested explicitly by using `compiler: both`. In some setups, for instance +when running on Node.JS, the generated entrypoint script may have to be +customized to add necessary preambles. This is possible with the +`entrypoint_template` option: + +```yaml +targets: + $default: + builders: + build_web_compilers:entrypoint: + options: + compiler: both + entrypoint_template: | + (async () => { + // Check for WasmGC being supported. + const bytes = [0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 95, 1, 120, 0]; + if (WebAssembly && WebAssembly.validate(new Uint8Array(bytes))) { + // Use the mjs loader emitted by dart2wasm + let { instantiate, invoke } = await import("./{{basename}}.mjs"); + + let modulePromise = WebAssembly.compileStreaming(fetch("{{basename}}.wasm")); + let instantiated = await instantiate(modulePromise, {}); + invoke(instantiated, []); + } else { + // Use the dart2js bundle + await import("./{{basename}}.bootstrap.js"); + } + })(); +``` + ### Configuring -D environment variables dartdevc is a modular compiler, so in order to ensure consistent builds diff --git a/build_web_compilers/lib/src/loader.js b/build_web_compilers/lib/src/loader.js index cdcd86871..529ccd540 100644 --- a/build_web_compilers/lib/src/loader.js +++ b/build_web_compilers/lib/src/loader.js @@ -26,15 +26,15 @@ function resolveUrlWithSegments(...segments) { (async () => { if (supportsWasmGC()) { - let { instantiate, invoke } = await import("./$basename.mjs"); + let { instantiate, invoke } = await import("./{{basename}}.mjs"); - let modulePromise = WebAssembly.compileStreaming(fetch("$basename.wasm")); + let modulePromise = WebAssembly.compileStreaming(fetch("{{basename}}.wasm")); let instantiated = await instantiate(modulePromise, {}); invoke(instantiated, []); } else { const scriptTag = document.createElement("script"); scriptTag.type = "application/javascript"; - scriptTag.src = resolveUrlWithSegments("./$basename.bootstrap.js"); + scriptTag.src = resolveUrlWithSegments("./{{basename}}.bootstrap.js"); document.head.append(scriptTag); } })(); diff --git a/build_web_compilers/lib/src/loader.min.js b/build_web_compilers/lib/src/loader.min.js index 707bc7012..0fac3f876 100644 --- a/build_web_compilers/lib/src/loader.min.js +++ b/build_web_compilers/lib/src/loader.min.js @@ -1 +1 @@ -function s(){let e=[0,97,115,109,1,0,0,0,1,5,1,95,1,120,0];return WebAssembly&&WebAssembly.validate(new Uint8Array(e))}function i(...e){return new URL(joinPathSegments(...e),document.baseURI).toString()}(async()=>{if(s()){let{instantiate:e,invoke:t}=await import("./$basename.mjs"),a=WebAssembly.compileStreaming(fetch("$basename.wasm")),n=await e(a,{});t(n,[])}else{let e=document.createElement("script");e.type="application/javascript",e.src=i("./$basename.bootstrap.js"),document.head.append(e)}})(); +function s(){let e=[0,97,115,109,1,0,0,0,1,5,1,95,1,120,0];return WebAssembly&&WebAssembly.validate(new Uint8Array(e))}function i(...e){return new URL(joinPathSegments(...e),document.baseURI).toString()}(async()=>{if(s()){let{instantiate:e,invoke:t}=await import("./{{basename}}.mjs"),a=WebAssembly.compileStreaming(fetch("{{basename}}.wasm")),n=await e(a,{});t(n,[])}else{let e=document.createElement("script");e.type="application/javascript",e.src=i("./{{basename}}.bootstrap.js"),document.head.append(e)}})(); diff --git a/build_web_compilers/lib/src/web_entrypoint_builder.dart b/build_web_compilers/lib/src/web_entrypoint_builder.dart index bb0793363..550f8e2c2 100644 --- a/build_web_compilers/lib/src/web_entrypoint_builder.dart +++ b/build_web_compilers/lib/src/web_entrypoint_builder.dart @@ -64,12 +64,14 @@ const _supportedOptions = [ _dart2jsArgsOption, _nativeNullAssertionsOption, _dart2wasmArgsOption, + _entrypointTemplateOption, ]; const _compilerOption = 'compiler'; const _dart2jsArgsOption = 'dart2js_args'; const _dart2wasmArgsOption = 'dart2wasm_args'; const _nativeNullAssertionsOption = 'native_null_assertions'; +const _entrypointTemplateOption = 'entrypoint_template'; /// The deprecated keys for the `options` config for the [WebEntrypointBuilder]. const _deprecatedOptions = [ @@ -92,11 +94,20 @@ class WebEntrypointBuilder implements Builder { /// will be used. final bool? nativeNullAssertions; + /// The template to use for the entrypoint script when compiling with both + /// dart2wasm and dart2js. + /// + /// When no template is given, we default to the `loader.min.js` file in this + /// package. In the template, `{{ basename }}` is replaced with the basename + /// of the Dart entrypoint. + final String? entrypointTemplate; + const WebEntrypointBuilder( this.webCompiler, { this.dart2JsArgs = const [], this.dart2WasmArgs = const [], required this.nativeNullAssertions, + this.entrypointTemplate, }); factory WebEntrypointBuilder.fromOptions(BuilderOptions options) { @@ -113,6 +124,7 @@ class WebEntrypointBuilder implements Builder { dart2WasmArgs: _parseCompilerOptions(options, _dart2wasmArgsOption), nativeNullAssertions: options.config[_nativeNullAssertionsOption] as bool?, + entrypointTemplate: options.config[_entrypointTemplateOption] as String?, ); } @@ -163,10 +175,11 @@ class WebEntrypointBuilder implements Builder { ); final basename = p.url.basenameWithoutExtension(buildStep.inputId.path); - final entrypointTemplate = await buildStep - .readAsString(AssetId('build_web_compilers', 'lib/src/loader.min.js')); + final entrypointTemplate = this.entrypointTemplate ?? + await buildStep.readAsString( + AssetId('build_web_compilers', 'lib/src/loader.min.js')); final entrypoint = - entrypointTemplate.replaceAll(r'$basename', basename); + entrypointTemplate.replaceAll(r'{{basename}}', basename); await buildStep.writeAsString( buildStep.inputId.changeExtension(jsEntrypointExtension), entrypoint, From b88e2e8d1c0adbd94d6017e2baf2a2f626d087cd Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 4 Sep 2024 21:46:44 +0200 Subject: [PATCH 03/14] Rename to dart2wasm+dart2js --- _test/test/goldens/generated_build_script.dart | 4 ++-- build_web_compilers/README.md | 8 ++++---- build_web_compilers/build.yaml | 2 +- build_web_compilers/lib/src/web_entrypoint_builder.dart | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/_test/test/goldens/generated_build_script.dart b/_test/test/goldens/generated_build_script.dart index d3aa8ac77..28b5921ce 100644 --- a/_test/test/goldens/generated_build_script.dart +++ b/_test/test/goldens/generated_build_script.dart @@ -140,8 +140,8 @@ final _builders = <_i1.BuilderApplication>[ r'dart2wasm_args': [r'--enable-asserts'], r'dart2js_args': [r'--enable-asserts'], }), - defaultReleaseOptions: - const _i7.BuilderOptions({r'compiler': r'dart2js'}), + defaultReleaseOptions: const _i7.BuilderOptions( + {r'compiler': r'dart2wasm+dart2js'}), appliesBuilders: const [r'build_web_compilers:dart2js_archive_extractor'], ), _i1.apply( diff --git a/build_web_compilers/README.md b/build_web_compilers/README.md index 401021116..779bdce0f 100644 --- a/build_web_compilers/README.md +++ b/build_web_compilers/README.md @@ -90,9 +90,9 @@ that will fetch the WebAssembly module or the compiled JavaScript bundle depending on whether WebAssembly with the GC extension is supported by the browser. This feature is enabled by default for release builds, but can also be -requested explicitly by using `compiler: both`. In some setups, for instance -when running on Node.JS, the generated entrypoint script may have to be -customized to add necessary preambles. This is possible with the +requested explicitly by using `compiler: dart2wasm+dart2js`. In some setups, +for instance when running on Node.JS, the generated entrypoint script may have +to be customized to add necessary preambles. This is possible with the `entrypoint_template` option: ```yaml @@ -101,7 +101,7 @@ targets: builders: build_web_compilers:entrypoint: options: - compiler: both + compiler: dart2wasm+dart2js entrypoint_template: | (async () => { // Check for WasmGC being supported. diff --git a/build_web_compilers/build.yaml b/build_web_compilers/build.yaml index e9fe8bc41..ff918b244 100644 --- a/build_web_compilers/build.yaml +++ b/build_web_compilers/build.yaml @@ -141,7 +141,7 @@ builders: dart2js_args: - --enable-asserts release_options: - compiler: both + compiler: dart2wasm+dart2js applies_builders: - build_web_compilers:dart2js_archive_extractor _stack_trace_mapper_copy: diff --git a/build_web_compilers/lib/src/web_entrypoint_builder.dart b/build_web_compilers/lib/src/web_entrypoint_builder.dart index 550f8e2c2..082353420 100644 --- a/build_web_compilers/lib/src/web_entrypoint_builder.dart +++ b/build_web_compilers/lib/src/web_entrypoint_builder.dart @@ -33,7 +33,7 @@ enum WebCompiler { // ignore: constant_identifier_names Dart2Wasm('dart2wasm'), // ignore: constant_identifier_names - Dart2JsAndDart2Wasm('both'); + Dart2JsAndDart2Wasm('dart2wasm+dart2js'); /// The name of this compiler used when identifying it in builder options. final String optionName; From 6068c878aa9cb8669de357c78af734e16f378b45 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 5 Sep 2024 20:58:03 +0200 Subject: [PATCH 04/14] Support multiple compilers --- .../test/goldens/generated_build_script.dart | 20 +- build_web_compilers/build.yaml | 19 +- .../lib/src/dart2js_bootstrap.dart | 20 +- .../lib/src/dart2wasm_bootstrap.dart | 49 +-- .../lib/src/dev_compiler_bootstrap.dart | 3 +- build_web_compilers/lib/src/loader.js | 40 -- build_web_compilers/lib/src/loader.min.js | 1 - .../lib/src/web_entrypoint_builder.dart | 399 ++++++++++++------ .../test/dart2js_bootstrap_test.dart | 34 +- .../test/dart2wasm_bootstrap_test.dart | 100 +++++ .../test/dev_compiler_bootstrap_test.dart | 34 +- .../test/entrypoint_options_test.dart | 86 ++++ 12 files changed, 566 insertions(+), 239 deletions(-) delete mode 100644 build_web_compilers/lib/src/loader.js delete mode 100644 build_web_compilers/lib/src/loader.min.js create mode 100644 build_web_compilers/test/dart2wasm_bootstrap_test.dart create mode 100644 build_web_compilers/test/entrypoint_options_test.dart diff --git a/_test/test/goldens/generated_build_script.dart b/_test/test/goldens/generated_build_script.dart index 28b5921ce..9ce7b01bd 100644 --- a/_test/test/goldens/generated_build_script.dart +++ b/_test/test/goldens/generated_build_script.dart @@ -133,15 +133,21 @@ final _builders = <_i1.BuilderApplication>[ r'test/**.vm_test.dart', ], ), - defaultOptions: const _i7.BuilderOptions({ - r'dart2js_args': [r'--minify'] - }), defaultDevOptions: const _i7.BuilderOptions({ - r'dart2wasm_args': [r'--enable-asserts'], - r'dart2js_args': [r'--enable-asserts'], + r'compilers': { + r'dartdevc': {r'extension': r'.dart.js'} + } + }), + defaultReleaseOptions: const _i7.BuilderOptions({ + r'compilers': { + r'dart2js': { + r'extension': r'.dart2js.js', + r'args': [r'--minify'], + }, + r'dart2wasm': {r'extension': r'.dart2wasm.mjs'}, + }, + r'loader': r'.dart.js', }), - defaultReleaseOptions: const _i7.BuilderOptions( - {r'compiler': r'dart2wasm+dart2js'}), appliesBuilders: const [r'build_web_compilers:dart2js_archive_extractor'], ), _i1.apply( diff --git a/build_web_compilers/build.yaml b/build_web_compilers/build.yaml index ff918b244..a89b7e4d7 100644 --- a/build_web_compilers/build.yaml +++ b/build_web_compilers/build.yaml @@ -132,16 +132,19 @@ builders: exclude: - test/**.node_test.dart - test/**.vm_test.dart - options: - dart2js_args: - - --minify dev_options: - dart2wasm_args: - - --enable-asserts - dart2js_args: - - --enable-asserts + compilers: + dartdevc: + extension: '.dart.js' release_options: - compiler: dart2wasm+dart2js + compilers: + dart2js: + extension: '.dart2js.js' + args: + - --minify + dart2wasm: + extension: '.dart2wasm.mjs' + loader: '.dart.js' applies_builders: - build_web_compilers:dart2js_archive_extractor _stack_trace_mapper_copy: diff --git a/build_web_compilers/lib/src/dart2js_bootstrap.dart b/build_web_compilers/lib/src/dart2js_bootstrap.dart index 8aee771d3..b37e76080 100644 --- a/build_web_compilers/lib/src/dart2js_bootstrap.dart +++ b/build_web_compilers/lib/src/dart2js_bootstrap.dart @@ -21,37 +21,29 @@ import 'platforms.dart'; import 'web_entrypoint_builder.dart'; /// Compiles an the primary input of [buildStep] with dart2js. -/// -/// When [generateEntrypoint] is `false` (it defaults to `true`), the generated -/// asset is named `.dart.bootstrap.js` instead of `.dart.js`. This is used by -/// the entrypoint builder to invoke both the dart2js and the dart2wasm -/// compilers and generate a custom entrypoint script detecting supported -/// browser features to load the relevant script. Future bootstrapDart2Js( BuildStep buildStep, List dart2JsArgs, { required bool? nativeNullAssertions, - bool generateEntrypoint = true, + String entrypointExtension = jsEntrypointExtension, }) => _resourcePool.withResource(() => _bootstrapDart2Js( buildStep, dart2JsArgs, nativeNullAssertions: nativeNullAssertions, - generateEntrypoint: generateEntrypoint, + entrypointExtension: entrypointExtension, )); Future _bootstrapDart2Js( BuildStep buildStep, List dart2JsArgs, { required bool? nativeNullAssertions, - required bool generateEntrypoint, + required String entrypointExtension, }) async { var dartEntrypointId = buildStep.inputId; var moduleId = dartEntrypointId.changeExtension(moduleExtension(dart2jsPlatform)); var args = []; - var outputExtension = - generateEntrypoint ? jsEntrypointExtension : ddcBootstrapExtension; { var module = Module.fromJson( json.decode(await buildStep.readAsString(moduleId)) @@ -88,7 +80,7 @@ https://github.com/dart-lang/build/blob/master/docs/faq.md#how-can-i-resolve-ski var jsOutputPath = p.withoutExtension(dartUri.scheme == 'package' ? 'packages/${dartUri.path}' : dartUri.path.substring(1)) + - outputExtension; + entrypointExtension; var librariesSpec = p.joinAll([sdkDir, 'lib', 'libraries.json']); _validateUserArgs(dart2JsArgs); args = dart2JsArgs.toList() @@ -116,7 +108,7 @@ https://github.com/dart-lang/build/blob/master/docs/faq.md#how-can-i-resolve-ski ...args, ], workingDirectory: scratchSpace.tempDir.path); - var jsOutputId = dartEntrypointId.changeExtension(outputExtension); + var jsOutputId = dartEntrypointId.changeExtension(entrypointExtension); var jsOutputFile = scratchSpace.fileFor(jsOutputId); if (result.exitCode == 0 && await jsOutputFile.exists()) { log.info('${result.stdout}\n${result.stderr}'); @@ -125,7 +117,7 @@ https://github.com/dart-lang/build/blob/master/docs/faq.md#how-can-i-resolve-ski var fileGlob = Glob('$dartFile.js*'); var archive = Archive(); await for (var jsFile in fileGlob.list(root: rootDir)) { - if (jsFile.path.endsWith(jsEntrypointExtension) || + if (jsFile.path.endsWith(entrypointExtension) || jsFile.path.endsWith(jsEntrypointSourceMapExtension)) { // These are explicitly output, and are not part of the archive. continue; diff --git a/build_web_compilers/lib/src/dart2wasm_bootstrap.dart b/build_web_compilers/lib/src/dart2wasm_bootstrap.dart index 3f8615282..51eb99bad 100644 --- a/build_web_compilers/lib/src/dart2wasm_bootstrap.dart +++ b/build_web_compilers/lib/src/dart2wasm_bootstrap.dart @@ -19,24 +19,21 @@ final _resourcePool = Pool(maxWorkersPerTask); /// Invokes `dart compile wasm` to compile the primary input of [buildStep]. /// -/// Additionally, generates a `.js` entrypoint file invoking the entrypoint -/// if [generateEntrypoint] is enabled. We generate a wasm-only entrypoint -/// (reflected by [generateEntrypoint] being enabled) when dart2wasm is the -/// only compiler. Otherwise, we generate a custom entrypoint loading either the -/// dart2js or dart2wasm-compiled program. +/// This only emits the `.wasm` and `.mjs` files produced by `dart2wasm`. An +/// entrypoint loader needs to be emitted separately. Future bootstrapDart2Wasm( BuildStep buildStep, List additionalArguments, - bool generateEntrypoint, + String javaScriptModuleExtension, ) async { - await _resourcePool.withResource(() => - _bootstrapDart2Wasm(buildStep, additionalArguments, generateEntrypoint)); + await _resourcePool.withResource(() => _bootstrapDart2Wasm( + buildStep, additionalArguments, javaScriptModuleExtension)); } Future _bootstrapDart2Wasm( BuildStep buildStep, List additionalArguments, - bool generateEntrypoint, + String javaScriptModuleExtension, ) async { var dartEntrypointId = buildStep.inputId; var moduleId = @@ -110,35 +107,17 @@ https://github.com/dart-lang/build/blob/master/docs/faq.md#how-can-i-resolve-ski if (result.exitCode == 0 && await wasmOutputFile.exists()) { log.info('${result.stdout}\n${result.stderr}'); - for (final extension in [wasmExtension, moduleJsExtension]) { - await scratchSpace.copyOutput( - dartEntrypointId.changeExtension(extension), buildStep); - } + await scratchSpace.copyOutput( + dartEntrypointId.changeExtension(wasmExtension), buildStep); - if (generateEntrypoint) { - final entrypoint = _entrypointRunner(buildStep.inputId); - await buildStep.writeAsString(entrypoint.$1, entrypoint.$2); - } + final loaderContents = await scratchSpace + .fileFor(dartEntrypointId.changeExtension(moduleJsExtension)) + .readAsBytes(); + await buildStep.writeAsBytes( + dartEntrypointId.changeExtension(javaScriptModuleExtension), + loaderContents); } else { log.severe('ExitCode:${result.exitCode}\nStdOut:\n${result.stdout}\n' 'StdErr:\n${result.stderr}'); } } - -(AssetId, String) _entrypointRunner(AssetId wasmSource) { - final id = wasmSource.changeExtension('.dart.js'); - final basename = p.url.basenameWithoutExtension(wasmSource.path); - - return ( - id, - ''' -(async () => { - let { instantiate, invoke } = await import("./$basename.mjs"); - - let modulePromise = WebAssembly.compileStreaming(fetch("$basename.wasm")); - let instantiated = await instantiate(modulePromise, {}); - invoke(instantiated, []); -})(); -''' - ); -} diff --git a/build_web_compilers/lib/src/dev_compiler_bootstrap.dart b/build_web_compilers/lib/src/dev_compiler_bootstrap.dart index 2e2b8dfb4..a0223c32f 100644 --- a/build_web_compilers/lib/src/dev_compiler_bootstrap.dart +++ b/build_web_compilers/lib/src/dev_compiler_bootstrap.dart @@ -31,6 +31,7 @@ Future bootstrapDdc( BuildStep buildStep, { DartPlatform? platform, Iterable requiredAssets = const [], + String entrypointExtension = jsEntrypointExtension, required bool? nativeNullAssertions, }) async { platform = ddcPlatform; @@ -132,7 +133,7 @@ https://github.com/dart-lang/build/blob/master/docs/faq.md#how-can-i-resolve-ski var entrypointJsContent = _entryPointJs(bootstrapModuleName); await buildStep.writeAsString( - dartEntrypointId.changeExtension(jsEntrypointExtension), + dartEntrypointId.changeExtension(entrypointExtension), entrypointJsContent); // Output the digests and merged_metadata for transitive modules. diff --git a/build_web_compilers/lib/src/loader.js b/build_web_compilers/lib/src/loader.js deleted file mode 100644 index 529ccd540..000000000 --- a/build_web_compilers/lib/src/loader.js +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2024, 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. - -// For apps compiled both with dart2js and dart2wasm, this entrypoint script -// is responsible for detecting browser features and then loading either the -// wasm or the JS bundle depending on which features are supported. -// -// Note: build_web_compiler loads a minified version of this file. To re-generate -// it, run: -// -// esbuild lib/src/loader.js --bundle "--external:*.mjs" --format=esm --minify > lib/src/loader.min.js - -function supportsWasmGC() { - // This attempts to instantiate a wasm module that only will validate if the - // final WasmGC spec is implemented in the browser. - // - // Copied from https://github.com/GoogleChromeLabs/wasm-feature-detect/blob/main/src/detectors/gc/index.js - const bytes = [0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 95, 1, 120, 0]; - return WebAssembly && WebAssembly.validate(new Uint8Array(bytes)); -} - -function resolveUrlWithSegments(...segments) { - return new URL(joinPathSegments(...segments), document.baseURI).toString() -} - -(async () => { - if (supportsWasmGC()) { - let { instantiate, invoke } = await import("./{{basename}}.mjs"); - - let modulePromise = WebAssembly.compileStreaming(fetch("{{basename}}.wasm")); - let instantiated = await instantiate(modulePromise, {}); - invoke(instantiated, []); - } else { - const scriptTag = document.createElement("script"); - scriptTag.type = "application/javascript"; - scriptTag.src = resolveUrlWithSegments("./{{basename}}.bootstrap.js"); - document.head.append(scriptTag); - } -})(); diff --git a/build_web_compilers/lib/src/loader.min.js b/build_web_compilers/lib/src/loader.min.js deleted file mode 100644 index 0fac3f876..000000000 --- a/build_web_compilers/lib/src/loader.min.js +++ /dev/null @@ -1 +0,0 @@ -function s(){let e=[0,97,115,109,1,0,0,0,1,5,1,95,1,120,0];return WebAssembly&&WebAssembly.validate(new Uint8Array(e))}function i(...e){return new URL(joinPathSegments(...e),document.baseURI).toString()}(async()=>{if(s()){let{instantiate:e,invoke:t}=await import("./{{basename}}.mjs"),a=WebAssembly.compileStreaming(fetch("{{basename}}.wasm")),n=await e(a,{});t(n,[])}else{let e=document.createElement("script");e.type="application/javascript",e.src=i("./{{basename}}.bootstrap.js"),document.head.append(e)}})(); diff --git a/build_web_compilers/lib/src/web_entrypoint_builder.dart b/build_web_compilers/lib/src/web_entrypoint_builder.dart index 082353420..23e4cc5e2 100644 --- a/build_web_compilers/lib/src/web_entrypoint_builder.dart +++ b/build_web_compilers/lib/src/web_entrypoint_builder.dart @@ -8,6 +8,7 @@ import 'package:analyzer/dart/analysis/utilities.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:build/build.dart'; import 'package:build_modules/build_modules.dart'; +import 'package:collection/collection.dart'; import 'package:path/path.dart' as p; import 'common.dart'; @@ -27,18 +28,33 @@ const mergedMetadataExtension = '.dart.ddc_merged_metadata'; /// Which compiler to use when compiling web entrypoints. enum WebCompiler { // ignore: constant_identifier_names - Dart2Js('dart2js'), + Dart2Js('dart2js', jsEntrypointExtension, '.dart2js.js'), // ignore: constant_identifier_names - DartDevc('dartdevc'), + DartDevc('dartdevc', jsEntrypointExtension, '.ddc.js'), // ignore: constant_identifier_names - Dart2Wasm('dart2wasm'), - // ignore: constant_identifier_names - Dart2JsAndDart2Wasm('dart2wasm+dart2js'); + Dart2Wasm('dart2wasm', moduleJsExtension, moduleJsExtension); /// The name of this compiler used when identifying it in builder options. final String optionName; - const WebCompiler(this.optionName); + /// The [EnabledEntrypointCompiler.extension] to use when this compiler is the + /// only compiler enabled. + /// This defaults to `.dart.js` for compilers targeting JavaScript and `.mjs` + /// for the module file emitted by `dart2wasm`. + final String entrypointExtensionWhenOnlyCompiler; + + /// The [EnabledEntrypointCompiler.extension] to use by default when multiple + /// compilers are enabled. + /// + /// This can't be `.dart.js` for multiple web compilers because we'd then run + /// into duplicate outputs being generated. + final String entrypointExtension; + + const WebCompiler( + this.optionName, + this.entrypointExtensionWhenOnlyCompiler, + this.entrypointExtension, + ); static WebCompiler fromOptionName(String name) { for (final compiler in values) { @@ -51,41 +67,26 @@ enum WebCompiler { throw ArgumentError.value( name, - _compilerOption, + null, 'Unknown web compiler, supported are: $supported.', ); } } -/// The top level keys supported for the `options` config for the -/// [WebEntrypointBuilder]. -const _supportedOptions = [ - _compilerOption, - _dart2jsArgsOption, - _nativeNullAssertionsOption, - _dart2wasmArgsOption, - _entrypointTemplateOption, -]; +final class EnabledEntrypointCompiler { + final WebCompiler compiler; + final String extension; + final List compilerArguments; -const _compilerOption = 'compiler'; -const _dart2jsArgsOption = 'dart2js_args'; -const _dart2wasmArgsOption = 'dart2wasm_args'; -const _nativeNullAssertionsOption = 'native_null_assertions'; -const _entrypointTemplateOption = 'entrypoint_template'; + EnabledEntrypointCompiler({ + required this.compiler, + required this.extension, + required this.compilerArguments, + }); +} -/// The deprecated keys for the `options` config for the [WebEntrypointBuilder]. -const _deprecatedOptions = [ - 'enable_sync_async', - 'ignore_cast_failures', -]; - -/// A builder which compiles entrypoints for the web. -/// -/// Supports `dart2js` and `dartdevc`. -class WebEntrypointBuilder implements Builder { - final WebCompiler webCompiler; - final List dart2JsArgs; - final List dart2WasmArgs; +final class EntrypointBuilderOptions { + final List compilers; /// Whether or not to enable runtime non-null assertions for values returned /// from browser apis. @@ -94,102 +95,141 @@ class WebEntrypointBuilder implements Builder { /// will be used. final bool? nativeNullAssertions; - /// The template to use for the entrypoint script when compiling with both - /// dart2wasm and dart2js. + /// dart2wasm emits a WebAssembly module and a `.mjs` file exporting symbols + /// to instantiate that module. /// - /// When no template is given, we default to the `loader.min.js` file in this - /// package. In the template, `{{ basename }}` is replaced with the basename - /// of the Dart entrypoint. - final String? entrypointTemplate; - - const WebEntrypointBuilder( - this.webCompiler, { - this.dart2JsArgs = const [], - this.dart2WasmArgs = const [], - required this.nativeNullAssertions, - this.entrypointTemplate, + /// To generate a runnable entrypoint file, a loader will have to load the + /// `.mjs` module and invoke the exported methods. + /// This loader can be generated by `build_web_compilers` (and is by default + /// if dart2wasm is enabled), but this can be turned off for users which need + /// a custom way to load WebAssembly modules (e.g. to target Node.JS). + /// When multiple compilers are enabled (typically dart2js + dart2wasm) for + /// release builds, the loader file is also responsible for running a feature + /// detection, preferring WebAssembly but falling back to JavaScript if + /// necessary. + final String? loaderExtension; + + EntrypointBuilderOptions({ + required this.compilers, + this.nativeNullAssertions, + this.loaderExtension, }); - factory WebEntrypointBuilder.fromOptions(BuilderOptions options) { - validateOptions( - options.config, _supportedOptions, 'build_web_compilers:entrypoint', - deprecatedOptions: _deprecatedOptions); - var compilerOption = - options.config[_compilerOption] as String? ?? 'dartdevc'; - var compiler = WebCompiler.fromOptionName(compilerOption); - - return WebEntrypointBuilder( - compiler, - dart2JsArgs: _parseCompilerOptions(options, _dart2jsArgsOption), - dart2WasmArgs: _parseCompilerOptions(options, _dart2wasmArgsOption), - nativeNullAssertions: - options.config[_nativeNullAssertionsOption] as bool?, - entrypointTemplate: options.config[_entrypointTemplateOption] as String?, + factory EntrypointBuilderOptions.fromOptions(BuilderOptions options) { + const deprecatedOptions = [ + 'enable_sync_async', + 'ignore_cast_failures', + ]; + + const compilerOption = 'compiler'; + const compilersOption = 'compilers'; + const dart2jsArgsOption = 'dart2js_args'; + const dart2wasmArgsOption = 'dart2wasm_args'; + const nativeNullAssertionsOption = 'native_null_assertions'; + const loaderOption = 'loader'; + String? defaultLoaderOption; + + const supportedOptions = [ + compilersOption, + compilerOption, + dart2jsArgsOption, + nativeNullAssertionsOption, + dart2wasmArgsOption, + loaderOption, + ]; + + var config = options.config; + var nativeNullAssertions = + options.config[nativeNullAssertionsOption] as bool?; + var compilers = []; + + validateOptions(config, supportedOptions, 'build_web_compilers:entrypoint', + deprecatedOptions: deprecatedOptions); + + // The compilers option is a map of compiler names to options only applying + // to that compiler, which allows compiling with multiple compilers (e.g. + // dart2js + dart2wasm). For backwards compatibility, we prefer the older + // configuration format using the `compiler` argument: + if (config.containsKey(compilerOption)) { + var compilerName = config[compilerOption] as String? ?? 'dartdevc'; + + var compiler = WebCompiler.fromOptionName(compilerName); + compilers.add(EnabledEntrypointCompiler( + compiler: compiler, + extension: compiler.entrypointExtensionWhenOnlyCompiler, + compilerArguments: switch (compiler) { + WebCompiler.DartDevc => const [], + WebCompiler.Dart2Js => + _parseCompilerOptions(config[dart2jsArgsOption], dart2jsArgsOption), + WebCompiler.Dart2Wasm => _parseCompilerOptions( + config[dart2wasmArgsOption], dart2wasmArgsOption), + }, + )); + + if (compiler == WebCompiler.Dart2Wasm) { + // dart2wasm needs a custom loader script to work as an entrypoint, so + // enable one by default if dart2wasm is configured as compiler. + defaultLoaderOption = '.dart.js'; + } + } else if (config.containsKey(compilersOption)) { + for (var MapEntry(:key, :value) in (config[compilersOption] as Map) + .cast>() + .entries) { + const extensionOption = 'extension'; + const argsOption = 'args'; + const supportedOptions = [extensionOption, argsOption]; + validateOptions( + value, supportedOptions, 'build_web_compilers:entrypoint'); + + var compiler = WebCompiler.fromOptionName(key); + compilers.add(EnabledEntrypointCompiler( + compiler: compiler, + extension: + value[extensionOption] as String? ?? compiler.entrypointExtension, + compilerArguments: + _parseCompilerOptions(value[argsOption], '$compilersOption.$key'), + )); + } + } + + return EntrypointBuilderOptions( + compilers: compilers, + nativeNullAssertions: nativeNullAssertions, + loaderExtension: config.containsKey(loaderOption) + ? config[loaderOption] as String? + : defaultLoaderOption, ); } - @override - final buildExtensions = const { - '.dart': [ - ddcBootstrapExtension, - jsEntrypointExtension, - jsEntrypointSourceMapExtension, - jsEntrypointArchiveExtension, - digestsEntrypointExtension, - mergedMetadataExtension, - wasmExtension, - moduleJsExtension, - ], - }; + EnabledEntrypointCompiler? optionsFor(WebCompiler compiler) { + return compilers.firstWhereOrNull((c) => c.compiler == compiler); + } - @override - Future build(BuildStep buildStep) async { - var dartEntrypointId = buildStep.inputId; - var isAppEntrypoint = await _isAppEntryPoint(dartEntrypointId, buildStep); - if (!isAppEntrypoint) return; - switch (webCompiler) { - case WebCompiler.DartDevc: - try { - await bootstrapDdc(buildStep, - nativeNullAssertions: nativeNullAssertions, - requiredAssets: _ddcSdkResources); - } on MissingModulesException catch (e) { - log.severe('$e'); - } - case WebCompiler.Dart2Js: - await bootstrapDart2Js(buildStep, dart2JsArgs, - nativeNullAssertions: nativeNullAssertions); - case WebCompiler.Dart2Wasm: - await bootstrapDart2Wasm(buildStep, dart2WasmArgs, true); - case WebCompiler.Dart2JsAndDart2Wasm: - await Future.wait( - [ - bootstrapDart2Js( - buildStep, - dart2JsArgs, - nativeNullAssertions: nativeNullAssertions, - generateEntrypoint: false, - ), - bootstrapDart2Wasm(buildStep, dart2WasmArgs, false), - ], - ); - final basename = p.url.basenameWithoutExtension(buildStep.inputId.path); - - final entrypointTemplate = this.entrypointTemplate ?? - await buildStep.readAsString( - AssetId('build_web_compilers', 'lib/src/loader.min.js')); - final entrypoint = - entrypointTemplate.replaceAll(r'{{basename}}', basename); - await buildStep.writeAsString( - buildStep.inputId.changeExtension(jsEntrypointExtension), - entrypoint, - ); - } + Map> get buildExtensions { + return { + '.dart': [ + if (optionsFor(WebCompiler.DartDevc) case final ddc?) ...[ + ddcBootstrapExtension, + mergedMetadataExtension, + digestsEntrypointExtension, + ddc.extension, + ], + if (optionsFor(WebCompiler.Dart2Js) case final dart2js?) ...[ + dart2js.extension, + jsEntrypointSourceMapExtension, + jsEntrypointArchiveExtension, + ], + if (optionsFor(WebCompiler.Dart2Wasm) case final dart2wasm?) ...[ + dart2wasm.extension, + wasmExtension, + ], + if (loaderExtension case final loader?) loader, + ] + }; } - static List _parseCompilerOptions( - BuilderOptions options, String key) { - return switch (options.config[key]) { + static List _parseCompilerOptions(Object? from, String key) { + return switch (from) { null => const [], List list => list.map((arg) => '$arg').toList(), String other => throw ArgumentError.value( @@ -202,6 +242,123 @@ class WebEntrypointBuilder implements Builder { } } +/// The deprecated keys for the `options` config for the [WebEntrypointBuilder]. + +/// A builder which compiles entrypoints for the web. +/// +/// Supports `dart2js` and `dartdevc`. +class WebEntrypointBuilder implements Builder { + final EntrypointBuilderOptions options; + + const WebEntrypointBuilder(this.options); + + factory WebEntrypointBuilder.fromOptions(BuilderOptions options) { + return WebEntrypointBuilder(EntrypointBuilderOptions.fromOptions(options)); + } + + @override + Map> get buildExtensions => options.buildExtensions; + + @override + Future build(BuildStep buildStep) async { + var dartEntrypointId = buildStep.inputId; + var isAppEntrypoint = await _isAppEntryPoint(dartEntrypointId, buildStep); + if (!isAppEntrypoint) return; + + final compilationSteps = []; + + for (final compiler in options.compilers) { + switch (compiler.compiler) { + case WebCompiler.DartDevc: + compilationSteps.add(Future(() async { + try { + await bootstrapDdc(buildStep, + nativeNullAssertions: options.nativeNullAssertions, + requiredAssets: _ddcSdkResources); + } on MissingModulesException catch (e) { + log.severe('$e'); + } + })); + case WebCompiler.Dart2Js: + compilationSteps.add(bootstrapDart2Js( + buildStep, + compiler.compilerArguments, + nativeNullAssertions: options.nativeNullAssertions, + entrypointExtension: compiler.extension, + )); + case WebCompiler.Dart2Wasm: + compilationSteps.add(bootstrapDart2Wasm( + buildStep, compiler.compilerArguments, compiler.extension)); + } + } + await Future.wait(compilationSteps); + if (_generateLoader(buildStep.inputId) case (var id, var loader)?) { + await buildStep.writeAsString(id, loader); + } + } + + (AssetId, String)? _generateLoader(AssetId input) { + var loaderExtension = options.loaderExtension; + var wasmCompiler = options.optionsFor(WebCompiler.Dart2Wasm); + if (loaderExtension == null || wasmCompiler == null) { + // Generating the loader has been disabled or no loader is necessary. + return null; + } + + var loaderId = input.changeExtension(options.loaderExtension!); + var basename = p.url.basenameWithoutExtension(input.path); + + // Are we compiling to JavaScript in addition to wasm? + var jsCompiler = options.optionsFor(WebCompiler.Dart2Js) ?? + options.optionsFor(WebCompiler.DartDevc); + + var loaderResult = StringBuffer('''(async () => { +function resolveUrlWithSegments(...segments) { + return new URL(joinPathSegments(...segments), document.baseURI).toString() +} +'''); + + // If we're compiling to JS, start a feature detection to prefer wasm but + // fall back to JS if necessary. + if (jsCompiler != null) { + loaderResult.writeln(''' +function supportsWasmGC() { + // This attempts to instantiate a wasm module that only will validate if the + // final WasmGC spec is implemented in the browser. + // + // Copied from https://github.com/GoogleChromeLabs/wasm-feature-detect/blob/main/src/detectors/gc/index.js + const bytes = [0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 95, 1, 120, 0]; + return WebAssembly && WebAssembly.validate(new Uint8Array(bytes)); +} + +if (supportsWasmGC()) { +'''); + } + + loaderResult.writeln(''' +let { instantiate, invoke } = await import("./$basename${wasmCompiler.extension}"); + +let modulePromise = WebAssembly.compileStreaming(fetch("$basename.wasm")); +let instantiated = await instantiate(modulePromise, {}); +invoke(instantiated, []); +'''); + + if (jsCompiler != null) { + loaderResult.writeln(''' +} else { +const scriptTag = document.createElement("script"); +scriptTag.type = "application/javascript"; +scriptTag.src = resolveUrlWithSegments("./$basename${jsCompiler.extension}"); +document.head.append(scriptTag); +} +'''); + } + + loaderResult.writeln('})();'); + return (loaderId, loaderResult.toString()); + } +} + /// Returns whether or not [dartId] is an app entrypoint (basically, whether /// or not it has a `main` function). Future _isAppEntryPoint(AssetId dartId, AssetReader reader) async { diff --git a/build_web_compilers/test/dart2js_bootstrap_test.dart b/build_web_compilers/test/dart2js_bootstrap_test.dart index ff931a413..17f3d160e 100644 --- a/build_web_compilers/test/dart2js_bootstrap_test.dart +++ b/build_web_compilers/test/dart2js_bootstrap_test.dart @@ -7,6 +7,7 @@ import 'dart:async'; 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/src/web_entrypoint_builder.dart'; import 'package:logging/logging.dart'; import 'package:test/test.dart'; @@ -59,8 +60,15 @@ void main() { 'a|web/index.dart.js.tar.gz': anything, }; await testBuilder( - const WebEntrypointBuilder(WebCompiler.Dart2Js, - nativeNullAssertions: false), + WebEntrypointBuilder( + EntrypointBuilderOptions(compilers: [ + EnabledEntrypointCompiler( + compiler: WebCompiler.Dart2Js, + extension: '.dart2js', + compilerArguments: [], + ), + ], nativeNullAssertions: false), + ), assets, outputs: expectedOutputs); }); @@ -71,8 +79,15 @@ void main() { 'a|web/index.dart.js.tar.gz': anything, }; await testBuilder( - const WebEntrypointBuilder(WebCompiler.Dart2Js, - dart2JsArgs: ['--no-source-maps'], nativeNullAssertions: false), + WebEntrypointBuilder( + EntrypointBuilderOptions(compilers: [ + EnabledEntrypointCompiler( + compiler: WebCompiler.Dart2Js, + extension: '.dart2js', + compilerArguments: ['--no-source-maps'], + ), + ], nativeNullAssertions: false), + ), assets, outputs: expectedOutputs); }); @@ -101,8 +116,15 @@ void main() { 'a|lib/index.dart.js.tar.gz': anything, }; await testBuilder( - const WebEntrypointBuilder(WebCompiler.Dart2Js, - nativeNullAssertions: false), + WebEntrypointBuilder( + EntrypointBuilderOptions(compilers: [ + EnabledEntrypointCompiler( + compiler: WebCompiler.Dart2Js, + extension: '.dart2js', + compilerArguments: [], + ), + ], nativeNullAssertions: false), + ), assets, outputs: expectedOutputs); }); diff --git a/build_web_compilers/test/dart2wasm_bootstrap_test.dart b/build_web_compilers/test/dart2wasm_bootstrap_test.dart new file mode 100644 index 000000000..2e0966cfa --- /dev/null +++ b/build_web_compilers/test/dart2wasm_bootstrap_test.dart @@ -0,0 +1,100 @@ +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:test/test.dart'; + +import 'util.dart'; + +void main() { + late Map assets; + + group('generates loader script', () { + setUp(() async { + assets = { + 'a|web/index.dart': ''' + void main() { + print('Hello world!'); + } + ''', + }; + + // Set up all the other required inputs for this test. + await testBuilderAndCollectAssets(const ModuleLibraryBuilder(), assets); + for (final platform in [dart2jsPlatform, dart2wasmPlatform]) { + await testBuilderAndCollectAssets(MetaModuleBuilder(platform), assets); + await testBuilderAndCollectAssets( + MetaModuleCleanBuilder(platform), assets); + await testBuilderAndCollectAssets(ModuleBuilder(platform), assets); + } + }); + + test('with old compiler option', () async { + await testBuilder( + WebEntrypointBuilder.fromOptions( + const BuilderOptions({'compiler': 'dart2wasm'})), + assets, + outputs: { + 'a|web/index.mjs': anything, + 'a|web/index.wasm': anything, + 'a|web/index.dart.js': + decodedMatches(contains('WebAssembly.compileStreaming')), + }, + ); + }); + + test('when using both dart2wasm and dart2js', () async { + await testBuilder( + WebEntrypointBuilder.fromOptions( + const BuilderOptions( + { + 'compilers': { + 'dart2js': {}, + 'dart2wasm': {}, + }, + 'loader': '.dart.js', + }, + ), + ), + assets, + outputs: { + 'a|web/index.mjs': anything, + 'a|web/index.wasm': anything, + 'a|web/index.dart2js.js': decodedMatches(contains('Hello world!')), + 'a|web/index.dart.js': decodedMatches( + stringContainsInOrder( + [ + 'if (supportsWasmGC())', + 'WebAssembly.compileStreaming', + 'else', + 'scriptTag.src = resolveUrlWithSegments("./index.dart2js.js");' + ], + ), + ), + }, + ); + }); + }); + + test('can disable generation of loader script', () async { + await testBuilder( + WebEntrypointBuilder.fromOptions( + const BuilderOptions( + { + 'compilers': { + 'dart2js': {}, + 'dart2wasm': {}, + }, + 'loader': null, + }, + ), + ), + assets, + outputs: { + 'a|web/index.mjs': anything, + 'a|web/index.wasm': anything, + 'a|web/index.dart2js.js': decodedMatches(contains('Hello world!')), + }, + ); + }); +} diff --git a/build_web_compilers/test/dev_compiler_bootstrap_test.dart b/build_web_compilers/test/dev_compiler_bootstrap_test.dart index b2889c4f8..8552bf3e8 100644 --- a/build_web_compilers/test/dev_compiler_bootstrap_test.dart +++ b/build_web_compilers/test/dev_compiler_bootstrap_test.dart @@ -7,6 +7,7 @@ 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:build_web_compilers/src/web_entrypoint_builder.dart'; import 'package:test/test.dart'; import 'util.dart'; @@ -54,8 +55,15 @@ void main() { ])), }; await testBuilder( - const WebEntrypointBuilder(WebCompiler.DartDevc, - nativeNullAssertions: false), + WebEntrypointBuilder( + EntrypointBuilderOptions(compilers: [ + EnabledEntrypointCompiler( + compiler: WebCompiler.DartDevc, + extension: '.dart2js', + compilerArguments: [], + ), + ], nativeNullAssertions: false), + ), assets, outputs: expectedOutputs); }); @@ -93,8 +101,15 @@ void main() { 'a|web/b.dart.js': isNotEmpty, }; await testBuilder( - const WebEntrypointBuilder(WebCompiler.DartDevc, - nativeNullAssertions: false), + WebEntrypointBuilder( + EntrypointBuilderOptions(compilers: [ + EnabledEntrypointCompiler( + compiler: WebCompiler.DartDevc, + extension: '.dart2js', + compilerArguments: [], + ), + ], nativeNullAssertions: false), + ), assets, outputs: expectedOutputs); }); @@ -116,8 +131,15 @@ void main() { 'a|lib/app.dart.js': isNotEmpty, }; await testBuilder( - const WebEntrypointBuilder(WebCompiler.DartDevc, - nativeNullAssertions: false), + WebEntrypointBuilder( + EntrypointBuilderOptions(compilers: [ + EnabledEntrypointCompiler( + compiler: WebCompiler.DartDevc, + extension: '.dart2js', + compilerArguments: [], + ), + ], nativeNullAssertions: false), + ), assets, outputs: expectedOutputs); }); diff --git a/build_web_compilers/test/entrypoint_options_test.dart b/build_web_compilers/test/entrypoint_options_test.dart new file mode 100644 index 000000000..645a98183 --- /dev/null +++ b/build_web_compilers/test/entrypoint_options_test.dart @@ -0,0 +1,86 @@ +// Copyright (c) 2024, 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_web_compilers/src/web_entrypoint_builder.dart'; +import 'package:test/test.dart'; + +void main() { + test('parses old dart2js options', () { + final options = EntrypointBuilderOptions.fromOptions(const BuilderOptions({ + 'compiler': 'dart2js', + 'dart2js_args': ['-O4'], + })); + + expect(options.nativeNullAssertions, isNull); + expect(options.loaderExtension, isNull); + expect(options.compilers, [ + isA() + .having((e) => e.compiler, 'compiler', WebCompiler.Dart2Js) + .having((e) => e.compilerArguments, 'compilerArguments', + ['-O4']).having((e) => e.extension, 'extension', '.dart.js') + ]); + }); + + test('parses old ddc options', () { + final options = EntrypointBuilderOptions.fromOptions(const BuilderOptions({ + 'compiler': 'dartdevc', + 'dart2js_args': ['-O4'], + })); + + expect(options.nativeNullAssertions, isNull); + expect(options.loaderExtension, isNull); + expect(options.compilers, [ + isA() + .having((e) => e.compiler, 'compiler', WebCompiler.DartDevc) + .having((e) => e.compilerArguments, 'compilerArguments', isEmpty) + .having((e) => e.extension, 'extension', '.dart.js') + ]); + }); + + test('parses old dart2wasm options', () { + final options = EntrypointBuilderOptions.fromOptions(const BuilderOptions({ + 'compiler': 'dart2wasm', + 'dart2wasm_args': ['-O4'], + })); + + expect(options.nativeNullAssertions, isNull); + expect(options.loaderExtension, '.dart.js'); + expect(options.compilers, [ + isA() + .having((e) => e.compiler, 'compiler', WebCompiler.Dart2Wasm) + .having((e) => e.compilerArguments, 'compilerArguments', + ['-O4']).having((e) => e.extension, 'extension', '.mjs') + ]); + }); + + test('can enable multiple compilers', () { + final options = EntrypointBuilderOptions.fromOptions(const BuilderOptions({ + 'compilers': { + 'dart2js': { + 'args': ['-O4'], + }, + 'dart2wasm': { + 'extension': '.custom_extension.js', + 'args': ['-O3'], + }, + }, + 'loader': '.dart.js', + })); + + expect(options.nativeNullAssertions, isNull); + expect(options.loaderExtension, '.dart.js'); + expect(options.compilers, [ + isA() + .having((e) => e.compiler, 'compiler', WebCompiler.Dart2Js) + .having((e) => e.compilerArguments, 'compilerArguments', + ['-O4']).having((e) => e.extension, 'extension', '.dart2js.js'), + isA() + .having((e) => e.compiler, 'compiler', WebCompiler.Dart2Wasm) + .having((e) => e.compilerArguments, 'compilerArguments', [ + '-O3' + ]).having((e) => e.extension, 'extension', '.custom_extension.js'), + ]); + }); +} From 68c34957e2f11a9e062351e78ab573143f6ba342 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 5 Sep 2024 23:44:32 +0200 Subject: [PATCH 05/14] Update readme to reflect new options --- build_web_compilers/README.md | 215 +++++++++++++----- build_web_compilers/build.yaml | 1 - .../lib/src/web_entrypoint_builder.dart | 21 +- .../test/dart2wasm_bootstrap_test.dart | 1 - 4 files changed, 175 insertions(+), 63 deletions(-) diff --git a/build_web_compilers/README.md b/build_web_compilers/README.md index 779bdce0f..768f1cdcc 100644 --- a/build_web_compilers/README.md +++ b/build_web_compilers/README.md @@ -39,16 +39,36 @@ then all you need is the `dev_dependency` listed above. ## Configuration -### Configuring the default compiler - By default, this package uses the [Dart development compiler][] (_dartdevc_, also known as _DDC_) to compile Dart to JavaScript. In release builds (running the build tool with `--release`, this package uses both `dart2js` and `dart2wasm` with a custom entrypoint loading the appropriate module depending on browser features). -If you would like to opt into dart2js for all builds, you will need to add a -`build.yaml` file, which should look roughly like the following: +This behavior can be changed with builder options. To understand the impact of +these options, be aware of differences between compiling to JavaScript and +compiling to WebAssembly: + +1. Dart has two compilers emitting JavaScript: `dart2js` and `dartdevc` (which + supports incremental rebuilds but is typically only used for development). + For both JavaScript compilers, `build_web_compilers` generates a primary + entrypoint script and additional module files or source maps depending on + compiler options. +2. Compiling with `dart2wasm` generates a WebAssembly module (a `.wasm` file) + and a JavaScript module (a `.mjs` file) exporting functions to instantiate + that module. `dart2wasm` alone generates no entrypoint file that could be + added as a `