diff --git a/dwds_test_common/fixtures/_webdev_smoke/pubspec.yaml b/dwds_test_common/fixtures/_webdev_smoke/pubspec.yaml index f9bf43255..550a1658d 100644 --- a/dwds_test_common/fixtures/_webdev_smoke/pubspec.yaml +++ b/dwds_test_common/fixtures/_webdev_smoke/pubspec.yaml @@ -4,7 +4,7 @@ description: A test fixture for webdev testing. publish_to: none environment: - sdk: ^3.10.0-0.0.dev + sdk: ^3.14.0-286.0.dev dev_dependencies: build_daemon: ^4.1.4 diff --git a/webdev/CHANGELOG.md b/webdev/CHANGELOG.md index fc4ec9d78..63d5e6d0a 100644 --- a/webdev/CHANGELOG.md +++ b/webdev/CHANGELOG.md @@ -1,3 +1,12 @@ +## 4.1.0-wip + +- Add a `--[no-]deprecated-js-interop` flag to `serve`, `build` and `daemon`. + When set, it allows or disallows the deprecated JS interop libraries, such + as `dart:html` and `package:js`, in dartdevc, dart2js and expression + evaluation. When not set, the Dart SDK default is used. Requires a Dart SDK + that supports the flag and `build_web_compilers` 4.9.0 or later. +- Require `package:dwds` `^27.2.0`. + ## 4.0.3 - Fix failing debugger expression evaluation. diff --git a/webdev/README.md b/webdev/README.md index ba81d1d0f..210b1f330 100644 --- a/webdev/README.md +++ b/webdev/README.md @@ -115,6 +115,15 @@ Common: (defaults to on) -v, --verbose Enables verbose logging. --offline Disable fetching from pub.dev. + --[no-]deprecated-js-interop Whether to allow the deprecated JS + interop libraries, such as + "dart:html", "dart:js", + "dart:js_util" and "package:js", when + compiling with ddc and dart2js. + If not set, the Dart SDK default is + used. Requires a Dart SDK and + "build_web_compilers" version that + support this option. Run "webdev help" to see global options. @@ -149,6 +158,15 @@ Usage: webdev build [arguments] (defaults to on) -v, --verbose Enables verbose logging. --offline Disable fetching from pub.dev. + --[no-]deprecated-js-interop Whether to allow the deprecated JS + interop libraries, such as + "dart:html", "dart:js", + "dart:js_util" and "package:js", when + compiling with ddc and dart2js. + If not set, the Dart SDK default is + used. Requires a Dart SDK and + "build_web_compilers" version that + support this option. Run "webdev help" to see global options. diff --git a/webdev/lib/src/command/configuration.dart b/webdev/lib/src/command/configuration.dart index 219fe399c..e0f6f4f69 100644 --- a/webdev/lib/src/command/configuration.dart +++ b/webdev/lib/src/command/configuration.dart @@ -36,6 +36,7 @@ const canaryFeaturesFlag = 'canary'; const moduleFormatFlag = 'module-format'; const offlineFlag = 'offline'; const webHotReloadFlag = 'web-hot-reload'; +const deprecatedJsInteropFlag = 'deprecated-js-interop'; ReloadConfiguration _parseReloadConfiguration(ArgResults argResults) { var auto = argResults.options.contains(autoOption) @@ -111,61 +112,42 @@ class Configuration { final String? _moduleFormat; final bool? _offline; final bool? _webHotReload; + final bool? _deprecatedJsInterop; Configuration({ - bool? autoRun, - int? chromeDebugPort, - bool? debugExtension, - bool? debug, - bool? enableInjectedClient, - String? hostname, - String? tlsCertChain, - String? tlsCertKey, - List? launchApps, - bool? launchInChrome, - String? userDataDir, - bool? logRequests, - String? output, + this._autoRun, + this._chromeDebugPort, + this._debugExtension, + this._debug, + this._enableInjectedClient, + this._hostname, + this._tlsCertChain, + this._tlsCertKey, + this._launchApps, + this._launchInChrome, + this._userDataDir, + this._logRequests, + this._output, this.outputInput, this.outputPath, - ReloadConfiguration? reload, - bool? release, - bool? requireBuildWebCompilers, - bool? enableExpressionEvaluation, - bool? verbose, - bool? disableDds, - List? experiments, + this._reload, + this._release, + this._requireBuildWebCompilers, + this._enableExpressionEvaluation, + this._verbose, + this._disableDds, + this._experiments, bool? canaryFeatures, String? moduleFormat, - bool? offline, + this._offline, bool? webHotReload, - }) : _autoRun = autoRun, - _chromeDebugPort = chromeDebugPort, - _debugExtension = debugExtension, - _debug = debug, - _enableInjectedClient = enableInjectedClient, - _hostname = hostname, - _tlsCertChain = tlsCertChain, - _tlsCertKey = tlsCertKey, - _launchApps = launchApps, - _launchInChrome = launchInChrome, - _userDataDir = userDataDir, - _logRequests = logRequests, - _output = output, - _release = release, - _reload = reload, - _requireBuildWebCompilers = requireBuildWebCompilers, - _disableDds = disableDds, - _enableExpressionEvaluation = enableExpressionEvaluation, - _verbose = verbose, - _experiments = experiments, - _canaryFeatures = (webHotReload == true && canaryFeatures == null) + this._deprecatedJsInterop, + }) : _canaryFeatures = (webHotReload == true && canaryFeatures == null) ? true : canaryFeatures, _moduleFormat = (webHotReload == true && moduleFormat == null) ? 'ddc' : moduleFormat, - _offline = offline, _webHotReload = webHotReload { _validateConfiguration(); } @@ -265,6 +247,7 @@ class Configuration { moduleFormat: other._moduleFormat ?? _moduleFormat, offline: other._offline ?? _offline, webHotReload: other._webHotReload ?? _webHotReload, + deprecatedJsInterop: other._deprecatedJsInterop ?? _deprecatedJsInterop, ); factory Configuration.noInjectedClientDefaults() => @@ -321,6 +304,12 @@ class Configuration { bool get offline => _offline ?? false; + /// Whether the compilers allow the deprecated JS interop libraries, such as + /// `dart:html`. + /// + /// If `null`, nothing is passed to the compilers and the SDK default is used. + bool? get deprecatedJsInterop => _deprecatedJsInterop; + /// Returns a new configuration with values updated from the parsed args. static Configuration fromArgs( ArgResults? argResults, { @@ -520,6 +509,12 @@ class Configuration { ? argResults[offlineFlag] as bool? : defaultConfiguration.offline; + final deprecatedJsInterop = + argResults.options.contains(deprecatedJsInteropFlag) && + argResults.wasParsed(deprecatedJsInteropFlag) + ? argResults[deprecatedJsInteropFlag] as bool? + : defaultConfiguration.deprecatedJsInterop; + return Configuration( autoRun: defaultConfiguration.autoRun, chromeDebugPort: chromeDebugPort, @@ -547,6 +542,7 @@ class Configuration { canaryFeatures: coercedCanaryFeatures ?? canaryFeatures, moduleFormat: coercedModuleFormat ?? moduleFormat, offline: offline, + deprecatedJsInterop: deprecatedJsInterop, ); } } diff --git a/webdev/lib/src/command/shared.dart b/webdev/lib/src/command/shared.dart index b492a84fd..c7ecb8c63 100644 --- a/webdev/lib/src/command/shared.dart +++ b/webdev/lib/src/command/shared.dart @@ -101,9 +101,43 @@ void addSharedArgs( defaultsTo: false, negatable: false, help: 'Disable fetching from pub.dev.', + ) + ..addFlag( + deprecatedJsInteropFlag, + defaultsTo: null, + negatable: true, + help: + 'Whether to allow the deprecated JS interop libraries, such as ' + '"dart:html", "dart:js", "dart:js_util" and "package:js", when ' + 'compiling with ddc and dart2js.\n' + 'If not set, the Dart SDK default is used. Requires a Dart SDK and ' + '"build_web_compilers" version that support this option.', ); } +/// The `build_web_compilers` builders that must be configured with the same +/// `deprecated-js-interop` option. +const _deprecatedJsInteropBuilders = [ + 'ddc', + 'ddc_modules', + 'dart2js_modules', + 'entrypoint', +]; + +/// Returns the `build_runner` arguments that configure +/// `deprecated-js-interop` for every builder that compiles user code to JS. +/// +/// Returns an empty list when [deprecatedJsInterop] is `null` so that the +/// SDK default is used. +List _deprecatedJsInteropArgs(bool? deprecatedJsInterop) => [ + if (deprecatedJsInterop != null) + for (final builder in _deprecatedJsInteropBuilders) ...[ + '--define', + 'build_web_compilers|$builder=' + '$deprecatedJsInteropFlag=$deprecatedJsInterop', + ], +]; + /// Parses the provided [Configuration] to return a list of /// `package:build_runner` appropriate arguments. List buildRunnerArgs(Configuration configuration) { @@ -158,6 +192,8 @@ List buildRunnerArgs(Configuration configuration) { ..add('build_web_compilers|entrypoint_marker=ddc-library-bundle=true'); } + arguments.addAll(_deprecatedJsInteropArgs(configuration.deprecatedJsInterop)); + return arguments; } @@ -166,9 +202,21 @@ Future validatePubspecLock(Configuration configuration) async { await checkPubspecLock( pubspecLock, requireBuildWebCompilers: configuration.requireBuildWebCompilers, + deprecatedJsInteropArgument: _deprecatedJsInteropFlagName( + configuration.deprecatedJsInterop, + ), ); } +/// The `--[no-]deprecated-js-interop` flag name (without `--`) the user +/// passed, or `null` if they did not pass it. +String? _deprecatedJsInteropFlagName(bool? deprecatedJsInterop) => + switch (deprecatedJsInterop) { + null => null, + true => deprecatedJsInteropFlag, + false => 'no-$deprecatedJsInteropFlag', + }; + /// Checks that the normalized form of [path] is a top level directory under /// such as `web` or `test`. /// diff --git a/webdev/lib/src/logging.dart b/webdev/lib/src/logging.dart index 6e65dff6d..ea0ed8553 100644 --- a/webdev/lib/src/logging.dart +++ b/webdev/lib/src/logging.dart @@ -8,14 +8,13 @@ import 'dart:io'; import 'package:io/ansi.dart'; import 'package:logging/logging.dart'; -typedef LogWriter = - void Function( - Level level, - String message, { - String? error, - String? loggerName, - String? stackTrace, - }); +typedef LogWriter = void Function( + Level level, + String message, { + String? error, + String? loggerName, + String? stackTrace, +}); var _verbose = false; StreamSubscription? _subscription; diff --git a/webdev/lib/src/pubspec.dart b/webdev/lib/src/pubspec.dart index b82d111bb..ab8068a74 100644 --- a/webdev/lib/src/pubspec.dart +++ b/webdev/lib/src/pubspec.dart @@ -31,8 +31,8 @@ class PackageExceptionDetails { const PackageExceptionDetails._( this.error, { this.description, - bool missingDependency = false, - }) : _missingDependency = missingDependency; + this._missingDependency = false, + }); static PackageExceptionDetails missingDep( String pkgName, @@ -91,13 +91,9 @@ class PubspecLock { dir = next; } - final pubspecLock = - loadYaml( - await File( - p.relative(p.join(dir, 'pubspec.lock')), - ).readAsString(), - ) - as YamlMap; + final pubspecLock = loadYaml( + await File(p.relative(p.join(dir, 'pubspec.lock'))).readAsString(), + ) as YamlMap; final packages = pubspecLock['packages'] as YamlMap?; return PubspecLock(packages); @@ -190,11 +186,22 @@ Future> _validateBuildDaemonVersion( final buildRunnerConstraint = VersionConstraint.parse('^2.4.0'); final buildWebCompilersConstraint = VersionConstraint.parse('^4.4.12'); +/// The `build_web_compilers` versions that support the `deprecated-js-interop` +/// option. +final buildWebCompilersDeprecatedJsInteropConstraint = VersionConstraint.parse( + '^4.9.0', +); + // Note the minimum versions should never be dev versions as users will not // get them by default. +// +// If [deprecatedJsInteropArgument] is set, it is the `--[no-]` form of the +// `deprecated-js-interop` flag passed by the user, which requires a newer +// `build_web_compilers`. Future checkPubspecLock( PubspecLock pubspecLock, { required bool requireBuildWebCompilers, + String? deprecatedJsInteropArgument, }) async { final issues = []; final buildRunnerIssues = pubspecLock.checkPackage( @@ -220,6 +227,25 @@ Future checkPubspecLock( if (issues.isNotEmpty) { throw PackageException(issues); } + + if (requireBuildWebCompilers && deprecatedJsInteropArgument != null) { + _checkDeprecatedJsInteropSupport(pubspecLock, deprecatedJsInteropArgument); + } +} + +/// Throws a [PackageException] if `build_web_compilers` does not support the +/// `deprecated-js-interop` option used by [argument]. +void _checkDeprecatedJsInteropSupport( + PubspecLock pubspecLock, + String argument, +) { + final issues = pubspecLock.checkPackage( + 'build_web_compilers', + buildWebCompilersDeprecatedJsInteropConstraint, + ); + if (issues.isNotEmpty) { + throw PackageException(issues, unsupportedArgument: argument); + } } class _PackageInfo { diff --git a/webdev/lib/src/serve/webdev_server.dart b/webdev/lib/src/serve/webdev_server.dart index da1f7f279..2598ad513 100644 --- a/webdev/lib/src/serve/webdev_server.dart +++ b/webdev/lib/src/serve/webdev_server.dart @@ -229,6 +229,7 @@ class WebDevServer { canaryFeatures: options.configuration.canaryFeatures, isFlutterApp: false, experiments: options.configuration.experiments, + deprecatedJsInterop: options.configuration.deprecatedJsInterop, ); final LoadStrategy loadStrategy; diff --git a/webdev/lib/src/version.dart b/webdev/lib/src/version.dart index ee7a14f00..78c01d1a6 100644 --- a/webdev/lib/src/version.dart +++ b/webdev/lib/src/version.dart @@ -1,2 +1,2 @@ // Generated code. Do not modify. -const packageVersion = '4.0.3'; +const packageVersion = '4.1.0-wip'; diff --git a/webdev/pubspec.yaml b/webdev/pubspec.yaml index 85a77bf77..2b82ca231 100644 --- a/webdev/pubspec.yaml +++ b/webdev/pubspec.yaml @@ -1,6 +1,6 @@ name: webdev # Every time this changes you need to run `dart run build_runner build`. -version: 4.0.3 +version: 4.1.0-wip # We should not depend on a dev SDK before publishing. # publish_to: none description: >- @@ -8,7 +8,7 @@ description: >- features for users and tools to build and deploy web applications with Dart. repository: https://github.com/dart-lang/webdev/tree/main/webdev environment: - sdk: ^3.10.0-0.0.dev + sdk: ^3.14.0-286.0.dev dependencies: args: ^2.3.1 @@ -18,7 +18,7 @@ dependencies: collection: ^1.15.0 crypto: ^3.0.2 dds: ^5.3.0 - dwds: ^27.1.2 + dwds: ^27.2.0 file: ^7.0.1 http: ^1.0.0 http_multi_server: ^3.2.0 diff --git a/webdev/test/configuration_test.dart b/webdev/test/configuration_test.dart index 321bb132d..7b7294323 100644 --- a/webdev/test/configuration_test.dart +++ b/webdev/test/configuration_test.dart @@ -131,14 +131,11 @@ void main() { ); }); - test( - 'webHotReload coerces canaryFeatures to true and moduleFormat to ddc if not set', - () { - final configuration = Configuration(webHotReload: true); - expect(configuration.canaryFeatures, isTrue); - expect(configuration.moduleFormat, equals('ddc')); - }, - ); + test('webHotReload coerces canaryFeatures to true and moduleFormat to ddc if not set', () { + final configuration = Configuration(webHotReload: true); + expect(configuration.canaryFeatures, isTrue); + expect(configuration.moduleFormat, equals('ddc')); + }); test('webHotReload + canaryFeatures false throws', () { expect( @@ -173,4 +170,61 @@ void main() { expect(config.moduleFormat, equals('amd')); expect(config.canaryFeatures, isFalse); }); + + group('--[no-]deprecated-js-interop', () { + test('is not set by default', () { + final config = Configuration.fromArgs(argParser.parse([])); + expect(config.deprecatedJsInterop, isNull); + expect( + buildRunnerArgs(config), + isNot(contains(contains('deprecated-js-interop'))), + ); + }); + + test('is read from args', () { + expect( + Configuration.fromArgs(argParser.parse(['--deprecated-js-interop'])) + .deprecatedJsInterop, + isTrue, + ); + expect( + Configuration.fromArgs(argParser.parse(['--no-deprecated-js-interop'])) + .deprecatedJsInterop, + isFalse, + ); + }); + + test('uses the default configuration when not passed', () { + final config = Configuration.fromArgs( + argParser.parse([]), + defaultConfiguration: Configuration(deprecatedJsInterop: false), + ); + expect(config.deprecatedJsInterop, isFalse); + }); + + for (final value in [true, false]) { + test('configures every JS compiler builder with $value', () { + final config = Configuration(deprecatedJsInterop: value); + final defines = [ + for (final builder in [ + 'ddc', + 'ddc_modules', + 'dart2js_modules', + 'entrypoint', + ]) + 'build_web_compilers|$builder=deprecated-js-interop=$value', + ]; + final args = buildRunnerArgs(config); + for (final define in defines) { + final index = args.indexOf(define); + expect(index, greaterThan(0), reason: define); + expect(args[index - 1], '--define'); + } + expect( + args.where((arg) => arg.contains('deprecated-js-interop')), + hasLength(defines.length), + ); + }); + } + }); } diff --git a/webdev/test/e2e_common.dart b/webdev/test/e2e_common.dart index 9080a1ae1..51695e160 100644 --- a/webdev/test/e2e_common.dart +++ b/webdev/test/e2e_common.dart @@ -66,9 +66,9 @@ void e2eTests({required TestRunner testRunner}) { tearDownAll(testRunner.tearDownAll); test('smoke test is configured properly', () async { - final smokeYaml = - loadYaml(await File('$exampleDirectory/pubspec.yaml').readAsString()) - as YamlMap; + final smokeYaml = loadYaml( + await File('$exampleDirectory/pubspec.yaml').readAsString(), + ) as YamlMap; final webdevYaml = loadYaml(await File('pubspec.yaml').readAsString()) as YamlMap; expect( diff --git a/webdev/test/pubspec_test.dart b/webdev/test/pubspec_test.dart new file mode 100644 index 000000000..356ff36fc --- /dev/null +++ b/webdev/test/pubspec_test.dart @@ -0,0 +1,71 @@ +// Copyright (c) 2026, 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:test/test.dart'; +import 'package:webdev/src/pubspec.dart'; +import 'package:yaml/yaml.dart'; + +/// A lock file with hosted `build_runner` and `build_web_compilers`, and no +/// `build_daemon`, so checking it does not need the network. +PubspecLock _lockWith({required String buildWebCompilersVersion}) => + PubspecLock( + loadYaml(''' +build_runner: + source: hosted + version: "2.5.0" +build_web_compilers: + source: hosted + version: "$buildWebCompilersVersion" +''') as YamlMap, + ); + +void main() { + group('checkPubspecLock with deprecated-js-interop', () { + test('allows an older build_web_compilers without the flag', () async { + await checkPubspecLock( + _lockWith(buildWebCompilersVersion: '4.8.0'), + requireBuildWebCompilers: true, + ); + }); + + test('rejects an older build_web_compilers with the flag', () async { + await expectLater( + checkPubspecLock( + _lockWith(buildWebCompilersVersion: '4.8.0'), + requireBuildWebCompilers: true, + deprecatedJsInteropArgument: 'no-deprecated-js-interop', + ), + throwsA( + isA() + .having( + (e) => e.unsupportedArgument, + 'unsupportedArgument', + 'no-deprecated-js-interop', + ) + .having( + (e) => e.details.single.error, + 'error', + contains('build_web_compilers'), + ), + ), + ); + }); + + test('allows a build_web_compilers that supports the flag', () async { + await checkPubspecLock( + _lockWith(buildWebCompilersVersion: '4.9.0'), + requireBuildWebCompilers: true, + deprecatedJsInteropArgument: 'deprecated-js-interop', + ); + }); + + test('skips the check without a build_web_compilers requirement', () async { + await checkPubspecLock( + _lockWith(buildWebCompilersVersion: '4.8.0'), + requireBuildWebCompilers: false, + deprecatedJsInteropArgument: 'deprecated-js-interop', + ); + }); + }); +}