Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hot reload capabilities to DartPad UI #3128

Merged
merged 11 commits into from
Feb 12, 2025
9 changes: 9 additions & 0 deletions pkgs/dart_services/lib/src/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,22 @@ void main() {
// https://github.com/flutter/flutter/blob/master/packages/flutter_tools/lib/src/web/bootstrap.dart#L236.
const kBootstrapFlutterCode = r'''
import 'dart:ui_web' as ui_web;
import 'dart:js_interop';
import 'dart:js_interop_unsafe';

import 'package:flutter_web_plugins/flutter_web_plugins.dart';

import 'generated_plugin_registrant.dart' as pluginRegistrant;
import 'main.dart' as entrypoint;

@JS('window')
external JSObject get _window;

Future<void> main() async {
// Mock DWDS indicators to allow Flutter to register hot reload 'reassemble'
// extension.
_window[r'$dwdsVersion'] = true.toJS;
_window[r'$emitRegisterEvent'] = ((String _) {}).toJS;
await ui_web.bootstrapEngine(
runApp: () {
entrypoint.main();
Expand Down
36 changes: 32 additions & 4 deletions pkgs/dart_services/lib/src/common_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ class CommonServerApi {
router.post(r'/api/<apiVersion>/analyze', handleAnalyze);
router.post(r'/api/<apiVersion>/compile', handleCompile);
router.post(r'/api/<apiVersion>/compileDDC', handleCompileDDC);
router.post(r'/api/<apiVersion>/compileNewDDC', handleCompileNewDDC);
router.post(
r'/api/<apiVersion>/compileNewDDCReload', handleCompileNewDDCReload);
router.post(r'/api/<apiVersion>/complete', handleComplete);
router.post(r'/api/<apiVersion>/fixes', handleFixes);
router.post(r'/api/<apiVersion>/format', handleFormat);
Expand Down Expand Up @@ -127,14 +130,18 @@ class CommonServerApi {
}
}

Future<Response> handleCompileDDC(Request request, String apiVersion) async {
Future<Response> _handleCompileDDC(
Request request,
String apiVersion,
Future<DDCCompilationResults> Function(api.CompileRequest)
compile) async {
if (apiVersion != api3) return unhandledVersion(apiVersion);

final sourceRequest =
api.SourceRequest.fromJson(await request.readAsJson());
final compileRequest =
api.CompileRequest.fromJson(await request.readAsJson());

final results = await serialize(() {
return impl.compiler.compileDDC(sourceRequest.source);
return compile(compileRequest);
});

if (results.hasOutput) {
Expand All @@ -144,13 +151,34 @@ class CommonServerApi {
}
return ok(api.CompileDDCResponse(
result: results.compiledJS!,
deltaDill: results.deltaDill,
modulesBaseUrl: modulesBaseUrl,
).toJson());
} else {
return failure(results.problems.map((p) => p.message).join('\n'));
}
}

Future<Response> handleCompileDDC(Request request, String apiVersion) async {
return await _handleCompileDDC(request, apiVersion,
(request) => impl.compiler.compileDDC(request.source));
}

Future<Response> handleCompileNewDDC(
Request request, String apiVersion) async {
return await _handleCompileDDC(request, apiVersion,
(request) => impl.compiler.compileNewDDC(request.source));
}

Future<Response> handleCompileNewDDCReload(
Request request, String apiVersion) async {
return await _handleCompileDDC(
request,
apiVersion,
(request) => impl.compiler
.compileNewDDCReload(request.source, request.deltaDill!));
}

Future<Response> handleComplete(Request request, String apiVersion) async {
if (apiVersion != api3) return unhandledVersion(apiVersion);

Expand Down
77 changes: 59 additions & 18 deletions pkgs/dart_services/lib/src/compiling.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:bazel_worker/driver.dart';
Expand Down Expand Up @@ -104,7 +105,8 @@ class Compiler {
}

/// Compile the given string and return the resulting [DDCCompilationResults].
Future<DDCCompilationResults> compileDDC(String source) async {
Future<DDCCompilationResults> _compileDDC(String source,
{String? deltaDill, required bool useNew}) async {
final imports = getAllImportsFor(source);

final temp = Directory.systemTemp.createTempSync('dartpad');
Expand All @@ -126,32 +128,48 @@ class Compiler {

File(bootstrapPath).writeAsStringSync(bootstrapContents);
File(path.join(temp.path, 'lib', kMainDart)).writeAsStringSync(source);
final newDeltaKernelPath = path.join(temp.path, 'new_kernel.dill');
String? oldDillPath;
if (deltaDill != null) {
final oldDillBytes = base64Decode(deltaDill);
oldDillPath = path.join(temp.path, 'old_kernel.dill');
File(oldDillPath)
..createSync()
..writeAsBytesSync(oldDillBytes);
}

final mainJsPath = path.join(temp.path, '$kMainDart.js');

final arguments = <String>[
'--modules=amd',
if (useNew) ...[
'--modules=ddc',
'--canary',
'--reload-delta-kernel=$newDeltaKernelPath',
if (oldDillPath != null) '--reload-last-accepted-kernel=$oldDillPath',
],
if (!useNew) ...[
'--modules=amd',
'--module-name=dartpad_main',
],
'--no-summarize',
if (usingFlutter) ...[
'-s',
_projectTemplates.summaryFilePath,
'-s',
'${_sdk.flutterWebSdkPath}/ddc_outline_sound.dill',
],
...['-o', path.join(temp.path, '$kMainDart.js')],
...['--module-name', 'dartpad_main'],
...['-o', mainJsPath],
'--enable-asserts',
if (_sdk.experiments.isNotEmpty)
'--enable-experiment=${_sdk.experiments.join(",")}',
bootstrapPath,
'--packages=${path.join(temp.path, '.dart_tool', 'package_config.json')}',
];

final mainJs = File(path.join(temp.path, '$kMainDart.js'));

_logger.fine('About to exec dartdevc worker: ${arguments.join(' ')}"');

final response =
await _ddcDriver.doWork(WorkRequest(arguments: arguments));

if (response.exitCode != 0) {
if (response.output.contains("Undefined name 'main'")) {
return DDCCompilationResults._missingMain;
Expand All @@ -160,18 +178,26 @@ class Compiler {
CompilationProblem._(_rewritePaths(response.output)),
]);
} else {
// The `--single-out-file` option for dartdevc was removed in v2.7.0. As
// a result, the JS code produced above does *not* provide a name for
// the module it contains. That's a problem for DartPad, since it's
// adding the code to a script tag in an iframe rather than loading it
// as an individual file from baseURL. As a workaround, this replace
// statement injects a name into the module definition.
final processedJs = mainJs
.readAsStringSync()
.replaceFirst('define([', "define('dartpad_main', [");
final mainJs = File(mainJsPath);
final newDeltaDill = File(newDeltaKernelPath);

var compiledJs = mainJs.readAsStringSync();

if (!useNew) {
// The `--single-out-file` option for dartdevc was removed in v2.7.0. As
// a result, the JS code produced above does *not* provide a name for
// the module it contains. That's a problem for DartPad, since it's
// adding the code to a script tag in an iframe rather than loading it
// as an individual file from baseURL. As a workaround, this replace
// statement injects a name into the module definition.
compiledJs =
compiledJs.replaceFirst('define([', "define('dartpad_main', [");
}

final results = DDCCompilationResults(
compiledJS: processedJs,
compiledJS: compiledJs,
deltaDill:
useNew ? base64Encode(newDeltaDill.readAsBytesSync()) : null,
modulesBaseUrl: 'https://storage.googleapis.com/$_storageBucket'
'/${_sdk.dartVersion}/',
);
Expand All @@ -186,6 +212,19 @@ class Compiler {
}
}

Future<DDCCompilationResults> compileDDC(String source) async {
return await _compileDDC(source, useNew: false);
}

Future<DDCCompilationResults> compileNewDDC(String source) async {
return await _compileDDC(source, useNew: true);
}

Future<DDCCompilationResults> compileNewDDCReload(
String source, String deltaDill) async {
return await _compileDDC(source, deltaDill: deltaDill, useNew: true);
}

Future<void> dispose() async {
return _ddcDriver.terminateWorkers();
}
Expand Down Expand Up @@ -225,14 +264,16 @@ class DDCCompilationResults {
]);

final String? compiledJS;
final String? deltaDill;
final String? modulesBaseUrl;
final List<CompilationProblem> problems;

DDCCompilationResults({this.compiledJS, this.modulesBaseUrl})
DDCCompilationResults({this.compiledJS, this.deltaDill, this.modulesBaseUrl})
: problems = const <CompilationProblem>[];

const DDCCompilationResults.failed(this.problems)
: compiledJS = null,
deltaDill = null,
modulesBaseUrl = null;

bool get hasOutput => compiledJS != null && compiledJS!.isNotEmpty;
Expand Down
14 changes: 14 additions & 0 deletions pkgs/dart_services/lib/src/sdk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,18 @@ final class Sdk {

return true;
}

static final _dartVersionMatch = RegExp(r'([0-9]+).([0-9]+)');

int get dartMajorVersion {
final dartVersionString =
_dartVersionMatch.firstMatch(dartVersion)!.group(1)!;
return int.parse(dartVersionString);
}

int get dartMinorVersion {
final dartVersionString =
_dartVersionMatch.firstMatch(dartVersion)!.group(2)!;
return int.parse(dartVersionString);
}
}
Loading