citrine-devtools 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +44 -0
- data/bin/citrine-devtools +27 -0
- data/docs/PROTOCOL.md +52 -0
- data/lib/citrine-devtools/bridge.js +264 -0
- data/lib/citrine-devtools/panel.html +127 -0
- data/lib/citrine-devtools/panel.js +1056 -0
- data/lib/citrine-devtools/server.rb +304 -0
- data/lib/citrine-devtools/vendor/cytoscape.min.js +32 -0
- data/lib/citrine-devtools/version.rb +7 -0
- data/lib/citrine-devtools.rb +6 -0
- metadata +113 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6ad9103211d1286b38e3106ddd895c46661b975fb5dcf08a30bb92d6262d4ea1
|
|
4
|
+
data.tar.gz: 40c98f02c45fbf012a60e32295d8f831d79c912b5042aefd29c6898c3d81cf68
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 936fcc314caf398396fdfd2240c9644a8293418a4baadbca114737aa0733f310ad1a4d1ae1a85a4d67f0469553cdacf4104b94fc5883e3cbad9410caf55df1bc
|
|
7
|
+
data.tar.gz: 43c261c4ad493df5c91cb9f21ef91d26c629654dd08b24f2cd2a8a266b3162846532990e4dcf04f4db6a0b2ba060af5b14256dc4ba1bf3a826cbb48a19c55509
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ShiningRay
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# citrine-devtools
|
|
2
|
+
|
|
3
|
+
Citrine/Emerald DevTools 传输层:中继服务 + 浏览器桥接脚本 + Web 调试面板。
|
|
4
|
+
|
|
5
|
+
信号式 Ruby UI 框架([citrine](https://github.com/ShiningRay/citrine) /
|
|
6
|
+
[emerald](https://github.com/ShiningRay/emerald))的调试器底座——把 M1 时序探针
|
|
7
|
+
(`write_log` / `flush_trace` / `event_stream` / `component_tree`)的数据经消息
|
|
8
|
+
总线送达调试面板,并把面板指令(`set_signal` / `force_rerun` / `request_tree` /
|
|
9
|
+
`request_graph`)转发回被调试页执行。协议契约见 [docs/PROTOCOL.md](docs/PROTOCOL.md)。
|
|
10
|
+
|
|
11
|
+
## 组成
|
|
12
|
+
|
|
13
|
+
| 组件 | 说明 |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `citrine-devtools serve` | 中继服务(默认 9527):SSE 下行 + ingest/cmd 上行 + 静态托管面板与桥接脚本 |
|
|
16
|
+
| `bridge.js` | 被调试页引入的桥接脚本:采集探针 ring、上报、执行面板指令 |
|
|
17
|
+
| `panel.html` / `panel.js` | Web 调试面板:组件树 / 信号列表 / 依赖图(Cytoscape)/ Timeline / Inspector |
|
|
18
|
+
|
|
19
|
+
## 快速开始
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install citrine-devtools
|
|
23
|
+
citrine-devtools serve # → http://localhost:9527/ 打开面板
|
|
24
|
+
|
|
25
|
+
# 被调试页(由 citrine dev server 服务时)加一行:
|
|
26
|
+
<script src="http://127.0.0.1:9527/bridge.js"></script>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 开发
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bundle install && bundle exec rake # Ruby 集成测试 + bridge 契约 + 面板模型契约
|
|
33
|
+
node --test # JS 契约(node 24+)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 设计文档
|
|
37
|
+
|
|
38
|
+
分层架构、红线与路线图见 citrine 仓的
|
|
39
|
+
[DESIGN-devtools.md](https://github.com/ShiningRay/citrine/blob/main/docs/DESIGN-devtools.md) 与
|
|
40
|
+
[PLAN-devtools.md](https://github.com/ShiningRay/citrine/blob/main/docs/PLAN-devtools.md)。
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
MIT(含 vendor 的 Cytoscape.js,MIT © The Cytoscape Consortium)。
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# lib 入 $LOAD_PATH:与 gem 安装后的 require 解析行为一致
|
|
5
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
6
|
+
$stdout.sync = true # CLI 输出重定向到文件/管道时不应块缓冲
|
|
7
|
+
|
|
8
|
+
require "citrine-devtools"
|
|
9
|
+
|
|
10
|
+
cmd = ARGV.shift
|
|
11
|
+
case cmd
|
|
12
|
+
when "serve"
|
|
13
|
+
Citrine::DevTools::Server.run!(ARGV)
|
|
14
|
+
when nil, "help", "-h", "--help"
|
|
15
|
+
puts <<~USAGE
|
|
16
|
+
citrine-devtools — Citrine/Emerald DevTools 中继服务
|
|
17
|
+
|
|
18
|
+
用法:
|
|
19
|
+
bin/citrine-devtools serve [-p 端口] 启动中继(消息总线,默认端口 9527)
|
|
20
|
+
|
|
21
|
+
被调试页引入桥接脚本(协议见 docs/PROTOCOL.md):
|
|
22
|
+
<script src="http://127.0.0.1:9527/bridge.js"></script>
|
|
23
|
+
USAGE
|
|
24
|
+
else
|
|
25
|
+
warn "未知命令: #{cmd}(试试 bin/citrine-devtools help)"
|
|
26
|
+
exit 1
|
|
27
|
+
end
|
data/docs/PROTOCOL.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# citrine-devtools · 传输协议 v1
|
|
2
|
+
|
|
3
|
+
> DESIGN-devtools §4 的落地契约。中继(`citrine-devtools serve`)是消息总线:
|
|
4
|
+
> 浏览器桥接脚本上报探针数据,调试面板订阅消费;面板指令经同一条总线下发桥接执行。
|
|
5
|
+
> 所有消息 JSON,顶层带 `v: 1` 与 `type`。
|
|
6
|
+
|
|
7
|
+
## 端点(默认端口 9527)
|
|
8
|
+
|
|
9
|
+
| 端点 | 方法 | 方向 | 说明 |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| `/__devtools/stream` | GET | 下行 | SSE 流。客户端两类:面板(消费数据+发指令)与桥接(收指令)。15s comment ping 清扫僵尸连接 |
|
|
12
|
+
| `/__devtools/ingest` | POST | 上行 | 桥接批量上报:`{v:1, messages:[…]}`。中继逐条广播给所有 stream 客户端 |
|
|
13
|
+
| `/__devtools/cmd` | POST | 上行 | 面板发指令:`{v:1, type:"cmd", cmd:"set_signal", …}`。中继原样广播(桥接消费、面板忽略) |
|
|
14
|
+
| `/bridge.js` | GET | — | 桥接脚本静态资源,`<script src="http://127.0.0.1:9527/bridge.js">` 引入被调试页 |
|
|
15
|
+
|
|
16
|
+
CORS:`Access-Control-Allow-Origin: *`(仅 localhost 调试场景),处理 OPTIONS 预检。
|
|
17
|
+
|
|
18
|
+
## 下行消息(v1)
|
|
19
|
+
|
|
20
|
+
| type | 载荷 | 生产者 |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `hello` | `{v, app_name?, citrine_version?}` | 桥接连接时上报(幂等:Emerald 热更新重连可重复发) |
|
|
23
|
+
| `signal_write` | M1 write_log 条目 `{t, signal_id, old, new, source}` | 桥接 |
|
|
24
|
+
| `flush` | M1 flush_trace 条目 `{flush_id, t, effects, trigger_signal_ids}` | 桥接 |
|
|
25
|
+
| `event` | M1 event_stream 条目 `{t, event_type, target_component, handler_name, flush_ids}` | 桥接 |
|
|
26
|
+
| `tree` | `Citrine.debug_component_tree` 快照 | 桥接(应 `request_tree` 指令触发,也允许定时) |
|
|
27
|
+
| `graph` | `Citrine.debug_dependency_graph` 快照 | 桥接(应 `request_graph` 指令触发) |
|
|
28
|
+
| `cmd` | `{cmd, …参数}` 见下表 | 面板 → 中继广播 → 桥接执行 |
|
|
29
|
+
|
|
30
|
+
## 指令(cmd 载荷)
|
|
31
|
+
|
|
32
|
+
| cmd | 参数 | 桥接行为 |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `set_signal` | `{signal_id, value}` | 在 `Signal.all` 里按 object_id 找信号并 `$set(value)`;找不到回 `error` |
|
|
35
|
+
| `force_rerun` | `{effect_id}` | 在 `Effect.all` 里按 object_id 找 effect 并 `$run` |
|
|
36
|
+
| `request_tree` / `request_graph` | `{}` | 采集快照以 `tree`/`graph` 消息回传 |
|
|
37
|
+
| `highlight_node` | `{node_id}` | v1 未实现:回 `{type:"error", error:"highlight_node 未实现(M3)"}` |
|
|
38
|
+
|
|
39
|
+
## 桥接脚本(bridge.js)行为契约
|
|
40
|
+
|
|
41
|
+
- 激活条件:`window.CITRINE_DEV && window.Opal`;否则静默不启动(生产/SSR 零影响)。
|
|
42
|
+
- 中继地址:`window.CITRINE_DEVTOOLS_URL` 覆盖,默认 `http://127.0.0.1:9527`。
|
|
43
|
+
- 采集:每 100ms 读 `Opal.Citrine` 的 write_log / flush_trace / event_stream 三个 ring
|
|
44
|
+
(`$to_a().$to_n()`),与上次快照 diff(按条目去重,不假设 ring 不滚动),
|
|
45
|
+
新条目攒批 POST ingest(另 50ms 聚合窗口)。
|
|
46
|
+
- 上报故障静默降级(warn 一次),绝不抛进被调试应用。
|
|
47
|
+
- 收到自身发不认识的 type 一律忽略;执行 cmd 的异常回 `{type:"error", cmd, error}`。
|
|
48
|
+
|
|
49
|
+
## 会话
|
|
50
|
+
|
|
51
|
+
v1 单会话:`hello` 的 `app_name` 仅作展示。多 Emerald 窗口同连一个中继时消息混合——
|
|
52
|
+
M5 前不按会话隔离(DESIGN §八 风险 5)。
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* citrine-devtools 桥接脚本(M2-2)——被调试页经
|
|
3
|
+
* <script src="http://127.0.0.1:9527/bridge.js"> 引入。
|
|
4
|
+
*
|
|
5
|
+
* 行为契约见 citrine-devtools docs/PROTOCOL.md v1 §桥接脚本:
|
|
6
|
+
* - 激活条件 window.CITRINE_DEV && window.Opal;否则静默不启动(生产/SSR 零影响)
|
|
7
|
+
* - 中继地址 window.CITRINE_DEVTOOLS_URL 覆盖,默认 http://127.0.0.1:9527
|
|
8
|
+
* - 每 100ms 读三个 ring(write_log / flush_trace / event_stream,$to_a().$to_n()),
|
|
9
|
+
* 按条目指纹去重 diff(不假设 ring 不滚动),新条目攒批 POST ingest
|
|
10
|
+
* (另 50ms 聚合窗口)
|
|
11
|
+
* - 上报故障静默降级(warn 一次),绝不抛进被调试应用
|
|
12
|
+
* - 收到自身发不认识的 type 一律忽略;执行 cmd 的异常回 {type:"error", cmd, error}
|
|
13
|
+
*
|
|
14
|
+
* request_tree 的根节点发现(本地实现细节,线上协议之外),按序尝试:
|
|
15
|
+
* 1. window.CITRINE_DEVTOOLS_ROOT(页面可选提供:根 Node 或组件实例)
|
|
16
|
+
* 2. 渲染器(Citrine.renderer)暴露的 root_node 方法(beryl 入口)
|
|
17
|
+
* 3. 渲染器的 @root 实例变量(Canvas 渲染器有存)
|
|
18
|
+
* 都找不到时回 error,由页面提供 CITRINE_DEVTOOLS_ROOT 解决。
|
|
19
|
+
*/
|
|
20
|
+
(function () {
|
|
21
|
+
"use strict";
|
|
22
|
+
|
|
23
|
+
if (typeof window === "undefined") return; // SSR:静默不启动
|
|
24
|
+
// 激活条件(协议):否则静默不启动
|
|
25
|
+
if (!window.CITRINE_DEV || !window.Opal) return;
|
|
26
|
+
// 同一页面二次求值不起第二个桥接实例(Emerald 热更新会重放脚本)
|
|
27
|
+
if (window.__CITRINE_DEVTOOLS_BRIDGE__) return;
|
|
28
|
+
window.__CITRINE_DEVTOOLS_BRIDGE__ = true;
|
|
29
|
+
|
|
30
|
+
var Opal = window.Opal;
|
|
31
|
+
var Citrine = Opal.Citrine;
|
|
32
|
+
if (!Citrine) return;
|
|
33
|
+
|
|
34
|
+
var PROTOCOL_VERSION = 1;
|
|
35
|
+
var COLLECT_INTERVAL_MS = 100; // 协议:每 100ms 采集
|
|
36
|
+
var FLUSH_WINDOW_MS = 50; // 协议:另 50ms 聚合窗口
|
|
37
|
+
var SEEN_FINGERPRINTS_LIMIT = 2000; // ring 容量 500/个,去重指纹 FIFO 封顶(不假设 ring 不滚动)
|
|
38
|
+
|
|
39
|
+
var relayUrl = String(window.CITRINE_DEVTOOLS_URL || "http://127.0.0.1:9527").replace(/\/+$/, "");
|
|
40
|
+
|
|
41
|
+
// 探针只在 debug_tracking 开启时记录(M1 口径):桥接是页面的调试代理,负责打开它
|
|
42
|
+
try { Citrine["$debug_tracking="](true); } catch (e) { /* 旧版内核无开关则探针自管 */ }
|
|
43
|
+
|
|
44
|
+
// ── 上行:攒批 POST ingest ─────────────────────────────
|
|
45
|
+
|
|
46
|
+
var outbox = [];
|
|
47
|
+
var flushTimer = null;
|
|
48
|
+
var lastFlushAt = 0;
|
|
49
|
+
var degraded = false; // 上报故障静默降级(warn 一次)
|
|
50
|
+
|
|
51
|
+
function enqueue(message) {
|
|
52
|
+
outbox.push(message);
|
|
53
|
+
scheduleFlush();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function scheduleFlush() {
|
|
57
|
+
if (flushTimer) return;
|
|
58
|
+
var wait = Math.max(0, FLUSH_WINDOW_MS - (Date.now() - lastFlushAt));
|
|
59
|
+
flushTimer = setTimeout(flush, wait);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function flush() {
|
|
63
|
+
flushTimer = null;
|
|
64
|
+
if (!outbox.length) return;
|
|
65
|
+
var batch = outbox;
|
|
66
|
+
outbox = [];
|
|
67
|
+
lastFlushAt = Date.now();
|
|
68
|
+
fetch(relayUrl + "/__devtools/ingest", {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { "Content-Type": "application/json" },
|
|
71
|
+
body: JSON.stringify({ v: PROTOCOL_VERSION, messages: batch })
|
|
72
|
+
}).then(function (res) {
|
|
73
|
+
if (res && res.ok === false) throw new Error("HTTP " + res.status); // 非 2xx 也按上报失败降级
|
|
74
|
+
}).catch(function (err) {
|
|
75
|
+
if (!degraded) {
|
|
76
|
+
degraded = true;
|
|
77
|
+
console.warn("[citrine-devtools] 上报失败,桥接静默降级:", err);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function reportError(cmd, error) {
|
|
83
|
+
enqueue({ v: PROTOCOL_VERSION, type: "error", cmd: cmd, error: String((error && error.message) || error) });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── 采集:三个 ring 的增量 diff ─────────────────────────
|
|
87
|
+
|
|
88
|
+
var rings = [
|
|
89
|
+
{ type: "signal_write", read: function () { return Citrine.$debug_write_log().$to_a().$to_n(); } },
|
|
90
|
+
{ type: "flush", read: function () { return Citrine.$debug_flush_trace().$to_a().$to_n(); } },
|
|
91
|
+
{ type: "event", read: function () { return Citrine.$debug_event_stream().$to_a().$to_n(); } }
|
|
92
|
+
];
|
|
93
|
+
var seen = new Map(); // 条目指纹 FIFO 集合:同一条目(ring 未滚动重读)只上报一次
|
|
94
|
+
|
|
95
|
+
function fingerprint(entry) {
|
|
96
|
+
try { return JSON.stringify(entry); } catch (e) { return String(entry); }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function markSeen(fp) {
|
|
100
|
+
if (seen.has(fp)) return false;
|
|
101
|
+
seen.set(fp, true);
|
|
102
|
+
if (seen.size > SEEN_FINGERPRINTS_LIMIT) seen.delete(seen.keys().next().value);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function collect() {
|
|
107
|
+
rings.forEach(function (ring) {
|
|
108
|
+
var entries;
|
|
109
|
+
try {
|
|
110
|
+
entries = ring.read();
|
|
111
|
+
} catch (e) {
|
|
112
|
+
return; // 探针读取失败不抛进被调试应用
|
|
113
|
+
}
|
|
114
|
+
if (!entries || !entries.length) return;
|
|
115
|
+
entries.forEach(function (entry) {
|
|
116
|
+
if (!entry || typeof entry !== "object") return;
|
|
117
|
+
var fp = fingerprint(entry);
|
|
118
|
+
if (!markSeen(fp)) return; // ring 未滚动时的重读 / 已上报条目
|
|
119
|
+
var message = { v: PROTOCOL_VERSION, type: ring.type };
|
|
120
|
+
for (var key in entry) message[key] = entry[key]; // 载荷即 M1 条目字段
|
|
121
|
+
enqueue(message);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── 下行:SSE 收指令 ───────────────────────────────────
|
|
127
|
+
|
|
128
|
+
var es = new EventSource(relayUrl + "/__devtools/stream");
|
|
129
|
+
|
|
130
|
+
// 连接时上报 hello(幂等:热更新重连可重复发);EventSource 自动重连
|
|
131
|
+
es.onopen = function () {
|
|
132
|
+
var hello = { v: PROTOCOL_VERSION, type: "hello", citrine_version: citrineVersion() };
|
|
133
|
+
if (typeof document !== "undefined" && document.title) hello.app_name = document.title;
|
|
134
|
+
enqueue(hello);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
es.onmessage = function (e) {
|
|
138
|
+
var message;
|
|
139
|
+
try {
|
|
140
|
+
message = JSON.parse(e.data);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
return; // 非 JSON 包一律忽略
|
|
143
|
+
}
|
|
144
|
+
if (!message || message.v !== PROTOCOL_VERSION) return;
|
|
145
|
+
// 畸形 cmd(缺 cmd 字段)按协议静默忽略;其余 type(含自身上报被广播回来的)一律忽略
|
|
146
|
+
if (message.type === "cmd" && typeof message.cmd === "string") handleCmd(message);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
function citrineVersion() {
|
|
150
|
+
try { return Citrine.$const_get("VERSION"); } catch (e) { return undefined; }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function handleCmd(cmd) {
|
|
154
|
+
try {
|
|
155
|
+
switch (cmd.cmd) {
|
|
156
|
+
case "set_signal": setSignal(cmd); break;
|
|
157
|
+
case "force_rerun": forceRerun(cmd); break;
|
|
158
|
+
case "request_tree": requestTree(cmd); break;
|
|
159
|
+
case "request_graph": requestGraph(); break;
|
|
160
|
+
case "highlight_node":
|
|
161
|
+
reportError("highlight_node", "highlight_node 未实现(M3)");
|
|
162
|
+
break;
|
|
163
|
+
default:
|
|
164
|
+
reportError(cmd.cmd, "未知指令: " + cmd.cmd);
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
reportError(cmd.cmd, err); // 执行 cmd 的异常按协议回 error
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// JS 值 → Opal 值(跨语言边界):null/undefined → Opal.nil,对象 → Opal.hash,数组递归,标量直通
|
|
172
|
+
function jsToOpal(value) {
|
|
173
|
+
if (value === null || value === undefined) return Opal.nil;
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
var list = [];
|
|
176
|
+
for (var i = 0; i < value.length; i++) list.push(jsToOpal(value[i]));
|
|
177
|
+
return list;
|
|
178
|
+
}
|
|
179
|
+
if (typeof value === "object") {
|
|
180
|
+
var args = [];
|
|
181
|
+
for (var key in value) {
|
|
182
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) args.push(key, jsToOpal(value[key]));
|
|
183
|
+
}
|
|
184
|
+
return Opal.hash.apply(null, args);
|
|
185
|
+
}
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function setSignal(cmd) {
|
|
190
|
+
var signal = findByObjectId(Citrine.Signal.$all(), cmd.signal_id);
|
|
191
|
+
if (!signal) {
|
|
192
|
+
reportError("set_signal", "找不到 signal(object_id=" + cmd.signal_id + ")");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
signal.$set(jsToOpal(cmd.value));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function forceRerun(cmd) {
|
|
199
|
+
var effect = findByObjectId(Citrine.Effect.$all(), cmd.effect_id);
|
|
200
|
+
if (!effect) {
|
|
201
|
+
reportError("force_rerun", "找不到 effect(object_id=" + cmd.effect_id + ")");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
effect.$run();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Opal 数组即 JS 数组,逐元素比 object_id(Signal.all / Effect.all 仅在埋点开启时填充,
|
|
208
|
+
// 桥接启动时已打开 debug_tracking)
|
|
209
|
+
function findByObjectId(all, id) {
|
|
210
|
+
if (!all) return null;
|
|
211
|
+
for (var i = 0; i < all.length; i++) {
|
|
212
|
+
if (all[i] && all[i].$object_id() === id) return all[i];
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function requestGraph() {
|
|
218
|
+
var graph = Citrine.$debug_dependency_graph().$to_n(); // 打开埋点并取 {signals, effects}
|
|
219
|
+
var message = { v: PROTOCOL_VERSION, type: "graph" };
|
|
220
|
+
for (var key in graph) message[key] = graph[key];
|
|
221
|
+
enqueue(message);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function requestTree(cmd) {
|
|
225
|
+
var rootNode = findRootNode();
|
|
226
|
+
if (!rootNode) {
|
|
227
|
+
reportError("request_tree",
|
|
228
|
+
"找不到渲染根节点:请在页面设置 window.CITRINE_DEVTOOLS_ROOT(根 Node 或组件实例)");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
var snapshot = Citrine.$debug_component_tree(rootNode).$to_n();
|
|
232
|
+
var message = { v: PROTOCOL_VERSION, type: "tree" };
|
|
233
|
+
for (var key in snapshot) message[key] = snapshot[key];
|
|
234
|
+
enqueue(message);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 根节点发现顺序见文件头注释;组件(有 $root)与 Node(有 $children)都接受
|
|
238
|
+
function findRootNode() {
|
|
239
|
+
var provided = window.CITRINE_DEVTOOLS_ROOT;
|
|
240
|
+
if (provided) {
|
|
241
|
+
if (typeof provided.$children === "function") return provided; // 已是 Node
|
|
242
|
+
if (typeof provided.$root === "function") return provided.$root(); // 组件实例
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
var renderer = Citrine.$renderer();
|
|
247
|
+
if (renderer) {
|
|
248
|
+
// beryl 入口:渲染器暴露 root_node 方法(Opal 侧 respond_to? 判存在,判不到则鸭子类型)
|
|
249
|
+
if (typeof renderer.$root_node === "function" &&
|
|
250
|
+
(!renderer.$respond_to || renderer.$respond_to("root_node"))) {
|
|
251
|
+
return renderer.$root_node();
|
|
252
|
+
}
|
|
253
|
+
// Canvas 渲染器:@root 实例变量
|
|
254
|
+
var root = renderer.$instance_variable_get("@root");
|
|
255
|
+
if (root) return root;
|
|
256
|
+
}
|
|
257
|
+
} catch (e) { /* 无渲染器或未挂载 */ }
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── 主循环 ────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
setInterval(collect, COLLECT_INTERVAL_MS);
|
|
264
|
+
})();
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Citrine DevTools · v1</title>
|
|
6
|
+
<style>
|
|
7
|
+
/* 暗色 token 参照 Emerald desktop.html(与其 theme.rb 调色板同源) */
|
|
8
|
+
:root { --bg: #0b0e14; --panel: #11151f; --panel2: #151b28; --line: #232b3b;
|
|
9
|
+
--fg: #c9d4e6; --dim: #7d8aa5; --accent: #4f8cff;
|
|
10
|
+
--ok: #3fb950; --err: #f85149; }
|
|
11
|
+
* { box-sizing: border-box; scrollbar-width: thin; scrollbar-color: #2a3348 transparent; }
|
|
12
|
+
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
13
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
14
|
+
::-webkit-scrollbar-thumb { background: #2a3348; border-radius: 4px; }
|
|
15
|
+
body { margin: 0; background: var(--bg); color: var(--fg); height: 100vh;
|
|
16
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
17
|
+
font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
|
|
18
|
+
button { background: var(--panel2); color: var(--fg); border: 1px solid var(--line);
|
|
19
|
+
border-radius: 6px; padding: 2px 10px; cursor: pointer; font: inherit; }
|
|
20
|
+
button:hover { border-color: var(--accent); }
|
|
21
|
+
|
|
22
|
+
/* 顶部标题栏:标题 + 连接状态点 + 刷新 */
|
|
23
|
+
#topbar { display: flex; align-items: center; gap: 8px; padding: 6px 10px;
|
|
24
|
+
background: var(--panel2); border-bottom: 1px solid var(--line); }
|
|
25
|
+
#topbar .title { font-weight: 600; }
|
|
26
|
+
#status { display: flex; align-items: center; gap: 5px; color: var(--dim); }
|
|
27
|
+
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--err); display: inline-block; }
|
|
28
|
+
.dot.on { background: var(--ok); }
|
|
29
|
+
#refresh { margin-left: auto; }
|
|
30
|
+
|
|
31
|
+
/* 标签页 */
|
|
32
|
+
#tabs { display: flex; gap: 2px; padding: 0 6px; background: var(--panel);
|
|
33
|
+
border-bottom: 1px solid var(--line); }
|
|
34
|
+
.tab { background: none; border: none; border-bottom: 2px solid transparent; border-radius: 0;
|
|
35
|
+
color: var(--dim); padding: 5px 10px; cursor: pointer; font: inherit; }
|
|
36
|
+
.tab:hover { color: var(--fg); }
|
|
37
|
+
.tab.active { color: var(--fg); border-bottom-color: var(--accent); }
|
|
38
|
+
|
|
39
|
+
main { flex: 1; overflow: auto; }
|
|
40
|
+
|
|
41
|
+
/* 组件树:缩进列表 */
|
|
42
|
+
.tree-line { padding: 2px 8px; white-space: nowrap; }
|
|
43
|
+
.tree-line:hover { background: var(--panel2); }
|
|
44
|
+
.empty { color: var(--dim); padding: 8px 10px; }
|
|
45
|
+
|
|
46
|
+
/* 信号列表 */
|
|
47
|
+
table { width: 100%; border-collapse: collapse; }
|
|
48
|
+
th { text-align: left; font-weight: normal; color: var(--dim); padding: 4px 8px;
|
|
49
|
+
border-bottom: 1px solid var(--line); background: var(--bg);
|
|
50
|
+
position: sticky; top: 0; }
|
|
51
|
+
td { padding: 2px 8px; border-bottom: 1px solid rgba(35, 43, 59, .5); white-space: nowrap; }
|
|
52
|
+
td.val { max-width: 260px; overflow: hidden; text-overflow: ellipsis; }
|
|
53
|
+
|
|
54
|
+
/* 依赖图:canvas 渲染区占满标签页剩余空间 */
|
|
55
|
+
#view-graph { height: 100%; }
|
|
56
|
+
#graph { width: 100%; height: 100%; }
|
|
57
|
+
|
|
58
|
+
/* 值变化行内闪烁(0.6s) */
|
|
59
|
+
@keyframes flash { from { background: rgba(79, 140, 255, .28); } to { background: transparent; } }
|
|
60
|
+
.flash { animation: flash .6s ease-out; }
|
|
61
|
+
|
|
62
|
+
/* Inspector 详情条(选中信号后出现) */
|
|
63
|
+
#inspector { display: flex; align-items: center; gap: 6px; padding: 5px 10px;
|
|
64
|
+
background: var(--panel2); border-bottom: 1px solid var(--line); }
|
|
65
|
+
#inspector[hidden] { display: none; }
|
|
66
|
+
#inspector input { background: var(--bg); color: var(--fg); border: 1px solid var(--line);
|
|
67
|
+
border-radius: 6px; padding: 2px 8px; font: inherit; width: 200px; }
|
|
68
|
+
#inspector input:focus { outline: none; border-color: var(--accent); }
|
|
69
|
+
#inspector-status.ok { color: var(--ok); }
|
|
70
|
+
#inspector-status.err { color: var(--err); }
|
|
71
|
+
#inspector-status.dim { color: var(--dim); }
|
|
72
|
+
tr.selected td { background: rgba(79, 140, 255, .12); }
|
|
73
|
+
|
|
74
|
+
/* Timeline:纵向时间轴(最新在上) */
|
|
75
|
+
#view-timeline { height: 100%; }
|
|
76
|
+
#timeline { height: 100%; overflow: auto; padding: 4px 0; }
|
|
77
|
+
.tl-row { display: flex; gap: 8px; padding: 1px 10px; white-space: nowrap; align-items: baseline; }
|
|
78
|
+
.tl-row:hover { background: var(--panel2); }
|
|
79
|
+
.tl-time { color: var(--dim); }
|
|
80
|
+
.tl-chip { flex: none; min-width: 64px; text-align: center; border-radius: 8px;
|
|
81
|
+
padding: 0 8px; font-size: 11px; border: 1px solid var(--line); }
|
|
82
|
+
.tl-chip-event { color: var(--accent); border-color: var(--accent); }
|
|
83
|
+
.tl-chip-flush { color: var(--ok); border-color: var(--ok); }
|
|
84
|
+
.tl-chip-signal { color: var(--fg); }
|
|
85
|
+
.tl-chip-other { color: var(--dim); }
|
|
86
|
+
.tl-row.tl-event { cursor: pointer; }
|
|
87
|
+
.tl-row.linked { background: rgba(79, 140, 255, .18); } /* event 点击关联高亮的 flush 行 */
|
|
88
|
+
</style>
|
|
89
|
+
</head>
|
|
90
|
+
<body>
|
|
91
|
+
<header id="topbar">
|
|
92
|
+
<span class="title">Citrine DevTools · v1</span>
|
|
93
|
+
<span id="status"><span id="dot" class="dot"></span><span id="status-text">未连接</span></span>
|
|
94
|
+
<button id="refresh" type="button">刷新</button>
|
|
95
|
+
</header>
|
|
96
|
+
<nav id="tabs">
|
|
97
|
+
<button class="tab active" data-tab="tree" type="button">组件树</button>
|
|
98
|
+
<button class="tab" data-tab="signals" type="button">信号列表</button>
|
|
99
|
+
<button class="tab" data-tab="graph" type="button">依赖图</button>
|
|
100
|
+
<button class="tab" data-tab="timeline" type="button">Timeline</button>
|
|
101
|
+
</nav>
|
|
102
|
+
<main>
|
|
103
|
+
<section id="view-tree"><div id="tree"></div></section>
|
|
104
|
+
<section id="view-signals" hidden>
|
|
105
|
+
<div id="inspector" hidden>
|
|
106
|
+
<span>选中 <b id="inspector-sig">#----</b></span>
|
|
107
|
+
<span id="inspector-value" class="val"></span>
|
|
108
|
+
<input id="inspector-input" type="text" spellcheck="false"
|
|
109
|
+
placeholder="新值:数字/布尔/null/JSON/裸字符串">
|
|
110
|
+
<button id="inspector-write" type="button">写入</button>
|
|
111
|
+
<button id="inspector-rerun" type="button">force_rerun</button>
|
|
112
|
+
<span id="inspector-status"></span>
|
|
113
|
+
</div>
|
|
114
|
+
<table>
|
|
115
|
+
<thead>
|
|
116
|
+
<tr><th>sig</th><th>当前值</th><th>写入次数</th><th>最近写入</th><th>订阅数</th><th>runs</th></tr>
|
|
117
|
+
</thead>
|
|
118
|
+
<tbody id="signals-body"></tbody>
|
|
119
|
+
</table>
|
|
120
|
+
</section>
|
|
121
|
+
<section id="view-graph" hidden><div id="graph"></div></section>
|
|
122
|
+
<section id="view-timeline" hidden><div id="timeline"></div></section>
|
|
123
|
+
</main>
|
|
124
|
+
<script src="/vendor/cytoscape.min.js"></script>
|
|
125
|
+
<script src="/panel.js"></script>
|
|
126
|
+
</body>
|
|
127
|
+
</html>
|