[flutter_tools] support sound null-safety mode for the web (#60570)
In web debug mode, infer sound null safety by default. When sound null safety is enabled, provide a separate dill and precompiled Dart SDK. Release builds do not need this setting since we run dart2js from source. Fixes #59873
This commit is contained in:
parent
2188196125
commit
7ca324ac01
@ -39,9 +39,22 @@ enum Artifact {
|
|||||||
kernelWorkerSnapshot,
|
kernelWorkerSnapshot,
|
||||||
/// The root of the web implementation of the dart SDK.
|
/// The root of the web implementation of the dart SDK.
|
||||||
flutterWebSdk,
|
flutterWebSdk,
|
||||||
|
/// The libraries JSON file for web release builds.
|
||||||
flutterWebLibrariesJson,
|
flutterWebLibrariesJson,
|
||||||
/// The summary dill for the dartdevc target.
|
/// The summary dill for the dartdevc target.
|
||||||
webPlatformKernelDill,
|
webPlatformKernelDill,
|
||||||
|
/// The summary dill with null safety enabled for the dartdevc target.
|
||||||
|
webPlatformSoundKernelDill,
|
||||||
|
/// The precompiled SDKs and sourcemaps for web debug builds.
|
||||||
|
webPrecompiledSdk,
|
||||||
|
webPrecompiledSdkSourcemaps,
|
||||||
|
webPrecompiledCanvaskitSdk,
|
||||||
|
webPrecompiledCanvaskitSdkSourcemaps,
|
||||||
|
webPrecompiledSoundSdk,
|
||||||
|
webPrecompiledSoundSdkSourcemaps,
|
||||||
|
webPrecompiledCanvaskitSoundSdk,
|
||||||
|
webPrecompiledCanvaskitSoundSdkSourcemaps,
|
||||||
|
|
||||||
iosDeploy,
|
iosDeploy,
|
||||||
idevicesyslog,
|
idevicesyslog,
|
||||||
idevicescreenshot,
|
idevicescreenshot,
|
||||||
@ -129,6 +142,8 @@ String _artifactToFileName(Artifact artifact, [ TargetPlatform platform, BuildMo
|
|||||||
return 'FlutterMacOS.podspec';
|
return 'FlutterMacOS.podspec';
|
||||||
case Artifact.webPlatformKernelDill:
|
case Artifact.webPlatformKernelDill:
|
||||||
return 'flutter_ddc_sdk.dill';
|
return 'flutter_ddc_sdk.dill';
|
||||||
|
case Artifact.webPlatformSoundKernelDill:
|
||||||
|
return 'flutter_ddc_sdk_sound.dill';
|
||||||
case Artifact.fuchsiaKernelCompiler:
|
case Artifact.fuchsiaKernelCompiler:
|
||||||
return 'kernel_compiler.snapshot';
|
return 'kernel_compiler.snapshot';
|
||||||
case Artifact.fuchsiaFlutterRunner:
|
case Artifact.fuchsiaFlutterRunner:
|
||||||
@ -141,6 +156,16 @@ String _artifactToFileName(Artifact artifact, [ TargetPlatform platform, BuildMo
|
|||||||
return 'const_finder.dart.snapshot';
|
return 'const_finder.dart.snapshot';
|
||||||
case Artifact.flutterWebLibrariesJson:
|
case Artifact.flutterWebLibrariesJson:
|
||||||
return 'libraries.json';
|
return 'libraries.json';
|
||||||
|
case Artifact.webPrecompiledSdk:
|
||||||
|
case Artifact.webPrecompiledCanvaskitSdk:
|
||||||
|
case Artifact.webPrecompiledSoundSdk:
|
||||||
|
case Artifact.webPrecompiledCanvaskitSoundSdk:
|
||||||
|
return 'dart_sdk.js';
|
||||||
|
case Artifact.webPrecompiledSdkSourcemaps:
|
||||||
|
case Artifact.webPrecompiledCanvaskitSdkSourcemaps:
|
||||||
|
case Artifact.webPrecompiledSoundSdkSourcemaps:
|
||||||
|
case Artifact.webPrecompiledCanvaskitSoundSdkSourcemaps:
|
||||||
|
return 'dart_sdk.js.map';
|
||||||
}
|
}
|
||||||
assert(false, 'Invalid artifact $artifact.');
|
assert(false, 'Invalid artifact $artifact.');
|
||||||
return null;
|
return null;
|
||||||
@ -348,6 +373,8 @@ class CachedArtifacts implements Artifacts {
|
|||||||
return _fileSystem.path.join(_getFlutterWebSdkPath(), _artifactToFileName(artifact));
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), _artifactToFileName(artifact));
|
||||||
case Artifact.webPlatformKernelDill:
|
case Artifact.webPlatformKernelDill:
|
||||||
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', _artifactToFileName(artifact));
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', _artifactToFileName(artifact));
|
||||||
|
case Artifact.webPlatformSoundKernelDill:
|
||||||
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', _artifactToFileName(artifact));
|
||||||
case Artifact.dart2jsSnapshot:
|
case Artifact.dart2jsSnapshot:
|
||||||
return _fileSystem.path.join(_dartSdkPath(_fileSystem), 'bin', 'snapshots', _artifactToFileName(artifact));
|
return _fileSystem.path.join(_dartSdkPath(_fileSystem), 'bin', 'snapshots', _artifactToFileName(artifact));
|
||||||
case Artifact.dartdevcSnapshot:
|
case Artifact.dartdevcSnapshot:
|
||||||
@ -380,6 +407,22 @@ class CachedArtifacts implements Artifacts {
|
|||||||
.childDirectory(getNameForTargetPlatform(platform))
|
.childDirectory(getNameForTargetPlatform(platform))
|
||||||
.childFile(_artifactToFileName(artifact, platform, mode))
|
.childFile(_artifactToFileName(artifact, platform, mode))
|
||||||
.path;
|
.path;
|
||||||
|
case Artifact.webPrecompiledSdk:
|
||||||
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', 'amd', _artifactToFileName(artifact, platform, mode));
|
||||||
|
case Artifact.webPrecompiledSdkSourcemaps:
|
||||||
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', 'amd', _artifactToFileName(artifact, platform, mode));
|
||||||
|
case Artifact.webPrecompiledCanvaskitSdk:
|
||||||
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', 'amd-canvaskit', _artifactToFileName(artifact, platform, mode));
|
||||||
|
case Artifact.webPrecompiledCanvaskitSdkSourcemaps:
|
||||||
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', 'amd-canvaskit', _artifactToFileName(artifact, platform, mode));
|
||||||
|
case Artifact.webPrecompiledSoundSdk:
|
||||||
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', 'amd-sound', _artifactToFileName(artifact, platform, mode));
|
||||||
|
case Artifact.webPrecompiledSoundSdkSourcemaps:
|
||||||
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', 'amd-sound', _artifactToFileName(artifact, platform, mode));
|
||||||
|
case Artifact.webPrecompiledCanvaskitSoundSdk:
|
||||||
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', 'amd-canvaskit-sound', _artifactToFileName(artifact, platform, mode));
|
||||||
|
case Artifact.webPrecompiledCanvaskitSoundSdkSourcemaps:
|
||||||
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', 'amd-canvaskit-sound', _artifactToFileName(artifact, platform, mode));
|
||||||
default:
|
default:
|
||||||
assert(false, 'Artifact $artifact not available for platform $platform.');
|
assert(false, 'Artifact $artifact not available for platform $platform.');
|
||||||
return null;
|
return null;
|
||||||
@ -542,6 +585,8 @@ class LocalEngineArtifacts implements Artifacts {
|
|||||||
return _fileSystem.path.join(_hostEngineOutPath, _artifactToFileName(artifact));
|
return _fileSystem.path.join(_hostEngineOutPath, _artifactToFileName(artifact));
|
||||||
case Artifact.webPlatformKernelDill:
|
case Artifact.webPlatformKernelDill:
|
||||||
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', _artifactToFileName(artifact));
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', _artifactToFileName(artifact));
|
||||||
|
case Artifact.webPlatformSoundKernelDill:
|
||||||
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', _artifactToFileName(artifact));
|
||||||
case Artifact.fuchsiaKernelCompiler:
|
case Artifact.fuchsiaKernelCompiler:
|
||||||
final String hostPlatform = getNameForHostPlatform(getCurrentHostPlatform());
|
final String hostPlatform = getNameForHostPlatform(getCurrentHostPlatform());
|
||||||
final String modeName = mode.isRelease ? 'release' : mode.toString();
|
final String modeName = mode.isRelease ? 'release' : mode.toString();
|
||||||
@ -557,6 +602,22 @@ class LocalEngineArtifacts implements Artifacts {
|
|||||||
return _fileSystem.path.join(_hostEngineOutPath, 'gen', artifactFileName);
|
return _fileSystem.path.join(_hostEngineOutPath, 'gen', artifactFileName);
|
||||||
case Artifact.flutterWebLibrariesJson:
|
case Artifact.flutterWebLibrariesJson:
|
||||||
return _fileSystem.path.join(_getFlutterWebSdkPath(), artifactFileName);
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), artifactFileName);
|
||||||
|
case Artifact.webPrecompiledSdk:
|
||||||
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', 'amd', artifactFileName);
|
||||||
|
case Artifact.webPrecompiledSdkSourcemaps:
|
||||||
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', 'amd', artifactFileName);
|
||||||
|
case Artifact.webPrecompiledCanvaskitSdk:
|
||||||
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', 'amd-canvaskit', artifactFileName);
|
||||||
|
case Artifact.webPrecompiledCanvaskitSdkSourcemaps:
|
||||||
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', 'amd-canvaskit', artifactFileName);
|
||||||
|
case Artifact.webPrecompiledSoundSdk:
|
||||||
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', 'amd-sound', artifactFileName);
|
||||||
|
case Artifact.webPrecompiledSoundSdkSourcemaps:
|
||||||
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', 'amd-sound', artifactFileName);
|
||||||
|
case Artifact.webPrecompiledCanvaskitSoundSdk:
|
||||||
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', 'amd-canvaskit-sound', artifactFileName);
|
||||||
|
case Artifact.webPrecompiledCanvaskitSoundSdkSourcemaps:
|
||||||
|
return _fileSystem.path.join(_getFlutterWebSdkPath(), 'kernel', 'amd-canvaskit-sound', artifactFileName);
|
||||||
}
|
}
|
||||||
assert(false, 'Invalid artifact $artifact.');
|
assert(false, 'Invalid artifact $artifact.');
|
||||||
return null;
|
return null;
|
||||||
|
@ -30,10 +30,16 @@ class BuildInfo {
|
|||||||
@required this.treeShakeIcons,
|
@required this.treeShakeIcons,
|
||||||
this.performanceMeasurementFile,
|
this.performanceMeasurementFile,
|
||||||
this.packagesPath = '.packages',
|
this.packagesPath = '.packages',
|
||||||
|
this.nullSafetyMode = NullSafetyMode.autodetect,
|
||||||
});
|
});
|
||||||
|
|
||||||
final BuildMode mode;
|
final BuildMode mode;
|
||||||
|
|
||||||
|
/// The null safety mode the application should be run in.
|
||||||
|
///
|
||||||
|
/// If not provided, defaults to [NullSafetyMode.autodetect].
|
||||||
|
final NullSafetyMode nullSafetyMode;
|
||||||
|
|
||||||
/// Whether the build should subdset icon fonts.
|
/// Whether the build should subdset icon fonts.
|
||||||
final bool treeShakeIcons;
|
final bool treeShakeIcons;
|
||||||
|
|
||||||
@ -688,3 +694,10 @@ List<String> decodeDartDefines(Map<String, String> environmentDefines, String ke
|
|||||||
.cast<String>()
|
.cast<String>()
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The null safety runtime mode the app should be built in.
|
||||||
|
enum NullSafetyMode {
|
||||||
|
sound,
|
||||||
|
unsound,
|
||||||
|
autodetect,
|
||||||
|
}
|
||||||
|
@ -103,6 +103,7 @@ class WebAssetServer implements AssetReader {
|
|||||||
this.internetAddress,
|
this.internetAddress,
|
||||||
this._modules,
|
this._modules,
|
||||||
this._digests,
|
this._digests,
|
||||||
|
this._buildInfo,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fallback to "application/octet-stream" on null which
|
// Fallback to "application/octet-stream" on null which
|
||||||
@ -167,6 +168,7 @@ class WebAssetServer implements AssetReader {
|
|||||||
address,
|
address,
|
||||||
modules,
|
modules,
|
||||||
digests,
|
digests,
|
||||||
|
buildInfo,
|
||||||
);
|
);
|
||||||
if (testMode) {
|
if (testMode) {
|
||||||
return server;
|
return server;
|
||||||
@ -263,6 +265,7 @@ class WebAssetServer implements AssetReader {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final BuildInfo _buildInfo;
|
||||||
final HttpServer _httpServer;
|
final HttpServer _httpServer;
|
||||||
// If holding these in memory is too much overhead, this can be switched to a
|
// If holding these in memory is too much overhead, this can be switched to a
|
||||||
// RandomAccessFile and read on demand.
|
// RandomAccessFile and read on demand.
|
||||||
@ -458,50 +461,31 @@ class WebAssetServer implements AssetReader {
|
|||||||
/// Whether to use the cavaskit SDK for rendering.
|
/// Whether to use the cavaskit SDK for rendering.
|
||||||
bool canvasKitRendering = false;
|
bool canvasKitRendering = false;
|
||||||
|
|
||||||
@visibleForTesting
|
|
||||||
final File dartSdk = globals.fs.file(globals.fs.path.join(
|
|
||||||
globals.artifacts.getArtifactPath(Artifact.flutterWebSdk),
|
|
||||||
'kernel',
|
|
||||||
'amd',
|
|
||||||
'dart_sdk.js',
|
|
||||||
));
|
|
||||||
|
|
||||||
@visibleForTesting
|
|
||||||
final File canvasKitDartSdk = globals.fs.file(globals.fs.path.join(
|
|
||||||
globals.artifacts.getArtifactPath(Artifact.flutterWebSdk),
|
|
||||||
'kernel',
|
|
||||||
'amd-canvaskit',
|
|
||||||
'dart_sdk.js',
|
|
||||||
));
|
|
||||||
|
|
||||||
@visibleForTesting
|
|
||||||
final File dartSdkSourcemap = globals.fs.file(globals.fs.path.join(
|
|
||||||
globals.artifacts.getArtifactPath(Artifact.flutterWebSdk),
|
|
||||||
'kernel',
|
|
||||||
'amd',
|
|
||||||
'dart_sdk.js.map',
|
|
||||||
));
|
|
||||||
|
|
||||||
@visibleForTesting
|
|
||||||
final File canvasKitDartSdkSourcemap = globals.fs.file(globals.fs.path.join(
|
|
||||||
globals.artifacts.getArtifactPath(Artifact.flutterWebSdk),
|
|
||||||
'kernel',
|
|
||||||
'amd-canvaskit',
|
|
||||||
'dart_sdk.js.map',
|
|
||||||
));
|
|
||||||
|
|
||||||
// Attempt to resolve `path` to a dart file.
|
// Attempt to resolve `path` to a dart file.
|
||||||
File _resolveDartFile(String path) {
|
File _resolveDartFile(String path) {
|
||||||
// Return the actual file objects so that local engine changes are automatically picked up.
|
// Return the actual file objects so that local engine changes are automatically picked up.
|
||||||
switch (path) {
|
switch (path) {
|
||||||
case 'dart_sdk.js':
|
case 'dart_sdk.js':
|
||||||
return canvasKitRendering
|
if (_buildInfo.nullSafetyMode == NullSafetyMode.unsound) {
|
||||||
? canvasKitDartSdk
|
return globals.fs.file(canvasKitRendering
|
||||||
: dartSdk;
|
? globals.artifacts.getArtifactPath(Artifact.webPrecompiledCanvaskitSdk)
|
||||||
|
: globals.artifacts.getArtifactPath(Artifact.webPrecompiledSdk));
|
||||||
|
} else {
|
||||||
|
return globals.fs.file(canvasKitRendering
|
||||||
|
? globals.artifacts.getArtifactPath(Artifact.webPrecompiledCanvaskitSoundSdk)
|
||||||
|
: globals.artifacts.getArtifactPath(Artifact.webPrecompiledSoundSdk));
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'dart_sdk.js.map':
|
case 'dart_sdk.js.map':
|
||||||
return canvasKitRendering
|
if (_buildInfo.nullSafetyMode == NullSafetyMode.unsound) {
|
||||||
? canvasKitDartSdkSourcemap
|
return globals.fs.file(canvasKitRendering
|
||||||
: dartSdkSourcemap;
|
? globals.artifacts.getArtifactPath(Artifact.webPrecompiledCanvaskitSdkSourcemaps)
|
||||||
|
: globals.artifacts.getArtifactPath(Artifact.webPrecompiledSdkSourcemaps));
|
||||||
|
} else {
|
||||||
|
return globals.fs.file(canvasKitRendering
|
||||||
|
? globals.artifacts.getArtifactPath(Artifact.webPrecompiledCanvaskitSoundSdkSourcemaps)
|
||||||
|
: globals.artifacts.getArtifactPath(Artifact.webPrecompiledSoundSdkSourcemaps));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// This is the special generated entrypoint.
|
// This is the special generated entrypoint.
|
||||||
if (path == 'web_entrypoint.dart') {
|
if (path == 'web_entrypoint.dart') {
|
||||||
|
@ -92,6 +92,20 @@ class FlutterDevice {
|
|||||||
// a warning message and dump some debug information which can be
|
// a warning message and dump some debug information which can be
|
||||||
// used to file a bug, but the compiler will still start up correctly.
|
// used to file a bug, but the compiler will still start up correctly.
|
||||||
if (targetPlatform == TargetPlatform.web_javascript) {
|
if (targetPlatform == TargetPlatform.web_javascript) {
|
||||||
|
Artifact platformDillArtifact;
|
||||||
|
List<String> extraFrontEndOptions;
|
||||||
|
if (buildInfo.nullSafetyMode == NullSafetyMode.unsound) {
|
||||||
|
platformDillArtifact = Artifact.webPlatformKernelDill;
|
||||||
|
extraFrontEndOptions = buildInfo.extraFrontEndOptions;
|
||||||
|
} else {
|
||||||
|
platformDillArtifact = Artifact.webPlatformSoundKernelDill;
|
||||||
|
extraFrontEndOptions = <String>[
|
||||||
|
...?buildInfo?.extraFrontEndOptions,
|
||||||
|
if (!(buildInfo?.extraFrontEndOptions?.contains('--sound-null-safety') ?? false))
|
||||||
|
'--sound-null-safety'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
generator = ResidentCompiler(
|
generator = ResidentCompiler(
|
||||||
globals.artifacts.getArtifactPath(Artifact.flutterWebSdk, mode: buildInfo.mode),
|
globals.artifacts.getArtifactPath(Artifact.flutterWebSdk, mode: buildInfo.mode),
|
||||||
buildMode: buildInfo.mode,
|
buildMode: buildInfo.mode,
|
||||||
@ -105,9 +119,9 @@ class FlutterDevice {
|
|||||||
dartDefines: buildInfo.dartDefines,
|
dartDefines: buildInfo.dartDefines,
|
||||||
),
|
),
|
||||||
targetModel: TargetModel.dartdevc,
|
targetModel: TargetModel.dartdevc,
|
||||||
extraFrontEndOptions: buildInfo.extraFrontEndOptions,
|
extraFrontEndOptions: extraFrontEndOptions,
|
||||||
platformDill: globals.fs.file(globals.artifacts
|
platformDill: globals.fs.file(globals.artifacts
|
||||||
.getArtifactPath(Artifact.webPlatformKernelDill, mode: buildInfo.mode))
|
.getArtifactPath(platformDillArtifact, mode: buildInfo.mode))
|
||||||
.absolute.uri.toString(),
|
.absolute.uri.toString(),
|
||||||
dartDefines: buildInfo.dartDefines,
|
dartDefines: buildInfo.dartDefines,
|
||||||
librariesSpec: globals.fs.file(globals.artifacts
|
librariesSpec: globals.fs.file(globals.artifacts
|
||||||
|
@ -479,8 +479,9 @@ abstract class FlutterCommand extends Command<void> {
|
|||||||
help:
|
help:
|
||||||
'Whether to override the inferred null safety mode. This allows null-safe '
|
'Whether to override the inferred null safety mode. This allows null-safe '
|
||||||
'libraries to depend on un-migrated (non-null safe) libraries. By default, '
|
'libraries to depend on un-migrated (non-null safe) libraries. By default, '
|
||||||
'Flutter applications will attempt to run at the null safety level of their '
|
'Flutter mobile & desktop applications will attempt to run at the null safety '
|
||||||
'entrypoint library (usually lib/main.dart).',
|
'level of their entrypoint library (usually lib/main.dart). Flutter web '
|
||||||
|
'applications will default to sound null-safety, unless specifically configured.',
|
||||||
defaultsTo: null,
|
defaultsTo: null,
|
||||||
hide: hide,
|
hide: hide,
|
||||||
);
|
);
|
||||||
@ -617,15 +618,20 @@ abstract class FlutterCommand extends Command<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NullSafetyMode nullSafetyMode = NullSafetyMode.unsound;
|
||||||
if (argParser.options.containsKey(FlutterOptions.kNullSafety)) {
|
if (argParser.options.containsKey(FlutterOptions.kNullSafety)) {
|
||||||
final bool nullSafety = boolArg(FlutterOptions.kNullSafety);
|
final bool nullSafety = boolArg(FlutterOptions.kNullSafety);
|
||||||
// Explicitly check for `true` and `false` so that `null` results in not
|
// Explicitly check for `true` and `false` so that `null` results in not
|
||||||
// passing a flag. This will use the automatically detected null-safety
|
// passing a flag. This will use the automatically detected null-safety
|
||||||
// value based on the entrypoint
|
// value based on the entrypoint
|
||||||
if (nullSafety == true) {
|
if (nullSafety == true) {
|
||||||
|
nullSafetyMode = NullSafetyMode.sound;
|
||||||
extraFrontEndOptions.add('--sound-null-safety');
|
extraFrontEndOptions.add('--sound-null-safety');
|
||||||
} else if (nullSafety == false) {
|
} else if (nullSafety == false) {
|
||||||
|
nullSafetyMode = NullSafetyMode.unsound;
|
||||||
extraFrontEndOptions.add('--no-sound-null-safety');
|
extraFrontEndOptions.add('--no-sound-null-safety');
|
||||||
|
} else if (extraFrontEndOptions.contains('--enable-experiment=non-nullable')) {
|
||||||
|
nullSafetyMode = NullSafetyMode.autodetect;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -685,7 +691,8 @@ abstract class FlutterCommand extends Command<void> {
|
|||||||
bundleSkSLPath: bundleSkSLPath,
|
bundleSkSLPath: bundleSkSLPath,
|
||||||
dartExperiments: experiments,
|
dartExperiments: experiments,
|
||||||
performanceMeasurementFile: performanceMeasurementFile,
|
performanceMeasurementFile: performanceMeasurementFile,
|
||||||
packagesPath: globalResults['packages'] as String ?? '.packages'
|
packagesPath: globalResults['packages'] as String ?? '.packages',
|
||||||
|
nullSafetyMode: nullSafetyMode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ Future<void> buildWeb(
|
|||||||
kCspMode: csp.toString(),
|
kCspMode: csp.toString(),
|
||||||
kIconTreeShakerFlag: buildInfo.treeShakeIcons.toString(),
|
kIconTreeShakerFlag: buildInfo.treeShakeIcons.toString(),
|
||||||
if (buildInfo.extraFrontEndOptions?.isNotEmpty ?? false)
|
if (buildInfo.extraFrontEndOptions?.isNotEmpty ?? false)
|
||||||
kExtraFrontEndOptions: buildInfo.extraFrontEndOptions.join(',')
|
kExtraFrontEndOptions: encodeDartDefines(buildInfo.extraFrontEndOptions),
|
||||||
},
|
},
|
||||||
artifacts: globals.artifacts,
|
artifacts: globals.artifacts,
|
||||||
fileSystem: globals.fs,
|
fileSystem: globals.fs,
|
||||||
|
@ -52,6 +52,41 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWithoutContext('precompiled web artifact paths are correct', () {
|
||||||
|
expect(
|
||||||
|
artifacts.getArtifactPath(Artifact.webPrecompiledSdk),
|
||||||
|
'root/bin/cache/flutter_web_sdk/kernel/amd/dart_sdk.js',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
artifacts.getArtifactPath(Artifact.webPrecompiledSdkSourcemaps),
|
||||||
|
'root/bin/cache/flutter_web_sdk/kernel/amd/dart_sdk.js.map',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
artifacts.getArtifactPath(Artifact.webPrecompiledCanvaskitSdk),
|
||||||
|
'root/bin/cache/flutter_web_sdk/kernel/amd-canvaskit/dart_sdk.js',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
artifacts.getArtifactPath(Artifact.webPrecompiledCanvaskitSdkSourcemaps),
|
||||||
|
'root/bin/cache/flutter_web_sdk/kernel/amd-canvaskit/dart_sdk.js.map',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
artifacts.getArtifactPath(Artifact.webPrecompiledSoundSdk),
|
||||||
|
'root/bin/cache/flutter_web_sdk/kernel/amd-sound/dart_sdk.js',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
artifacts.getArtifactPath(Artifact.webPrecompiledSoundSdkSourcemaps),
|
||||||
|
'root/bin/cache/flutter_web_sdk/kernel/amd-sound/dart_sdk.js.map',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
artifacts.getArtifactPath(Artifact.webPrecompiledCanvaskitSoundSdk),
|
||||||
|
'root/bin/cache/flutter_web_sdk/kernel/amd-canvaskit-sound/dart_sdk.js',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
artifacts.getArtifactPath(Artifact.webPrecompiledCanvaskitSoundSdkSourcemaps),
|
||||||
|
'root/bin/cache/flutter_web_sdk/kernel/amd-canvaskit-sound/dart_sdk.js.map',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWithoutContext('getEngineType', () {
|
testWithoutContext('getEngineType', () {
|
||||||
expect(
|
expect(
|
||||||
artifacts.getEngineType(TargetPlatform.android_arm, BuildMode.debug),
|
artifacts.getEngineType(TargetPlatform.android_arm, BuildMode.debug),
|
||||||
|
@ -1540,7 +1540,7 @@ void main() {
|
|||||||
expect(fakeVmServiceHost.hasRemainingExpectations, false);
|
expect(fakeVmServiceHost.hasRemainingExpectations, false);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
testUsingContext('FlutterDevice uses dartdevc configuration when targeting web', () => testbed.run(() async {
|
testUsingContext('FlutterDevice uses dartdevc configuration when targeting web', () async {
|
||||||
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
|
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
|
||||||
final MockDevice mockDevice = MockDevice();
|
final MockDevice mockDevice = MockDevice();
|
||||||
when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async {
|
when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async {
|
||||||
@ -1549,7 +1549,12 @@ void main() {
|
|||||||
|
|
||||||
final DefaultResidentCompiler residentCompiler = (await FlutterDevice.create(
|
final DefaultResidentCompiler residentCompiler = (await FlutterDevice.create(
|
||||||
mockDevice,
|
mockDevice,
|
||||||
buildInfo: BuildInfo.debug,
|
buildInfo: const BuildInfo(
|
||||||
|
BuildMode.debug,
|
||||||
|
'',
|
||||||
|
treeShakeIcons: false,
|
||||||
|
nullSafetyMode: NullSafetyMode.unsound,
|
||||||
|
),
|
||||||
flutterProject: FlutterProject.current(),
|
flutterProject: FlutterProject.current(),
|
||||||
target: null,
|
target: null,
|
||||||
)).generator as DefaultResidentCompiler;
|
)).generator as DefaultResidentCompiler;
|
||||||
@ -1562,12 +1567,46 @@ void main() {
|
|||||||
expect(residentCompiler.targetModel, TargetModel.dartdevc);
|
expect(residentCompiler.targetModel, TargetModel.dartdevc);
|
||||||
expect(residentCompiler.sdkRoot,
|
expect(residentCompiler.sdkRoot,
|
||||||
globals.artifacts.getArtifactPath(Artifact.flutterWebSdk, mode: BuildMode.debug) + '/');
|
globals.artifacts.getArtifactPath(Artifact.flutterWebSdk, mode: BuildMode.debug) + '/');
|
||||||
expect(
|
expect(residentCompiler.platformDill, 'file:///Artifact.webPlatformKernelDill.debug');
|
||||||
residentCompiler.platformDill,
|
}, overrides: <Type, Generator>{
|
||||||
globals.fs.file(globals.artifacts.getArtifactPath(Artifact.webPlatformKernelDill, mode: BuildMode.debug))
|
Artifacts: () => Artifacts.test(),
|
||||||
.absolute.uri.toString(),
|
FileSystem: () => MemoryFileSystem.test(),
|
||||||
);
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
}));
|
});
|
||||||
|
|
||||||
|
testUsingContext('FlutterDevice uses dartdevc configuration when targeting web with null-safety autodetected', () async {
|
||||||
|
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
|
||||||
|
final MockDevice mockDevice = MockDevice();
|
||||||
|
when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async {
|
||||||
|
return TargetPlatform.web_javascript;
|
||||||
|
});
|
||||||
|
|
||||||
|
final DefaultResidentCompiler residentCompiler = (await FlutterDevice.create(
|
||||||
|
mockDevice,
|
||||||
|
buildInfo: const BuildInfo(
|
||||||
|
BuildMode.debug,
|
||||||
|
'',
|
||||||
|
treeShakeIcons: false,
|
||||||
|
extraFrontEndOptions: <String>['--enable-experiment=non-nullable'],
|
||||||
|
),
|
||||||
|
flutterProject: FlutterProject.current(),
|
||||||
|
target: null,
|
||||||
|
)).generator as DefaultResidentCompiler;
|
||||||
|
|
||||||
|
expect(residentCompiler.initializeFromDill,
|
||||||
|
globals.fs.path.join(getBuildDirectory(), 'cache.dill'));
|
||||||
|
expect(residentCompiler.librariesSpec,
|
||||||
|
globals.fs.file(globals.artifacts.getArtifactPath(Artifact.flutterWebLibrariesJson))
|
||||||
|
.uri.toString());
|
||||||
|
expect(residentCompiler.targetModel, TargetModel.dartdevc);
|
||||||
|
expect(residentCompiler.sdkRoot,
|
||||||
|
globals.artifacts.getArtifactPath(Artifact.flutterWebSdk, mode: BuildMode.debug) + '/');
|
||||||
|
expect(residentCompiler.platformDill, 'file:///Artifact.webPlatformSoundKernelDill.debug');
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
Artifacts: () => Artifacts.test(),
|
||||||
|
FileSystem: () => MemoryFileSystem.test(),
|
||||||
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
|
});
|
||||||
|
|
||||||
testUsingContext('connect sets up log reader', () => testbed.run(() async {
|
testUsingContext('connect sets up log reader', () => testbed.run(() async {
|
||||||
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
|
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
|
||||||
|
@ -764,7 +764,7 @@ void main() {
|
|||||||
testUsingContext('web resident runner can toggle CanvasKit', () async {
|
testUsingContext('web resident runner can toggle CanvasKit', () async {
|
||||||
final ResidentRunner residentWebRunner = setUpResidentRunner(mockFlutterDevice);
|
final ResidentRunner residentWebRunner = setUpResidentRunner(mockFlutterDevice);
|
||||||
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
|
fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[]);
|
||||||
final WebAssetServer webAssetServer = WebAssetServer(null, null, null, null, null);
|
final WebAssetServer webAssetServer = WebAssetServer(null, null, null, null, null, null);
|
||||||
when(mockWebDevFS.webAssetServer).thenReturn(webAssetServer);
|
when(mockWebDevFS.webAssetServer).thenReturn(webAssetServer);
|
||||||
|
|
||||||
expect(residentWebRunner.supportsCanvasKit, true);
|
expect(residentWebRunner.supportsCanvasKit, true);
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dwds/dwds.dart';
|
import 'package:dwds/dwds.dart';
|
||||||
|
import 'package:flutter_tools/src/artifacts.dart';
|
||||||
import 'package:flutter_tools/src/base/file_system.dart';
|
import 'package:flutter_tools/src/base/file_system.dart';
|
||||||
import 'package:flutter_tools/src/base/io.dart';
|
import 'package:flutter_tools/src/base/io.dart';
|
||||||
import 'package:flutter_tools/src/base/platform.dart';
|
import 'package:flutter_tools/src/base/platform.dart';
|
||||||
@ -53,6 +54,8 @@ void main() {
|
|||||||
InternetAddress.loopbackIPv4,
|
InternetAddress.loopbackIPv4,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -416,7 +419,12 @@ void main() {
|
|||||||
packagesFilePath: '.packages',
|
packagesFilePath: '.packages',
|
||||||
urlTunneller: null,
|
urlTunneller: null,
|
||||||
useSseForDebugProxy: true,
|
useSseForDebugProxy: true,
|
||||||
buildInfo: BuildInfo.debug,
|
buildInfo: const BuildInfo(
|
||||||
|
BuildMode.debug,
|
||||||
|
'',
|
||||||
|
treeShakeIcons: false,
|
||||||
|
nullSafetyMode: NullSafetyMode.unsound,
|
||||||
|
),
|
||||||
enableDwds: false,
|
enableDwds: false,
|
||||||
entrypoint: Uri.base,
|
entrypoint: Uri.base,
|
||||||
testMode: true,
|
testMode: true,
|
||||||
@ -428,24 +436,31 @@ void main() {
|
|||||||
|
|
||||||
final Uri uri = await webDevFS.create();
|
final Uri uri = await webDevFS.create();
|
||||||
webDevFS.webAssetServer.entrypointCacheDirectory = globals.fs.currentDirectory;
|
webDevFS.webAssetServer.entrypointCacheDirectory = globals.fs.currentDirectory;
|
||||||
|
final String webPrecompiledSdk = globals.artifacts
|
||||||
|
.getArtifactPath(Artifact.webPrecompiledSdk);
|
||||||
|
final String webPrecompiledSdkSourcemaps = globals.artifacts
|
||||||
|
.getArtifactPath(Artifact.webPrecompiledSdkSourcemaps);
|
||||||
|
final String webPrecompiledCanvaskitSdk = globals.artifacts
|
||||||
|
.getArtifactPath(Artifact.webPrecompiledCanvaskitSdk);
|
||||||
|
final String webPrecompiledCanvaskitSdkSourcemaps = globals.artifacts
|
||||||
|
.getArtifactPath(Artifact.webPrecompiledCanvaskitSdkSourcemaps);
|
||||||
globals.fs.currentDirectory
|
globals.fs.currentDirectory
|
||||||
.childDirectory('lib')
|
.childDirectory('lib')
|
||||||
.childFile('web_entrypoint.dart')
|
.childFile('web_entrypoint.dart')
|
||||||
..createSync(recursive: true)
|
..createSync(recursive: true)
|
||||||
..writeAsStringSync('GENERATED');
|
..writeAsStringSync('GENERATED');
|
||||||
webDevFS.webAssetServer.dartSdk
|
globals.fs.file(webPrecompiledSdk)
|
||||||
..createSync(recursive: true)
|
..createSync(recursive: true)
|
||||||
..writeAsStringSync('HELLO');
|
..writeAsStringSync('HELLO');
|
||||||
webDevFS.webAssetServer.dartSdkSourcemap
|
globals.fs.file(webPrecompiledSdkSourcemaps)
|
||||||
..createSync(recursive: true)
|
..createSync(recursive: true)
|
||||||
..writeAsStringSync('THERE');
|
..writeAsStringSync('THERE');
|
||||||
webDevFS.webAssetServer.canvasKitDartSdk
|
globals.fs.file(webPrecompiledCanvaskitSdk)
|
||||||
..createSync(recursive: true)
|
..createSync(recursive: true)
|
||||||
..writeAsStringSync('OL');
|
..writeAsStringSync('OL');
|
||||||
webDevFS.webAssetServer.canvasKitDartSdkSourcemap
|
globals.fs.file(webPrecompiledCanvaskitSdkSourcemaps)
|
||||||
..createSync(recursive: true)
|
..createSync(recursive: true)
|
||||||
..writeAsStringSync('CHUM');
|
..writeAsStringSync('CHUM');
|
||||||
webDevFS.webAssetServer.dartSdkSourcemap.createSync(recursive: true);
|
|
||||||
|
|
||||||
await webDevFS.update(
|
await webDevFS.update(
|
||||||
mainUri: globals.fs.file(globals.fs.path.join('lib', 'main.dart')).uri,
|
mainUri: globals.fs.file(globals.fs.path.join('lib', 'main.dart')).uri,
|
||||||
@ -465,7 +480,7 @@ void main() {
|
|||||||
expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js.map'), 'THERE');
|
expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js.map'), 'THERE');
|
||||||
|
|
||||||
// Update to the SDK.
|
// Update to the SDK.
|
||||||
webDevFS.webAssetServer.dartSdk.writeAsStringSync('BELLOW');
|
globals.fs.file(webPrecompiledSdk).writeAsStringSync('BELLOW');
|
||||||
|
|
||||||
// New SDK should be visible..
|
// New SDK should be visible..
|
||||||
expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js'), 'BELLOW');
|
expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js'), 'BELLOW');
|
||||||
@ -483,6 +498,116 @@ void main() {
|
|||||||
expect(uri, Uri.http('localhost:0', ''));
|
expect(uri, Uri.http('localhost:0', ''));
|
||||||
|
|
||||||
await webDevFS.destroy();
|
await webDevFS.destroy();
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
Artifacts: () => Artifacts.test(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
test('Can start web server with specified assets in sound null safety mode', () => testbed.run(() async {
|
||||||
|
globals.fs.file('.packages').writeAsStringSync('\n');
|
||||||
|
final File outputFile = globals.fs.file(globals.fs.path.join('lib', 'main.dart'))
|
||||||
|
..createSync(recursive: true);
|
||||||
|
outputFile.parent.childFile('a.sources').writeAsStringSync('');
|
||||||
|
outputFile.parent.childFile('a.json').writeAsStringSync('{}');
|
||||||
|
outputFile.parent.childFile('a.map').writeAsStringSync('{}');
|
||||||
|
outputFile.parent.childFile('.packages').writeAsStringSync('\n');
|
||||||
|
|
||||||
|
final ResidentCompiler residentCompiler = MockResidentCompiler();
|
||||||
|
when(residentCompiler.recompile(
|
||||||
|
any,
|
||||||
|
any,
|
||||||
|
outputPath: anyNamed('outputPath'),
|
||||||
|
packageConfig: anyNamed('packageConfig'),
|
||||||
|
)).thenAnswer((Invocation invocation) async {
|
||||||
|
return const CompilerOutput('a', 0, <Uri>[]);
|
||||||
|
});
|
||||||
|
|
||||||
|
final WebDevFS webDevFS = WebDevFS(
|
||||||
|
hostname: 'localhost',
|
||||||
|
port: 0,
|
||||||
|
packagesFilePath: '.packages',
|
||||||
|
urlTunneller: null,
|
||||||
|
useSseForDebugProxy: true,
|
||||||
|
buildInfo: const BuildInfo(
|
||||||
|
BuildMode.debug,
|
||||||
|
'',
|
||||||
|
treeShakeIcons: false,
|
||||||
|
nullSafetyMode: NullSafetyMode.autodetect,
|
||||||
|
),
|
||||||
|
enableDwds: false,
|
||||||
|
entrypoint: Uri.base,
|
||||||
|
testMode: true,
|
||||||
|
expressionCompiler: null,
|
||||||
|
chromiumLauncher: null,
|
||||||
|
);
|
||||||
|
webDevFS.requireJS.createSync(recursive: true);
|
||||||
|
webDevFS.stackTraceMapper.createSync(recursive: true);
|
||||||
|
|
||||||
|
final Uri uri = await webDevFS.create();
|
||||||
|
webDevFS.webAssetServer.entrypointCacheDirectory = globals.fs.currentDirectory;
|
||||||
|
globals.fs.currentDirectory
|
||||||
|
.childDirectory('lib')
|
||||||
|
.childFile('web_entrypoint.dart')
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsStringSync('GENERATED');
|
||||||
|
final String webPrecompiledSoundSdk = globals.artifacts
|
||||||
|
.getArtifactPath(Artifact.webPrecompiledSoundSdk);
|
||||||
|
final String webPrecompiledSoundSdkSourcemaps = globals.artifacts
|
||||||
|
.getArtifactPath(Artifact.webPrecompiledSoundSdkSourcemaps);
|
||||||
|
final String webPrecompiledCanvaskitSoundSdk = globals.artifacts
|
||||||
|
.getArtifactPath(Artifact.webPrecompiledCanvaskitSoundSdk);
|
||||||
|
final String webPrecompiledCanvaskitSoundSdkSourcemaps = globals.artifacts
|
||||||
|
.getArtifactPath(Artifact.webPrecompiledCanvaskitSoundSdkSourcemaps);
|
||||||
|
globals.fs.file(webPrecompiledSoundSdk)
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsStringSync('HELLO');
|
||||||
|
globals.fs.file(webPrecompiledSoundSdkSourcemaps)
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsStringSync('THERE');
|
||||||
|
globals.fs.file(webPrecompiledCanvaskitSoundSdk)
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsStringSync('OL');
|
||||||
|
globals.fs.file(webPrecompiledCanvaskitSoundSdkSourcemaps)
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsStringSync('CHUM');
|
||||||
|
|
||||||
|
await webDevFS.update(
|
||||||
|
mainUri: globals.fs.file(globals.fs.path.join('lib', 'main.dart')).uri,
|
||||||
|
generator: residentCompiler,
|
||||||
|
trackWidgetCreation: true,
|
||||||
|
bundleFirstUpload: true,
|
||||||
|
invalidatedFiles: <Uri>[],
|
||||||
|
packageConfig: PackageConfig.empty,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(webDevFS.webAssetServer.getFile('require.js'), isNotNull);
|
||||||
|
expect(webDevFS.webAssetServer.getFile('stack_trace_mapper.js'), isNotNull);
|
||||||
|
expect(webDevFS.webAssetServer.getFile('main.dart'), isNotNull);
|
||||||
|
expect(webDevFS.webAssetServer.getFile('manifest.json'), isNotNull);
|
||||||
|
expect(webDevFS.webAssetServer.getFile('flutter_service_worker.js'), isNotNull);
|
||||||
|
expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js'), 'HELLO');
|
||||||
|
expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js.map'), 'THERE');
|
||||||
|
|
||||||
|
// Update to the SDK.
|
||||||
|
globals.fs.file(webPrecompiledSoundSdk).writeAsStringSync('BELLOW');
|
||||||
|
|
||||||
|
// New SDK should be visible..
|
||||||
|
expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js'), 'BELLOW');
|
||||||
|
|
||||||
|
// Toggle CanvasKit
|
||||||
|
webDevFS.webAssetServer.canvasKitRendering = true;
|
||||||
|
expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js'), 'OL');
|
||||||
|
expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js.map'), 'CHUM');
|
||||||
|
|
||||||
|
// Generated entrypoint.
|
||||||
|
expect(await webDevFS.webAssetServer.dartSourceContents('web_entrypoint.dart'),
|
||||||
|
contains('GENERATED'));
|
||||||
|
|
||||||
|
// served on localhost
|
||||||
|
expect(uri, Uri.http('localhost:0', ''));
|
||||||
|
|
||||||
|
await webDevFS.destroy();
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
Artifacts: () => Artifacts.test(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
test('Can start web server with hostname any', () => testbed.run(() async {
|
test('Can start web server with hostname any', () => testbed.run(() async {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user