Skip to content

Commit

Permalink
Add hot reload option to DartPad UI.
Browse files Browse the repository at this point in the history
  • Loading branch information
natebiggs committed Jan 24, 2025
1 parent 0c2868d commit 4c4857f
Show file tree
Hide file tree
Showing 16 changed files with 589 additions and 160 deletions.
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
93 changes: 68 additions & 25 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 @@ -32,13 +33,15 @@ class Compiler {
}) : this._(sdk, path.join(sdk.dartSdkPath, 'bin', 'dart'), storageBucket);

Compiler._(this._sdk, this._dartPath, this._storageBucket)
: _ddcDriver = BazelWorkerDriver(
() => Process.start(_dartPath, [
path.join(_sdk.dartSdkPath, 'bin', 'snapshots',
'dartdevc.dart.snapshot'),
'--persistent_worker'
]),
maxWorkers: 1),
: _ddcDriver = BazelWorkerDriver(() async {
final p = await Process.start(_dartPath, [
path.join(
_sdk.dartSdkPath, 'bin', 'snapshots', 'dartdevc.dart.snapshot'),
'--persistent_worker'
]);
p.stderr.listen((e) => print(utf8.decode(e)));
return p;
}, maxWorkers: 1),
_projectTemplates = ProjectTemplates.projectTemplates;

/// Compile the given string and return the resulting [CompilationResults].
Expand Down Expand Up @@ -104,7 +107,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 +130,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',
'--new-reload-delta-kernel=$newDeltaKernelPath',
if (oldDillPath != null) '--old-reload-delta-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 +180,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 +214,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 +266,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
Loading

0 comments on commit 4c4857f

Please sign in to comment.