Skip to content
Open
2 changes: 1 addition & 1 deletion examples/backend_serverpod/example_client/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,4 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.6.0 <4.0.0"
dart: ">=3.8.0 <4.0.0"
1 change: 1 addition & 0 deletions examples/flutter_embedding/lib/main.server.options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ import 'package:flutter_embedding_demo/components/app.dart' as _app;
ServerOptions get defaultServerOptions => ServerOptions(
clientId: 'main.client.dart.js',
clients: {_app.App: ClientTarget<_app.App>('app')},
styles: () => [],
);
4 changes: 4 additions & 0 deletions packages/jaspr/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## Unreleased breaking

- Added hot-reload support.

## 0.23.1

- Fixed expression compilation when debugging a client-side application with an AOT installed CLI.
Expand Down
12 changes: 12 additions & 0 deletions packages/jaspr/lib/src/client/client_binding.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:developer';

import 'package:universal_web/js_interop.dart';
import 'package:universal_web/web.dart' as web;
Expand All @@ -13,6 +14,17 @@ class ClientAppBinding extends AppBinding with ComponentsBinding {
@override
bool get isClient => true;

ClientAppBinding() {
assert(() {
registerExtension('ext.jaspr.reassemble', (method, parameters) async {
// ignore: invalid_use_of_protected_member
rootElement?.visitChildren((element) => element.reassemble());
return ServiceExtensionResponse.result('{}');
});
return true;
}());
}

static final String _baseOrigin = () {
final base = web.document.querySelector('head>base') as web.HTMLBaseElement?;
return base?.href ?? web.window.location.origin;
Expand Down
15 changes: 15 additions & 0 deletions packages/jaspr/lib/src/framework/framework.dart
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,21 @@ abstract class Element implements BuildContext {
}
}

/// Called whenever the application is reassembled during debugging, for
/// example during hot reload.
///
/// This method should rerun any initialization logic that depends on global
/// state, for example, image loading from asset bundles (since the asset
/// bundle may have changed).
@mustCallSuper
@protected
void reassemble() {
markNeedsBuild();
visitChildren((Element child) {
child.reassemble();
});
}

void _updateDepth(int parentDepth) {
final int expectedDepth = parentDepth + 1;
if (depth < expectedDepth) {
Expand Down
23 changes: 23 additions & 0 deletions packages/jaspr/lib/src/framework/stateful_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,23 @@ abstract class State<T extends StatefulComponent> {
@protected
@mustCallSuper
void didChangeDependencies() {}

/// Called whenever the application is reassembled during debugging, for
/// example during hot reload.
///
/// This method should rerun any initialization logic that depends on global
/// state, for example, image loading from asset bundles (since the asset
/// bundle may have changed).
///
/// This function will only be called during development. In release builds,
/// the `ext.jaspr.reassemble` hook is not available, and so this code will
/// never execute.
///
/// Implementations of this method should end with a call to the inherited
/// method, as in `super.reassemble()`.
@protected
@mustCallSuper
void reassemble() {}
}

/// Mixin on [State] that preloads state on the server
Expand Down Expand Up @@ -727,6 +744,12 @@ class StatefulElement extends BuildableElement {
super.deactivate();
}

@override
void reassemble() {
state.reassemble();
super.reassemble();
}

@override
void unmount() {
super.unmount();
Expand Down
22 changes: 18 additions & 4 deletions packages/jaspr/lib/src/server/server_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:io';

import 'package:http/http.dart' as http;
import 'package:http/retry.dart' as retry;
import 'package:path/path.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_gzip/shelf_gzip.dart';
Expand Down Expand Up @@ -80,7 +81,12 @@ Handler createHandler(
var isAllowedPath = false;
final segment = request.url.pathSegments.lastOrNull ?? '';
if (!segment.contains('.')) {
isAllowedPath = true;
if (kDebugMode) {
isAllowedPath =
!request.url.path.contains('dwds') && !request.url.path.startsWith(r'$') && request.url.path != 'null';
} else {
isAllowedPath = true;
}
} else {
final suffix = segment.split('.').last;
if (Jaspr.allowedPathSuffixes.contains(suffix)) {
Expand Down Expand Up @@ -117,9 +123,17 @@ Future<String?> Function(String) proxyFileLoader(Request req, Handler proxyHandl
}

Handler createProxyHandler(http.Client? client) {
final handler = proxyHandler('http://localhost:$jasprProxyPort/', client: client);
// Determine and pass the base path to the proxy handler so it can rewrite DWDS handler paths correctly.
return (req) => handler(req.change(headers: {'jaspr_base_path': req.handlerPath}));
final c = retry.RetryClient(client ?? http.Client(), whenError: (e, _) => e is http.ClientException);
final handler = proxyHandler('http://localhost:$jasprProxyPort/', client: c);
return (req) async {
try {
return await handler(req);
} on http.ClientException {
return Response(503, headers: {'Retry-After': '1'});
} on SocketException {
return Response(503, headers: {'Retry-After': '1'});
}
};
}

// coverage:ignore-start
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ class ClientOptionsBuilder implements Builder {
}
}

bool _triedRunningFlutter = false;

Future<List<Plugin>> loadWebPlugins(BuildStep buildStep) async {
final pluginsDependenciesId = AssetId(buildStep.inputId.package, '.flutter-plugins-dependencies');

Expand All @@ -136,7 +138,8 @@ Future<List<Plugin>> loadWebPlugins(BuildStep buildStep) async {

var content = await readPluginsDependencies();

if (content == null) {
if (content == null && !_triedRunningFlutter) {
_triedRunningFlutter = true;
try {
final result = await Process.run('flutter', ['packages', 'get']);
if (result.exitCode != 0) {
Expand Down
50 changes: 43 additions & 7 deletions packages/jaspr_cli/lib/src/commands/dev_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@ abstract class DevCommand extends BaseCommand with ProxyHelper, FlutterHelper {
'mode',
abbr: 'm',
help: 'Sets the reload/refresh mode.',
allowed: ['reload', 'refresh'],
allowed: ['reload', 'restart', 'refresh'],
allowedHelp: {
'reload': 'Reloads js modules without server reload (loses current state)',
'reload': 'Hot-reloads both client and server apps',
'restart': 'Restarts the client app (loses current state)',
'refresh': 'Performs a full page refresh and server reload',
},
defaultsTo: 'refresh',
defaultsTo: 'reload',
);
argParser.addOption(
'port',
Expand All @@ -57,6 +58,7 @@ abstract class DevCommand extends BaseCommand with ProxyHelper, FlutterHelper {
argParser.addFlag('debug', abbr: 'd', help: 'Serves the app in debug mode.', negatable: false);
argParser.addFlag('release', abbr: 'r', help: 'Serves the app in release mode.', negatable: false);
argParser.addFlag('experimental-wasm', help: 'Compile to wasm', negatable: false);
argParser.addOption('module-format', help: 'The module format to use.', allowed: ['ddc', 'amd']);
argParser.addFlag(
'managed-build-options',
help:
Expand Down Expand Up @@ -85,6 +87,7 @@ abstract class DevCommand extends BaseCommand with ProxyHelper, FlutterHelper {
late final port = argResults!.option('port') ?? project.port ?? defaultServePort;
late final customProxyPort = argResults!.option('proxy-port') ?? serverProxyPort;
late final useWasm = argResults!.flag('experimental-wasm');
late final moduleFormat = argResults!.option('module-format');
late final managedBuildOptions = argResults!.flag('managed-build-options');
late final skipServer = argResults!.flag('skip-server');

Expand Down Expand Up @@ -282,7 +285,7 @@ abstract class DevCommand extends BaseCommand with ProxyHelper, FlutterHelper {
project.checkWasmSupport();
}

logger.write('Starting web compilers...', tag: Tag.cli, progress: ProgressState.running);
logger.write('Starting web compiler...', tag: Tag.cli, progress: ProgressState.running);

final compiler = useWasm

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider changing this to a private enum, then any conditional handling can validate you handled each case and spelling mistakes are avoided.

? 'dart2wasm'
Expand Down Expand Up @@ -312,16 +315,26 @@ abstract class DevCommand extends BaseCommand with ProxyHelper, FlutterHelper {
for (final e in dartDefines.entries) '-D${e.key}=${e.value}',
];

final reloadConfig = switch (mode) {
'reload' => ReloadConfiguration.hotReload,
'refresh' => ReloadConfiguration.liveReload,
'restart' || _ => ReloadConfiguration.hotRestart,
};

final moduleFormat = reloadConfig == ReloadConfiguration.hotReload ? 'ddc' : this.moduleFormat ?? 'ddc';
final usesDdcLibraryBundles = moduleFormat == 'ddc';

List<String> additionalFlutterBuildArgs() {
final sdkKernelPath = p.url.join(
'kernel',
flutterVersion.compareTo('3.32.0') >= 0 ? 'ddc_outline.dill' : 'ddc_outline_sound.dill',
);
final librariesPath = p.join(webSdkDir, 'libraries.json');
final ddcSdkPrefix = usesDdcLibraryBundles ? 'ddcLibraryBundle-canvaskit' : 'amd-canvaskit';
final sdkJsPath = p.join(
webSdkDir,
'kernel',
flutterVersion.compareTo('3.32.0') >= 0 ? 'amd-canvaskit' : 'amd-canvaskit-sound',
flutterVersion.compareTo('3.32.0') >= 0 ? ddcSdkPrefix : '$ddcSdkPrefix-sound',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly unrelated, but I guess we can remove this (and related) handling now. The package's SDK constraint requires Dart 3.8 or later, so Flutter 3.32 or later is required anyway.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll keep it just a bit longer, but yes then I will remove it.

);
return [
'--define=build_web_compilers:entrypoint=use-ui-libraries=true',
Expand All @@ -342,11 +355,32 @@ abstract class DevCommand extends BaseCommand with ProxyHelper, FlutterHelper {
}

final buildArgs = [
// Enable build_runner debugging
// '--force-jit',
// '--dart-jit-vm-arg=--observe',
// '--dart-jit-vm-arg=--pause-isolates-on-start',
if (release) '--release',
'--delete-conflicting-outputs',
if (managedBuildOptions) ...[
'--define=build_web_compilers:ddc=generate-full-dill=true',
'--define=build_web_compilers:entrypoint=compiler=$compiler',

// Add DDC Library Bundle defines.
if (usesDdcLibraryBundles) ...[
'--define=build_web_compilers:ddc=ddc-library-bundle=true',
'--define=build_web_compilers:sdk_js=ddc-library-bundle=true',
'--define=build_web_compilers:entrypoint=ddc-library-bundle=true',
'--define=build_web_compilers:entrypoint_marker=ddc-library-bundle=true',
],

// Add Web Hot Reload defines.
if (reloadConfig == ReloadConfiguration.hotReload) ...[
'--define=build_web_compilers:sdk_js=web-hot-reload=true',
'--define=build_web_compilers:entrypoint=web-hot-reload=true',
'--define=build_web_compilers:entrypoint_marker=web-hot-reload=true',
'--define=build_web_compilers:ddc=web-hot-reload=true',
'--define=build_web_compilers:ddc_modules=web-hot-reload=true',
],
Comment on lines +382 to +397

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should all of these only be added if the compiler is dartdevc?

switch (compiler) {
'dartdevc' => '--define=build_web_compilers:ddc=environment=${jsonEncode(ddcDefines)}',
_ => '--define=build_web_compilers:entrypoint=${compiler}_args=${jsonEncode(dart2jsDefines)}',
Expand All @@ -360,8 +394,10 @@ abstract class DevCommand extends BaseCommand with ProxyHelper, FlutterHelper {
buildArgs,
logger,
guardResource,
enableDebugging: launchInChrome,
reload: mode == 'reload' ? ReloadConfiguration.hotRestart : ReloadConfiguration.liveReload,
enableDebugging: true,
useDwdsWebSocketConnection: !launchInChrome,
reload: reloadConfig,
moduleFormat: moduleFormat,
);
if (workflow == null) {
return null;
Expand Down
32 changes: 6 additions & 26 deletions packages/jaspr_cli/lib/src/commands/serve_command.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
// ignore: implementation_imports
import 'dart:async';

import '../daemon/daemon.dart';
import '../dev/client_domain.dart';
import '../dev/client_workflow.dart';
import '../logging.dart';
import 'dev_command.dart';
Expand All @@ -25,36 +20,21 @@ class ServeCommand extends DevCommand {
@override
late final bool launchInChrome = argResults?.flag('launch-in-chrome') ?? false;

late Daemon daemon;

@override
Future<int> runCommand() async {
final fakeInput = StreamController<Map<String, Object?>>();
daemon = Daemon(fakeInput.stream, (data) {
if (data['event'] == 'client.debugPort') {
final params = data['params'] as Map<String, Object?>;
void handleClientWorkflow(ClientWorkflow workflow) {
workflow.devProxy.clientEvents.listen((event) {
if (launchInChrome && event['method'] == 'client.debugPort') {
final params = event['params'] as Map<String, Object?>;
final wsUri = params['wsUri'] as String;
logger.write(
'The Dart VM service is listening on http${wsUri.substring(2, wsUri.length - 2)}',
tag: Tag.client,
);
}
if (data['event'] == 'client.log') {
final params = data['params'] as Map<String, Object?>;
if (event['method'] == 'client.log') {
final params = event['params'] as Map<String, Object?>;
logger.write(params['log'] as String, tag: Tag.client);
}
});
guardResource(() {
daemon.shutdown();
});

return super.runCommand();
}

@override
void handleClientWorkflow(ClientWorkflow workflow) {
if (launchInChrome) {
daemon.registerDomain(ClientDomain(daemon, workflow.devProxy));
}
}
}
Loading
Loading