Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
? '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',
);
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',
],
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