Skip to content
Closed
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
Binary file added .github/assets/inspector/1_tree_ssr_csr.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/assets/inspector/2_button_detail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/assets/inspector/3_search.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/assets/inspector/4_csr_detail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion packages/jaspr/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## Unreleased patch
## Unreleased minor

- Allow `analyzer` versions 10.x.
- Added debug-only component tree inspector (Ctrl+Shift+D) for visualizing the live component tree in development builds.

## 0.22.3

Expand Down
8 changes: 8 additions & 0 deletions packages/jaspr/lib/devtools.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/// Public API for Jaspr DevTools integration.
///
/// This library exports the types needed by the standalone DevTools app
/// to communicate with and display the component tree.
library;

export 'src/devtools/devtools_protocol.dart';
export 'src/devtools/tree_snapshot.dart' show InspectorNode;
2 changes: 2 additions & 0 deletions packages/jaspr/lib/src/client/client_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:async';
import 'package:universal_web/js_interop.dart';
import 'package:universal_web/web.dart' as web;

import '../devtools/inspector.dart';
import '../foundation/basic_types.dart';
import '../foundation/binding.dart';
import '../framework/framework.dart';
Expand Down Expand Up @@ -60,6 +61,7 @@ class ClientAppBinding extends AppBinding with ComponentsBinding {
void completeInitialFrame() {
(rootElement!.renderObject as DomRenderObject).finalize();
super.completeInitialFrame();
initInspector(this);
}

@override
Expand Down
7 changes: 7 additions & 0 deletions packages/jaspr/lib/src/client/dom_render_object.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ abstract class DomRenderObject implements RenderObject {
@override
web.Node get node;

@override
bool? debugWasHydrated;

@override
DomRenderObject? parent;

Expand Down Expand Up @@ -68,12 +71,14 @@ class DomRenderElement extends DomRenderObject
print('Hydrate html node: $retakeNode');
}
node = retakeNode as web.Element;
debugWasHydrated = true;

toHydrate = retakeNode.childNodes.toIterable().toList();
return;
}

node = _createElement(tag, namespace);
debugWasHydrated = false;
if (kVerboseMode) {
web.console.log('Create html node: $node'.toJS);
}
Expand Down Expand Up @@ -234,6 +239,7 @@ class DomRenderText extends DomRenderObject implements RenderText {
print('Hydrate text node: $retakeNode');
}
node = retakeNode as web.Text;
debugWasHydrated = true;
if (node.textContent != text) {
node.textContent = text;
if (kVerboseMode) {
Expand All @@ -244,6 +250,7 @@ class DomRenderText extends DomRenderObject implements RenderText {
}

node = web.Text(text);
debugWasHydrated = false;
if (kVerboseMode) {
print('Create text node: $text');
}
Expand Down
177 changes: 177 additions & 0 deletions packages/jaspr/lib/src/devtools/devtools_protocol.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import 'dart:convert';

import 'tree_snapshot.dart';

/// Represents a single change in a tree diff.
class NodeChange {
NodeChange({required this.type, required this.nodeId, this.node, this.parentId});

/// 'added', 'removed', or 'updated'.
final String type;

/// The ID of the changed node.
final int nodeId;

/// The new/updated node data (null for 'removed').
final InspectorNode? node;

/// Parent node ID (for 'added' to know where to insert).
final int? parentId;

Map<String, Object?> toJson() => {
'type': type,
'nodeId': nodeId,
if (node != null) 'node': node!.toJson(),
if (parentId != null) 'parentId': parentId,
};

factory NodeChange.fromJson(Map<String, Object?> json) => NodeChange(
type: json['type'] as String,
nodeId: json['nodeId'] as int,
node: json['node'] != null ? InspectorNode.fromJson((json['node'] as Map).cast<String, Object?>()) : null,
parentId: json['parentId'] as int?,
);
}

/// Message types sent from the running app to the DevTools UI.
enum AppToDevToolsType {
/// Full serialized component tree.
treeUpdate,

/// App metadata (name, mode, URL).
appInfo,

/// Detailed info about a specific element.
elementDetails,
}

/// Message types sent from the DevTools UI to the running app.
enum DevToolsToAppType {
/// Request a full tree snapshot.
requestTree,

/// Ask the embedded toolbar to highlight a specific node.
highlightElement,

/// Ask the embedded toolbar to select a specific node.
selectElement,

/// Request detailed info for a specific node ID.
getElementDetails,
}

/// A message in the DevTools protocol, sent over WebSocket.
///
/// The [type] field identifies the kind of message. The [payload] carries the
/// message-specific data. Both directions (app↔devtools) use this same envelope.
class DevToolsMessage {
DevToolsMessage({
required this.type,
required this.payload,
DateTime? timestamp,
}) : timestamp = timestamp ?? DateTime.now();

/// The message type as a string (e.g. `'tree_update'`, `'request_tree'`).
final String type;

/// Message-specific data.
final Map<String, Object?> payload;

/// When the message was created.
final DateTime timestamp;

/// Serializes to a JSON string for WebSocket transport.
String encode() => jsonEncode({
'type': type,
'timestamp': timestamp.millisecondsSinceEpoch,
'payload': payload,
});

/// Deserializes a [DevToolsMessage] from a JSON string.
factory DevToolsMessage.decode(String raw) {
final json = jsonDecode(raw) as Map<String, Object?>;
return DevToolsMessage(
type: json['type'] as String,
payload: (json['payload'] as Map?)?.cast<String, Object?>() ?? {},
timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int? ?? 0),
);
}

// -- App → DevTools factory constructors --

/// Creates a tree update message with the full serialized tree.
factory DevToolsMessage.treeUpdate(InspectorNode tree) => DevToolsMessage(
type: 'tree_update',
payload: {'tree': tree.toJson()},
);

/// Creates an app info message with app metadata.
factory DevToolsMessage.appInfo({
required String appName,
required String url,
required bool isDebug,
}) =>
DevToolsMessage(
type: 'app_info',
payload: {
'appName': appName,
'url': url,
'isDebug': isDebug,
},
);

/// Creates an element details message for a specific node.
factory DevToolsMessage.elementDetails({
required int nodeId,
required Map<String, Object?> details,
}) =>
DevToolsMessage(
type: 'element_details',
payload: {'nodeId': nodeId, 'details': details},
);

// -- DevTools → App factory constructors --

/// Creates a request tree message.
factory DevToolsMessage.requestTree() => DevToolsMessage(
type: 'request_tree',
payload: {},
);

/// Creates a highlight element message for the given node ID.
factory DevToolsMessage.highlightElement(int nodeId) => DevToolsMessage(
type: 'highlight_element',
payload: {'nodeId': nodeId},
);

/// Creates a select element message for the given node ID.
factory DevToolsMessage.selectElement(int nodeId) => DevToolsMessage(
type: 'select_element',
payload: {'nodeId': nodeId},
);

/// Creates a get element details request for the given node ID.
factory DevToolsMessage.getElementDetails(int nodeId) => DevToolsMessage(
type: 'get_element_details',
payload: {'nodeId': nodeId},
);

/// Requests children of a specific node (for lazy tree loading).
factory DevToolsMessage.requestChildren(int nodeId) => DevToolsMessage(
type: 'request_children',
payload: {'nodeId': nodeId},
);

/// Response with children of a node.
factory DevToolsMessage.childrenResponse({
required int nodeId,
required List<InspectorNode> children,
}) =>
DevToolsMessage(
type: 'children_response',
payload: {
'nodeId': nodeId,
'children': children.map((c) => c.toJson()).toList(),
},
);
}
Loading
Loading