blob: d3b7fa8a3e199dd81fc889bb09ab79bed9c001c3 [file] [log] [blame] [edit]
// Copyright 2020 The Dart Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Note: this is a copy from flutter tools, updated to work with dwds tests
import 'dart:convert';
import 'dart:io';
import 'package:dwds/asset_reader.dart';
import 'package:dwds/config.dart';
import 'package:dwds/expression_compiler.dart';
// ignore: implementation_imports
import 'package:dwds/src/debugging/metadata/module_metadata.dart';
import 'package:dwds/utilities.dart';
import 'package:file/file.dart';
import 'package:path/path.dart' as p;
import 'package:test_common/test_sdk_layout.dart';
import 'asset_server.dart';
import 'bootstrap.dart';
import 'frontend_server_client.dart';
class WebDevFS {
WebDevFS({
required this.fileSystem,
required this.hostname,
required this.port,
required this.projectDirectory,
required this.packageUriMapper,
required this.index,
this.soundNullSafety = true,
this.urlTunneler,
required this.sdkLayout,
required this.compilerOptions,
});
final FileSystem fileSystem;
late final TestAssetServer assetServer;
final String hostname;
final int port;
final Uri projectDirectory;
final PackageUriMapper packageUriMapper;
final String index;
final UrlEncoder? urlTunneler;
List<Uri> sources = <Uri>[];
DateTime? lastCompiled;
@Deprecated('Only sound null safety is supported as of Dart 3.0')
final bool soundNullSafety;
final TestSdkLayout sdkLayout;
final CompilerOptions compilerOptions;
Future<Uri> create() async {
assetServer = await TestAssetServer.start(
sdkLayout.sdkDirectory,
projectDirectory,
fileSystem,
index,
hostname,
port,
urlTunneler,
packageUriMapper,
);
return Uri.parse('http://$hostname:$port');
}
Future<void> dispose() {
return assetServer.close();
}
Future<UpdateFSReport> update({
required Uri mainUri,
required String dillOutputPath,
required ResidentCompiler generator,
required List<Uri> invalidatedFiles,
required bool initialCompile,
required bool fullRestart,
// The uri of the `HttpServer` that handles file requests.
// TODO(srujzs): This should be the same as the uri of the AssetServer to
// align with Flutter tools, but currently is not. Delete when that's fixed.
required Uri? fileServerUri,
}) async {
final mainPath = mainUri.toFilePath();
final outputDirectory = fileSystem.directory(
fileSystem.file(projectDirectory.resolve(mainPath)).parent.path,
);
final entryPoint = mainUri.toString();
var prefix = '';
// If base path is not overwritten, use main's subdirectory
// to store all files, so the paths match the requests.
if (assetServer.basePath.isEmpty) {
final directory = p.dirname(entryPoint);
prefix = '$directory/';
}
if (initialCompile) {
final ddcModuleLoader = '${prefix}ddc_module_loader.js';
final require = '${prefix}require.js';
final stackMapper = '${prefix}stack_trace_mapper.js';
final main = '${prefix}main.dart.js';
final bootstrap = '${prefix}main_module.bootstrap.js';
assetServer.writeFile(
entryPoint,
fileSystem.file(projectDirectory.resolve(mainPath)).readAsStringSync(),
);
assetServer.writeFile(stackMapper, stackTraceMapper.readAsStringSync());
switch (ddcModuleFormat) {
case ModuleFormat.amd:
assetServer.writeFile(require, requireJS.readAsStringSync());
assetServer.writeFile(
main,
generateBootstrapScript(
requireUrl: 'require.js',
mapperUrl: 'stack_trace_mapper.js',
entrypoint: entryPoint,
),
);
assetServer.writeFile(
bootstrap,
generateMainModule(entrypoint: entryPoint),
);
break;
case ModuleFormat.ddc:
assetServer.writeFile(
ddcModuleLoader,
ddcModuleLoaderJS.readAsStringSync(),
);
String bootstrapper;
String mainModule;
if (compilerOptions.canaryFeatures) {
bootstrapper = generateDDCLibraryBundleBootstrapScript(
ddcModuleLoaderUrl: ddcModuleLoader,
mapperUrl: stackMapper,
entrypoint: entryPoint,
bootstrapUrl: bootstrap,
);
const onLoadEndBootstrap = 'on_load_end_bootstrap.js';
assetServer.writeFile(
onLoadEndBootstrap,
generateDDCLibraryBundleOnLoadEndBootstrap(),
);
mainModule = generateDDCLibraryBundleMainModule(
entrypoint: entryPoint,
onLoadEndBootstrap: onLoadEndBootstrap,
);
} else {
bootstrapper = generateDDCBootstrapScript(
ddcModuleLoaderUrl: ddcModuleLoader,
mapperUrl: stackMapper,
entrypoint: entryPoint,
bootstrapUrl: bootstrap,
);
// DDC uses a simple heuristic to determine exported identifier
// names. The module name (entrypoint name here) has its extension
// removed, and special path elements like '/', '\', and '..' are
// replaced with
// '__'.
final exportedMainName = pathToJSIdentifier(
entryPoint.split('.')[0],
);
mainModule = generateDDCMainModule(
entrypoint: entryPoint,
exportedMain: exportedMainName,
);
}
assetServer.writeFile(main, bootstrapper);
assetServer.writeFile(bootstrap, mainModule);
break;
default:
throw Exception('Unsupported DDC module format $ddcModuleFormat.');
}
assetServer.writeFile('main_module.digests', '{}');
final sdk = dartSdk;
final sdkSourceMap = dartSdkSourcemap;
assetServer.writeFile('dart_sdk.js', sdk.readAsStringSync());
assetServer.writeFile('dart_sdk.js.map', sdkSourceMap.readAsStringSync());
generator.reset();
}
final compilerOutput = await generator.recompile(
Uri.parse('org-dartlang-app:///$mainUri'),
invalidatedFiles,
outputPath: p.join(dillOutputPath, 'app.dill'),
packageConfig: packageUriMapper.packageConfig,
recompileRestart: fullRestart,
);
if (compilerOutput == null || compilerOutput.errorCount > 0) {
return UpdateFSReport(success: false);
}
sources = compilerOutput.sources;
lastCompiled = DateTime.now();
File codeFile;
File manifestFile;
File sourcemapFile;
File metadataFile;
List<String> modules;
try {
codeFile = outputDirectory.childFile(
'${compilerOutput.outputFilename}.sources',
);
manifestFile = outputDirectory.childFile(
'${compilerOutput.outputFilename}.json',
);
sourcemapFile = outputDirectory.childFile(
'${compilerOutput.outputFilename}.map',
);
metadataFile = outputDirectory.childFile(
'${compilerOutput.outputFilename}.metadata',
);
modules = assetServer.write(
codeFile,
manifestFile,
sourcemapFile,
metadataFile,
);
} on FileSystemException catch (err) {
throw Exception('Failed to load recompiled sources:\n$err');
}
if (ddcModuleFormat == ModuleFormat.ddc &&
compilerOptions.canaryFeatures &&
!initialCompile) {
if (fullRestart) {
performRestart(modules, fileServerUri!);
} else {
performReload(modules, prefix, fileServerUri!);
}
}
return UpdateFSReport(
success: true,
syncedBytes: codeFile.lengthSync(),
invalidatedSourcesCount: invalidatedFiles.length,
)..invalidatedModules = modules;
}
/// Given a list of [modules] that need to be loaded, writes a list of sources
/// mapped to their ids to the file system that can then be consumed by the
/// hot restart callback.
///
/// For example:
/// ```json
/// [
/// {
/// "src": "<base_uri>/<file_name>",
/// "id": "<id>",
/// },
/// ]
/// ```
void performRestart(List<String> modules, Uri fileServerUri) {
final srcIdsList = <Map<String, String>>[];
for (final src in modules) {
srcIdsList.add(<String, String>{'src': '$fileServerUri/$src', 'id': src});
}
assetServer.writeFile('restart_scripts.json', json.encode(srcIdsList));
}
static const String reloadScriptsFileName = 'reload_scripts.json';
/// Given a list of [modules] that need to be reloaded, writes a file that
/// contains a list of objects each with three fields:
///
/// `src`: A string that corresponds to the file path containing a DDC library
/// bundle.
/// `module`: The name of the library bundle in `src`.
/// `libraries`: An array of strings containing the libraries that were
/// compiled in `src`.
///
/// For example:
/// ```json
/// [
/// {
/// "src": "<base_uri>/<file_name>",
/// "module": "<module_name>",
/// "libraries": ["<lib1>", "<lib2>"],
/// },
/// ]
/// ```
///
/// The path of the output file should stay consistent across the lifetime of
/// the app.
///
/// [entrypointDirectory] is used to make the module paths relative to the
/// entrypoint, which is needed in order to load `src`s correctly.
void performReload(
List<String> modules,
String entrypointDirectory,
Uri fileServerUri,
) {
final moduleToLibrary = <Map<String, Object>>[];
for (final module in modules) {
final metadata = ModuleMetadata.fromJson(
json.decode(
utf8.decode(assetServer.getMetadata('$module.metadata').toList()),
)
as Map<String, dynamic>,
);
final libraries = metadata.libraries.keys.toList();
moduleToLibrary.add(<String, Object>{
'src': '$fileServerUri/$module',
'module': metadata.name,
'libraries': libraries,
});
}
assetServer.writeFile(reloadScriptsFileName, json.encode(moduleToLibrary));
}
File get ddcModuleLoaderJS =>
fileSystem.file(sdkLayout.ddcModuleLoaderJsPath);
File get requireJS => fileSystem.file(sdkLayout.requireJsPath);
File get dartSdk => fileSystem.file(switch (ddcModuleFormat) {
ModuleFormat.amd => sdkLayout.amdJsPath,
ModuleFormat.ddc => sdkLayout.ddcJsPath,
_ => throw Exception('Unsupported DDC module format $ddcModuleFormat.'),
});
File get dartSdkSourcemap => fileSystem.file(switch (ddcModuleFormat) {
ModuleFormat.amd => sdkLayout.amdJsMapPath,
ModuleFormat.ddc => sdkLayout.ddcJsMapPath,
_ => throw Exception('Unsupported DDC module format $ddcModuleFormat.'),
});
File get stackTraceMapper => fileSystem.file(sdkLayout.stackTraceMapperPath);
ModuleFormat get ddcModuleFormat => compilerOptions.moduleFormat;
}
class UpdateFSReport {
final bool _success;
final int _invalidatedSourcesCount;
final int _syncedBytes;
UpdateFSReport({
bool success = false,
int invalidatedSourcesCount = 0,
int syncedBytes = 0,
}) : _success = success,
_invalidatedSourcesCount = invalidatedSourcesCount,
_syncedBytes = syncedBytes;
bool get success => _success;
int get invalidatedSourcesCount => _invalidatedSourcesCount;
int get syncedBytes => _syncedBytes;
/// JavaScript modules produced by the incremental compiler in `dartdevc`
/// mode.
///
/// Only used for JavaScript compilation.
List<String>? invalidatedModules;
}
/// The result of an invalidation check from [ProjectFileInvalidator].
class InvalidationResult {
const InvalidationResult({this.uris});
final List<Uri>? uris;
}
/// The [ProjectFileInvalidator] track the dependencies for a running
/// application to determine when they are dirty.
class ProjectFileInvalidator {
ProjectFileInvalidator({required FileSystem fileSystem})
: _fileSystem = fileSystem;
final FileSystem _fileSystem;
static const String _pubCachePathLinuxAndMac = '.pub-cache';
static const String _pubCachePathWindows = 'Pub/Cache';
Future<InvalidationResult> findInvalidated({
required DateTime? lastCompiled,
required List<Uri> urisToMonitor,
required String packagesPath,
}) async {
if (lastCompiled == null) {
// Initial load.
assert(urisToMonitor.isEmpty);
return InvalidationResult(uris: <Uri>[]);
}
final urisToScan = <Uri>[
// Don't watch pub cache directories to speed things up a little.
for (final Uri uri in urisToMonitor)
if (_isNotInPubCache(uri)) uri,
];
final invalidatedFiles = <Uri>[];
for (final uri in urisToScan) {
// Calling fs.statSync() is more performant than fs.file().statSync(),
// but uri.toFilePath() does not work with MultiRootFileSystem.
final updatedAt = uri.hasScheme && uri.scheme != 'file'
? _fileSystem.file(uri).statSync().modified
: _fileSystem
.statSync(uri.toFilePath(windows: Platform.isWindows))
.modified;
if (updatedAt.isAfter(lastCompiled)) {
invalidatedFiles.add(uri);
}
}
// We need to check the .dart_tool/package_config.json file too since it is
// not used in compilation.
final packageFile = _fileSystem.file(packagesPath);
final packageUri = packageFile.uri;
final updatedAt = packageFile.statSync().modified;
if (updatedAt.isAfter(lastCompiled)) {
invalidatedFiles.add(packageUri);
}
return InvalidationResult(uris: invalidatedFiles);
}
bool _isNotInPubCache(Uri uri) {
return !(Platform.isWindows && uri.path.contains(_pubCachePathWindows)) &&
!uri.path.contains(_pubCachePathLinuxAndMac);
}
}