@proteus-vue/hmr 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.
- package/README.md +122 -0
- package/dist/cdp/index.d.ts +63 -0
- package/dist/cdp/index.js +109 -0
- package/dist/client.d.ts +37 -0
- package/dist/dev-server/index.d.ts +66 -0
- package/dist/dev-server/index.js +263 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +273 -0
- package/dist/runtime.d.ts +39 -0
- package/dist/safe-reload.d.ts +15 -0
- package/dist/style-gate/index.d.ts +29 -0
- package/dist/style-gate/index.js +55 -0
- package/dist/types.d.ts +58 -0
- package/dist/vue-adapter.d.ts +28 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# @proteus-vue/hmr
|
|
2
|
+
|
|
3
|
+
Proteus HMR 运行时(devtools-plus **G-34 M1**)——HMR payload 协议 + 运行时 + Vue `import.meta.hot` 适配 + WebSocket 客户端 + 安全 reload。**开发期工具链**,不随业务产物发布;**零依赖**(全部接口注入式),纯逻辑可单测(Web 端优先)。
|
|
4
|
+
|
|
5
|
+
## 导出
|
|
6
|
+
|
|
7
|
+
| API | 说明 |
|
|
8
|
+
|-----|------|
|
|
9
|
+
| `createHmrRuntime(options)` | **HMR Runtime 核心**:接收增量 payload → 分派(vue/js 热替换 / native-binding 安全 reload / css-asset 资源替换)+ 按 id 防乱序 + 状态保留 + 严格规则 + 可观测事件;**`applyBatch` 批量接口**(同文件合并只保留最终状态 + 按 id 排序) |
|
|
10
|
+
| `createHmrClient(options)` | **WebSocket 客户端**:连接 Dev Server → payload 分发到 runtime;断线指数退避重连(maxAttempts 上限);`createSocket` 注入可单测 |
|
|
11
|
+
| `createVueHotAdapter(options)` | **Vue `import.meta.hot` 适配**:accept/dispose/invalidate 语义面 + `applyWithState` 状态保留(dispose 快照 → 新模块恢复,Flutter Hot Reload 体验);无 hot 环境安全降级 no-op |
|
|
12
|
+
| `createSafeReload(options)` | **安全 reload**(HMR002):保存状态(collect 注入;M3 原生侧联动 Router G-32 栈序列化)→ reload → 恢复;Web 实现用 sessionStorage + location.reload |
|
|
13
|
+
|
|
14
|
+
## 子路径:`@proteus-vue/hmr/dev-server`(Node 侧,G-34 收尾)
|
|
15
|
+
|
|
16
|
+
| API | 说明 |
|
|
17
|
+
|-----|------|
|
|
18
|
+
| `createHmrDevServer(options)` | **HMR Dev Server**:WS 服务端(host 缺省 127.0.0.1)+ 文件 watch 防抖管线 + **增量编译回调注入**(`compile(files) → HmrPayload[]`)→ 广播;链路:保存文件 → watch 收集 → 防抖合并 → 增量编译 → WS 广播 → 客户端 Runtime 应用;`port 0` 随机端口 + 实际端口 getter |
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { createHmrDevServer } from '@proteus-vue/hmr/dev-server'
|
|
22
|
+
|
|
23
|
+
const server = createHmrDevServer({
|
|
24
|
+
port: 5174,
|
|
25
|
+
watchRoots: ['pages', 'src'],
|
|
26
|
+
debounceMs: 300,
|
|
27
|
+
compile: (files) => files.filter((f) => f.endsWith('.vue')).map((f) => ({
|
|
28
|
+
id: ++id, file: relative(root, f), type: 'vue', action: 'update', timestamp: Date.now(),
|
|
29
|
+
code: compileVueSfc(readFileSync(f, 'utf-8')).js, // 单文件增量编译
|
|
30
|
+
})),
|
|
31
|
+
onEvent: (e) => console.log('[hmr]', e.type),
|
|
32
|
+
})
|
|
33
|
+
await server.start() // WS 监听 + watch 启动
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 子路径:`@proteus-vue/hmr/cdp`(G-34 M2:DevTools 桥接)
|
|
37
|
+
|
|
38
|
+
| API | 说明 |
|
|
39
|
+
|-----|------|
|
|
40
|
+
| `createCdpBridge(options)` | **CDP 桥接**:处理 CDP 命令子集(`Runtime.enable/disable/evaluate` + `Proteus.enable/disable/getStyleGates` 自定义域,未知方法 -32601)+ Proteus 事件(TraceBus trace / HMR hmr / style-gate)→ CDP 消息转译(`Runtime.consoleAPICalled` + 结构化 `Proteus.event` / `Proteus.styleGate`);`styleGateBufferSize` 环形缓冲(`Proteus.getStyleGates` 查询);注入式(transport/subscribe/evaluate) |
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { createCdpBridge } from '@proteus-vue/hmr/cdp'
|
|
44
|
+
|
|
45
|
+
const bridge = createCdpBridge({
|
|
46
|
+
transport: { send: (m) => ws.send(JSON.stringify(m)) }, // CDP 客户端通道
|
|
47
|
+
evaluate: async (expr) => (0, eval)(expr), // Runtime.evaluate
|
|
48
|
+
})
|
|
49
|
+
bridge.handleMessage({ id: 1, method: 'Runtime.enable' })
|
|
50
|
+
bridge.push({ kind: 'trace', event: traceEvent }) // TraceBus 事件接入
|
|
51
|
+
bridge.push({ kind: 'style-gate', record: gateRecord }) // Style Gate 可视化
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 子路径:`@proteus-vue/hmr/style-gate`(G-34 M2:Style Safety 可视化数据源)
|
|
55
|
+
|
|
56
|
+
| API | 说明 |
|
|
57
|
+
|-----|------|
|
|
58
|
+
| `collectStyleGateRecords(style, options)` | **样式闸门链可视化**:每条样式经 白名单 → 校验 → 平台收窄 三道闸门的完整轨迹(`StyleGateRecord { gates, decision: pass\|narrow\|drop, nativeValues, rejectReason }`);`allPlatforms` 产出五端原生值映射(联动 `@proteus-vue/runtime/style-safety`) |
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { collectStyleGateRecords } from '@proteus-vue/hmr/style-gate'
|
|
62
|
+
|
|
63
|
+
const records = collectStyleGateRecords({ width: '100px', display: 'flex' }, { platform: 'skyline', allPlatforms: true })
|
|
64
|
+
// width → decision: 'narrow',nativeValues.skyline = 100(px → 数值)
|
|
65
|
+
// display→ decision: 'drop',rejectReason 含 STS004(FORBIDDEN)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
★生产接入参考 `examples/scripts/dev-mp.ts`(dev-mp 已内置 HMR dev server:改 .vue → compileVueSfc 增量 → WS 广播,E2E 实测闭环通过)。
|
|
69
|
+
|
|
70
|
+
## 类型
|
|
71
|
+
|
|
72
|
+
- `HmrPayload`:`{ id, file, type: 'vue'|'js'|'css'|'asset'|'native-binding', code?, timestamp, action: 'update'|'reload' }`——一次增量更新的传输单元
|
|
73
|
+
- `HmrEvent`:可观测事件(connected/disconnected/reconnecting/payload/apply/rule/error)——**原则 #3 编译透明的 DevTools 数据源**
|
|
74
|
+
- `HmrTransport` / `HmrWebSocketLike` / `SafeReload`:结构类型,注入式零硬依赖
|
|
75
|
+
|
|
76
|
+
## 严格规则
|
|
77
|
+
|
|
78
|
+
| 编号 | 规则 | 处理 |
|
|
79
|
+
|------|------|------|
|
|
80
|
+
| HMR001 | 替换前存在未清理的模块实例(副作用未 dispose) | warn(`checkSideEffects` 可关) |
|
|
81
|
+
| HMR002 | 原生 binding 变更无法热替换 | 自动安全 reload |
|
|
82
|
+
| HMR003 | 状态丢失检测(reload/应用失败/缺 code) | warn + 降级安全 reload(`checkStateLoss` 可关) |
|
|
83
|
+
|
|
84
|
+
## 使用
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { createHmrRuntime, createHmrClient, createSafeReload } from '@proteus-vue/hmr'
|
|
88
|
+
|
|
89
|
+
// 1. 运行时(模块应用器 + 安全 reload + 可观测)
|
|
90
|
+
const runtime = createHmrRuntime({
|
|
91
|
+
applyModule(file, code) {
|
|
92
|
+
// vue/js:应用新模块(Vite import.meta.hot / 编译产物替换)
|
|
93
|
+
return applyVueModule(file, code) // 失败返回 false → 自动降级安全 reload
|
|
94
|
+
},
|
|
95
|
+
reload: safeReload.reload,
|
|
96
|
+
onEvent: (e) => console.log('[hmr]', e), // DevTools 面板数据源
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// 2. Dev Server 推送 → WS 客户端(自动重连)
|
|
100
|
+
const client = createHmrClient({
|
|
101
|
+
url: 'ws://localhost:5174/__proteus_hmr__',
|
|
102
|
+
runtime,
|
|
103
|
+
})
|
|
104
|
+
client.connect()
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 性能预算(G-34 §6)
|
|
108
|
+
|
|
109
|
+
| 指标 | 预算 | 现状 |
|
|
110
|
+
|------|------|------|
|
|
111
|
+
| HMR 编译耗时 | < 50ms | 🔶 编译侧增量未落地(dev-mp 全量重建 + M8 缓存) |
|
|
112
|
+
| 推送到渲染 | < 100ms | ✅ 1000 payload 批量应用实测 < 100ms(单测基准断言) |
|
|
113
|
+
| 安全 reload | < 2s | 🔶 未验证(Web 实现为 location.reload) |
|
|
114
|
+
| DevTools 开销 | < 5% CPU | 🔶 M2 面板未落地 |
|
|
115
|
+
|
|
116
|
+
**运行时侧已做优化**:批量 payload 同文件合并(一次保存多文件变更 → 只保留最终状态,中间态丢弃)+ 乱序 batch 按 id 全局排序 + 单条消息可带 payload 数组(client 自动走 `applyBatch`)。
|
|
117
|
+
|
|
118
|
+
## 后续里程碑
|
|
119
|
+
|
|
120
|
+
- **M2** DevTools 桥接(CDP + Style Safety 可视化)
|
|
121
|
+
- **M3** 原生侧安全 reload(iOS/Android/鸿蒙,联动 App Renderer + Router G-32)
|
|
122
|
+
- **M4** 原生视图检查器 + LeakRegistry 集成
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { HmrEvent } from '../types';
|
|
2
|
+
import type { StyleGateRecord } from '../style-gate';
|
|
3
|
+
/** TraceBus 事件形状(结构类型:@proteus-vue/devtools-runtime 的 TraceEvent 天然符合) */
|
|
4
|
+
export interface TraceEventLike {
|
|
5
|
+
source: string;
|
|
6
|
+
phase: string;
|
|
7
|
+
name: string;
|
|
8
|
+
payload?: unknown;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
traceId?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Proteus 开发事件(桥接输入:trace / hmr / style-gate 三类) */
|
|
13
|
+
export type ProteusDevEvent = {
|
|
14
|
+
kind: 'trace';
|
|
15
|
+
event: TraceEventLike;
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'hmr';
|
|
18
|
+
event: HmrEvent;
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'style-gate';
|
|
21
|
+
record: StyleGateRecord;
|
|
22
|
+
};
|
|
23
|
+
/** CDP 消息(标准 JSON-RPC 形状:请求/响应 { id } / 事件推送 { method, params } 无 id) */
|
|
24
|
+
export interface CdpMessage {
|
|
25
|
+
id?: number;
|
|
26
|
+
method?: string;
|
|
27
|
+
params?: Record<string, unknown>;
|
|
28
|
+
result?: unknown;
|
|
29
|
+
error?: {
|
|
30
|
+
code: number;
|
|
31
|
+
message: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** CDP 客户端传输(注入:WS / 内存通道均可) */
|
|
35
|
+
export interface CdpTransport {
|
|
36
|
+
send(message: CdpMessage): void;
|
|
37
|
+
}
|
|
38
|
+
export interface CdpBridgeOptions {
|
|
39
|
+
/** 客户端传输 */
|
|
40
|
+
transport: CdpTransport;
|
|
41
|
+
/** Runtime.evaluate 执行器(缺省拒绝——未注入时返回 error) */
|
|
42
|
+
evaluate?: (expression: string) => Promise<unknown>;
|
|
43
|
+
/** ★应用信息提供器(Proteus.appInfo 命令——面板 pages/依赖图数据;缺省返回空对象) */
|
|
44
|
+
appInfo?: () => unknown;
|
|
45
|
+
/** StyleGateRecord 环形缓冲上限(缺省 500;Proteus.getStyleGates 查询用) */
|
|
46
|
+
styleGateBufferSize?: number;
|
|
47
|
+
/** 可观测(桥接自身事件:client 命令/推送) */
|
|
48
|
+
onEvent?: (event: {
|
|
49
|
+
type: 'command' | 'push';
|
|
50
|
+
method: string;
|
|
51
|
+
}) => void;
|
|
52
|
+
}
|
|
53
|
+
export interface CdpBridge {
|
|
54
|
+
/** 处理客户端一条命令(响应经 transport.send 回发) */
|
|
55
|
+
handleMessage(message: CdpMessage): void;
|
|
56
|
+
/** 推送 Proteus 事件(按客户端 enable 状态转 CDP 消息) */
|
|
57
|
+
push(event: ProteusDevEvent): void;
|
|
58
|
+
/** 查询已收集的 StyleGateRecord 缓冲 */
|
|
59
|
+
styleGates(): StyleGateRecord[];
|
|
60
|
+
readonly runtimeEnabled: boolean;
|
|
61
|
+
readonly proteusEnabled: boolean;
|
|
62
|
+
}
|
|
63
|
+
export declare function createCdpBridge(options: CdpBridgeOptions): CdpBridge;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// src/cdp/index.ts
|
|
2
|
+
function createCdpBridge(options) {
|
|
3
|
+
const { transport, onEvent } = options;
|
|
4
|
+
const evaluate = options.evaluate ?? (() => Promise.reject(new Error("Runtime.evaluate \u672A\u6CE8\u5165\u6267\u884C\u5668")));
|
|
5
|
+
const bufferSize = options.styleGateBufferSize ?? 500;
|
|
6
|
+
let runtimeEnabled = false;
|
|
7
|
+
let proteusEnabled = false;
|
|
8
|
+
const gateBuffer = [];
|
|
9
|
+
function reply(id, result) {
|
|
10
|
+
if (id === void 0) return;
|
|
11
|
+
transport.send({ id, result });
|
|
12
|
+
}
|
|
13
|
+
function replyError(id, code, message) {
|
|
14
|
+
if (id === void 0) return;
|
|
15
|
+
transport.send({ id, error: { code, message } });
|
|
16
|
+
}
|
|
17
|
+
function handleMessage(message) {
|
|
18
|
+
const { id, method } = message;
|
|
19
|
+
if (!method) {
|
|
20
|
+
replyError(id, -32600, "Invalid Request\uFF1A\u7F3A method");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
onEvent?.({ type: "command", method });
|
|
24
|
+
switch (method) {
|
|
25
|
+
case "Runtime.enable":
|
|
26
|
+
runtimeEnabled = true;
|
|
27
|
+
reply(id, {});
|
|
28
|
+
return;
|
|
29
|
+
case "Runtime.disable":
|
|
30
|
+
runtimeEnabled = false;
|
|
31
|
+
reply(id, {});
|
|
32
|
+
return;
|
|
33
|
+
case "Proteus.enable":
|
|
34
|
+
proteusEnabled = true;
|
|
35
|
+
reply(id, {});
|
|
36
|
+
return;
|
|
37
|
+
case "Proteus.disable":
|
|
38
|
+
proteusEnabled = false;
|
|
39
|
+
reply(id, {});
|
|
40
|
+
return;
|
|
41
|
+
case "Runtime.evaluate": {
|
|
42
|
+
const expression = String(message.params?.expression ?? "");
|
|
43
|
+
void evaluate(expression).then((value) => reply(id, { result: { type: "string", value: JSON.stringify(value) } })).catch((err) => replyError(id, -32e3, String(err instanceof Error ? err.message : err)));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
case "Proteus.getStyleGates":
|
|
47
|
+
reply(id, { records: gateBuffer });
|
|
48
|
+
return;
|
|
49
|
+
case "Proteus.appInfo":
|
|
50
|
+
reply(id, options.appInfo ? options.appInfo() : {});
|
|
51
|
+
return;
|
|
52
|
+
default:
|
|
53
|
+
replyError(id, -32601, `Method not found: ${method}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function push(event) {
|
|
57
|
+
if (event.kind === "style-gate") {
|
|
58
|
+
const record = event.record;
|
|
59
|
+
gateBuffer.push(record);
|
|
60
|
+
if (gateBuffer.length > bufferSize) gateBuffer.splice(0, gateBuffer.length - bufferSize);
|
|
61
|
+
if (proteusEnabled) {
|
|
62
|
+
onEvent?.({ type: "push", method: "Proteus.styleGate" });
|
|
63
|
+
transport.send({ method: "Proteus.styleGate", params: { record } });
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const source = event.kind === "trace" ? event.event.source : "hmr";
|
|
68
|
+
const name = event.kind === "trace" ? event.event.name : event.event.type;
|
|
69
|
+
const timestamp = event.kind === "trace" ? event.event.timestamp : Date.now();
|
|
70
|
+
if (runtimeEnabled) {
|
|
71
|
+
onEvent?.({ type: "push", method: "Runtime.consoleAPICalled" });
|
|
72
|
+
transport.send({
|
|
73
|
+
method: "Runtime.consoleAPICalled",
|
|
74
|
+
params: {
|
|
75
|
+
type: "info",
|
|
76
|
+
args: [{ type: "string", value: `[${source}] ${name}` }],
|
|
77
|
+
timestamp
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (proteusEnabled) {
|
|
82
|
+
onEvent?.({ type: "push", method: "Proteus.event" });
|
|
83
|
+
transport.send({
|
|
84
|
+
method: "Proteus.event",
|
|
85
|
+
params: {
|
|
86
|
+
source,
|
|
87
|
+
name,
|
|
88
|
+
phase: event.kind === "trace" ? event.event.phase : void 0,
|
|
89
|
+
payload: event.kind === "trace" ? event.event.payload : event.event,
|
|
90
|
+
timestamp
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
handleMessage,
|
|
97
|
+
push,
|
|
98
|
+
styleGates: () => gateBuffer,
|
|
99
|
+
get runtimeEnabled() {
|
|
100
|
+
return runtimeEnabled;
|
|
101
|
+
},
|
|
102
|
+
get proteusEnabled() {
|
|
103
|
+
return proteusEnabled;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export {
|
|
108
|
+
createCdpBridge
|
|
109
|
+
};
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { HmrEvent } from './types';
|
|
2
|
+
import type { HmrRuntime } from './runtime';
|
|
3
|
+
/** WebSocket 形状(结构类型:原生 WebSocket / mock 均可注入) */
|
|
4
|
+
export interface HmrWebSocketLike {
|
|
5
|
+
onopen: (() => void) | null;
|
|
6
|
+
onmessage: ((ev: {
|
|
7
|
+
data: unknown;
|
|
8
|
+
}) => void) | null;
|
|
9
|
+
onclose: ((ev: {
|
|
10
|
+
code?: number;
|
|
11
|
+
reason?: string;
|
|
12
|
+
}) => void) | null;
|
|
13
|
+
onerror: (() => void) | null;
|
|
14
|
+
close(): void;
|
|
15
|
+
}
|
|
16
|
+
export interface HmrClientOptions {
|
|
17
|
+
/** Dev Server WebSocket 地址(如 ws://localhost:5174/__proteus_hmr__) */
|
|
18
|
+
url: string;
|
|
19
|
+
runtime: HmrRuntime;
|
|
20
|
+
/** WebSocket 工厂(注入可单测;缺省 new WebSocket(url)) */
|
|
21
|
+
createSocket?: (url: string) => HmrWebSocketLike;
|
|
22
|
+
/** 重连:maxAttempts(缺省 -1 无限)+ baseDelayMs(指数退避 baseDelayMs * 2^attempt,缺省 500) */
|
|
23
|
+
reconnect?: {
|
|
24
|
+
maxAttempts?: number;
|
|
25
|
+
baseDelayMs?: number;
|
|
26
|
+
};
|
|
27
|
+
onEvent?: (event: HmrEvent) => void;
|
|
28
|
+
}
|
|
29
|
+
export interface HmrClient {
|
|
30
|
+
connect(): void;
|
|
31
|
+
disconnect(): void;
|
|
32
|
+
/** 当前是否已连接 */
|
|
33
|
+
readonly connected: boolean;
|
|
34
|
+
/** 已尝试的重连次数 */
|
|
35
|
+
readonly attempt: number;
|
|
36
|
+
}
|
|
37
|
+
export declare function createHmrClient(options: HmrClientOptions): HmrClient;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { HmrPayload } from '../types';
|
|
2
|
+
/** Dev Server 可观测事件(原则 #3 编译透明:编译→推送全链路可见) */
|
|
3
|
+
export type HmrDevServerEvent = {
|
|
4
|
+
type: 'listening';
|
|
5
|
+
port: number;
|
|
6
|
+
} | {
|
|
7
|
+
type: 'client-connect';
|
|
8
|
+
clientCount: number;
|
|
9
|
+
} | {
|
|
10
|
+
type: 'client-disconnect';
|
|
11
|
+
clientCount: number;
|
|
12
|
+
} | {
|
|
13
|
+
type: 'files-changed';
|
|
14
|
+
files: string[];
|
|
15
|
+
} | {
|
|
16
|
+
type: 'compiled';
|
|
17
|
+
payloads: HmrPayload[];
|
|
18
|
+
} | {
|
|
19
|
+
type: 'broadcast';
|
|
20
|
+
payloads: HmrPayload[];
|
|
21
|
+
} | {
|
|
22
|
+
type: 'error';
|
|
23
|
+
message: string;
|
|
24
|
+
};
|
|
25
|
+
export interface HmrDevServerOptions {
|
|
26
|
+
/** WS 监听端口(0 = 随机) */
|
|
27
|
+
port: number;
|
|
28
|
+
/** WS 监听主机(缺省 127.0.0.1——开发工具本机服务;★显式 IPv4 避免双栈 `::` 绑定下 IPv4 连接不达) */
|
|
29
|
+
host?: string;
|
|
30
|
+
/** 监听根目录(文件变更触发增量编译 + 广播) */
|
|
31
|
+
watchRoots: string[];
|
|
32
|
+
/** 忽略规则(返回 true 跳过;缺省忽略 node_modules/dist/.git 与隐藏文件) */
|
|
33
|
+
ignore?: (file: string) => boolean;
|
|
34
|
+
/** 防抖毫秒(缺省 300——一次保存的多文件变更合并为一批) */
|
|
35
|
+
debounceMs?: number;
|
|
36
|
+
/** ★增量编译回调:变更文件集合 → 增量 payload(编译侧注入) */
|
|
37
|
+
compile: (files: string[]) => HmrPayload[];
|
|
38
|
+
/** ★DevTools 面板 CDP 桥:Runtime.evaluate 执行器(可选——未注入时面板 evaluate 返回错误) */
|
|
39
|
+
evaluate?: (expression: string) => Promise<unknown>;
|
|
40
|
+
/** ★DevTools 面板 CDP 桥:应用信息提供器(Proteus.appInfo 命令——面板 pages/依赖图数据源;缺省返回空对象) */
|
|
41
|
+
appInfo?: () => unknown;
|
|
42
|
+
/** 外部 TraceBus 事件注入(可选——面板 Proteus.event 通道数据源) */
|
|
43
|
+
traceSource?: () => Array<{
|
|
44
|
+
source: string;
|
|
45
|
+
phase: string;
|
|
46
|
+
name: string;
|
|
47
|
+
payload?: unknown;
|
|
48
|
+
timestamp: number;
|
|
49
|
+
traceId?: string;
|
|
50
|
+
}>;
|
|
51
|
+
/** 可观测 */
|
|
52
|
+
onEvent?: (event: HmrDevServerEvent) => void;
|
|
53
|
+
}
|
|
54
|
+
export interface HmrDevServer {
|
|
55
|
+
/** 启动(WS 监听 + watch) */
|
|
56
|
+
start(): Promise<void>;
|
|
57
|
+
/** 停止(关闭 WS + 解绑 watch) */
|
|
58
|
+
close(): Promise<void>;
|
|
59
|
+
/** 手动广播 payload(不经过编译管线) */
|
|
60
|
+
broadcast(payloads: HmrPayload[]): void;
|
|
61
|
+
/** WS 端口 */
|
|
62
|
+
readonly port: number;
|
|
63
|
+
/** 当前连接客户端数 */
|
|
64
|
+
readonly clientCount: number;
|
|
65
|
+
}
|
|
66
|
+
export declare function createHmrDevServer(options: HmrDevServerOptions): HmrDevServer;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// src/dev-server/index.ts
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
5
|
+
|
|
6
|
+
// src/cdp/index.ts
|
|
7
|
+
function createCdpBridge(options) {
|
|
8
|
+
const { transport, onEvent } = options;
|
|
9
|
+
const evaluate = options.evaluate ?? (() => Promise.reject(new Error("Runtime.evaluate \u672A\u6CE8\u5165\u6267\u884C\u5668")));
|
|
10
|
+
const bufferSize = options.styleGateBufferSize ?? 500;
|
|
11
|
+
let runtimeEnabled = false;
|
|
12
|
+
let proteusEnabled = false;
|
|
13
|
+
const gateBuffer = [];
|
|
14
|
+
function reply(id, result) {
|
|
15
|
+
if (id === void 0) return;
|
|
16
|
+
transport.send({ id, result });
|
|
17
|
+
}
|
|
18
|
+
function replyError(id, code, message) {
|
|
19
|
+
if (id === void 0) return;
|
|
20
|
+
transport.send({ id, error: { code, message } });
|
|
21
|
+
}
|
|
22
|
+
function handleMessage(message) {
|
|
23
|
+
const { id, method } = message;
|
|
24
|
+
if (!method) {
|
|
25
|
+
replyError(id, -32600, "Invalid Request\uFF1A\u7F3A method");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
onEvent?.({ type: "command", method });
|
|
29
|
+
switch (method) {
|
|
30
|
+
case "Runtime.enable":
|
|
31
|
+
runtimeEnabled = true;
|
|
32
|
+
reply(id, {});
|
|
33
|
+
return;
|
|
34
|
+
case "Runtime.disable":
|
|
35
|
+
runtimeEnabled = false;
|
|
36
|
+
reply(id, {});
|
|
37
|
+
return;
|
|
38
|
+
case "Proteus.enable":
|
|
39
|
+
proteusEnabled = true;
|
|
40
|
+
reply(id, {});
|
|
41
|
+
return;
|
|
42
|
+
case "Proteus.disable":
|
|
43
|
+
proteusEnabled = false;
|
|
44
|
+
reply(id, {});
|
|
45
|
+
return;
|
|
46
|
+
case "Runtime.evaluate": {
|
|
47
|
+
const expression = String(message.params?.expression ?? "");
|
|
48
|
+
void evaluate(expression).then((value) => reply(id, { result: { type: "string", value: JSON.stringify(value) } })).catch((err) => replyError(id, -32e3, String(err instanceof Error ? err.message : err)));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
case "Proteus.getStyleGates":
|
|
52
|
+
reply(id, { records: gateBuffer });
|
|
53
|
+
return;
|
|
54
|
+
case "Proteus.appInfo":
|
|
55
|
+
reply(id, options.appInfo ? options.appInfo() : {});
|
|
56
|
+
return;
|
|
57
|
+
default:
|
|
58
|
+
replyError(id, -32601, `Method not found: ${method}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function push(event) {
|
|
62
|
+
if (event.kind === "style-gate") {
|
|
63
|
+
const record = event.record;
|
|
64
|
+
gateBuffer.push(record);
|
|
65
|
+
if (gateBuffer.length > bufferSize) gateBuffer.splice(0, gateBuffer.length - bufferSize);
|
|
66
|
+
if (proteusEnabled) {
|
|
67
|
+
onEvent?.({ type: "push", method: "Proteus.styleGate" });
|
|
68
|
+
transport.send({ method: "Proteus.styleGate", params: { record } });
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const source = event.kind === "trace" ? event.event.source : "hmr";
|
|
73
|
+
const name = event.kind === "trace" ? event.event.name : event.event.type;
|
|
74
|
+
const timestamp = event.kind === "trace" ? event.event.timestamp : Date.now();
|
|
75
|
+
if (runtimeEnabled) {
|
|
76
|
+
onEvent?.({ type: "push", method: "Runtime.consoleAPICalled" });
|
|
77
|
+
transport.send({
|
|
78
|
+
method: "Runtime.consoleAPICalled",
|
|
79
|
+
params: {
|
|
80
|
+
type: "info",
|
|
81
|
+
args: [{ type: "string", value: `[${source}] ${name}` }],
|
|
82
|
+
timestamp
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (proteusEnabled) {
|
|
87
|
+
onEvent?.({ type: "push", method: "Proteus.event" });
|
|
88
|
+
transport.send({
|
|
89
|
+
method: "Proteus.event",
|
|
90
|
+
params: {
|
|
91
|
+
source,
|
|
92
|
+
name,
|
|
93
|
+
phase: event.kind === "trace" ? event.event.phase : void 0,
|
|
94
|
+
payload: event.kind === "trace" ? event.event.payload : event.event,
|
|
95
|
+
timestamp
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
handleMessage,
|
|
102
|
+
push,
|
|
103
|
+
styleGates: () => gateBuffer,
|
|
104
|
+
get runtimeEnabled() {
|
|
105
|
+
return runtimeEnabled;
|
|
106
|
+
},
|
|
107
|
+
get proteusEnabled() {
|
|
108
|
+
return proteusEnabled;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/dev-server/index.ts
|
|
114
|
+
function defaultIgnore(file) {
|
|
115
|
+
const name = path.basename(file);
|
|
116
|
+
if (name.startsWith(".")) return true;
|
|
117
|
+
return /(^|[\\/])(node_modules|dist|\.git)([\\/]|$)/.test(file);
|
|
118
|
+
}
|
|
119
|
+
function createHmrDevServer(options) {
|
|
120
|
+
const { port, watchRoots, compile, onEvent } = options;
|
|
121
|
+
const host = options.host ?? "127.0.0.1";
|
|
122
|
+
const debounceMs = options.debounceMs ?? 300;
|
|
123
|
+
const ignore = options.ignore ?? defaultIgnore;
|
|
124
|
+
let wss = null;
|
|
125
|
+
const watchers = [];
|
|
126
|
+
const clients = /* @__PURE__ */ new Set();
|
|
127
|
+
const bridges = /* @__PURE__ */ new Map();
|
|
128
|
+
let timer = null;
|
|
129
|
+
const pendingFiles = /* @__PURE__ */ new Set();
|
|
130
|
+
function emit(event) {
|
|
131
|
+
onEvent?.(event);
|
|
132
|
+
}
|
|
133
|
+
function pushBridges(devEvent) {
|
|
134
|
+
for (const bridge of bridges.values()) bridge.push(devEvent);
|
|
135
|
+
}
|
|
136
|
+
function pushTraceEvents() {
|
|
137
|
+
const traces = options.traceSource?.();
|
|
138
|
+
if (!traces) return;
|
|
139
|
+
for (const t of traces) {
|
|
140
|
+
pushBridges({ kind: "trace", event: t });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function flush() {
|
|
144
|
+
timer = null;
|
|
145
|
+
const files = Array.from(pendingFiles).filter((f) => {
|
|
146
|
+
try {
|
|
147
|
+
return fs.existsSync(f) && fs.statSync(f).isFile();
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
pendingFiles.clear();
|
|
153
|
+
if (files.length === 0) return;
|
|
154
|
+
emit({ type: "files-changed", files });
|
|
155
|
+
pushBridges({ kind: "trace", event: { source: "compiler", phase: "start", name: "watch files", payload: { count: files.length }, timestamp: Date.now() } });
|
|
156
|
+
let payloads;
|
|
157
|
+
try {
|
|
158
|
+
payloads = compile(files);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
emit({ type: "error", message: `\u589E\u91CF\u7F16\u8BD1\u5931\u8D25\uFF1A${String(err)}` });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (payloads.length === 0) return;
|
|
164
|
+
emit({ type: "compiled", payloads });
|
|
165
|
+
pushBridges({ kind: "trace", event: { source: "compiler", phase: "end", name: "incremental compile", payload: { count: payloads.length }, timestamp: Date.now() } });
|
|
166
|
+
broadcast(payloads);
|
|
167
|
+
}
|
|
168
|
+
function schedule(file) {
|
|
169
|
+
if (ignore(file)) return;
|
|
170
|
+
pendingFiles.add(file);
|
|
171
|
+
if (timer) clearTimeout(timer);
|
|
172
|
+
timer = setTimeout(flush, debounceMs);
|
|
173
|
+
}
|
|
174
|
+
function watchRoot(dir) {
|
|
175
|
+
if (!fs.existsSync(dir)) return;
|
|
176
|
+
try {
|
|
177
|
+
const watcher = fs.watch(dir, { recursive: true }, (_ev, file) => {
|
|
178
|
+
if (file) schedule(path.join(dir, file.toString()));
|
|
179
|
+
});
|
|
180
|
+
watchers.push(watcher);
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function broadcast(payloads) {
|
|
185
|
+
emit({ type: "broadcast", payloads });
|
|
186
|
+
pushBridges({ kind: "trace", event: { source: "hmr", phase: "point", name: "hmr broadcast", payload: { count: payloads.length }, timestamp: Date.now() } });
|
|
187
|
+
const data = JSON.stringify(payloads.length === 1 ? payloads[0] : payloads);
|
|
188
|
+
for (const client of clients) {
|
|
189
|
+
if (client.readyState === WebSocket.OPEN) client.send(data);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
get port() {
|
|
194
|
+
const addr = wss?.address();
|
|
195
|
+
return typeof addr === "object" && addr !== null ? addr.port : options.port;
|
|
196
|
+
},
|
|
197
|
+
get clientCount() {
|
|
198
|
+
return clients.size;
|
|
199
|
+
},
|
|
200
|
+
async start() {
|
|
201
|
+
if (wss) return;
|
|
202
|
+
wss = new WebSocketServer({ port, host });
|
|
203
|
+
wss.on("connection", (socket) => {
|
|
204
|
+
clients.add(socket);
|
|
205
|
+
emit({ type: "client-connect", clientCount: clients.size });
|
|
206
|
+
const bridge = createCdpBridge({
|
|
207
|
+
transport: {
|
|
208
|
+
send: (m) => {
|
|
209
|
+
if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(m));
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
evaluate: options.evaluate,
|
|
213
|
+
appInfo: options.appInfo
|
|
214
|
+
});
|
|
215
|
+
bridges.set(socket, bridge);
|
|
216
|
+
socket.on("message", (data) => {
|
|
217
|
+
try {
|
|
218
|
+
const msg = JSON.parse(String(data));
|
|
219
|
+
if (msg && typeof msg === "object" && "method" in msg) bridge.handleMessage(msg);
|
|
220
|
+
} catch {
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
socket.on("close", () => {
|
|
224
|
+
clients.delete(socket);
|
|
225
|
+
bridges.delete(socket);
|
|
226
|
+
emit({ type: "client-disconnect", clientCount: clients.size });
|
|
227
|
+
});
|
|
228
|
+
pushTraceEvents();
|
|
229
|
+
});
|
|
230
|
+
await new Promise((resolve) => {
|
|
231
|
+
wss?.once("listening", () => {
|
|
232
|
+
emit({ type: "listening", port });
|
|
233
|
+
resolve();
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
for (const root of watchRoots) {
|
|
237
|
+
if (fs.existsSync(root)) watchRoot(root);
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
async close() {
|
|
241
|
+
if (timer) {
|
|
242
|
+
clearTimeout(timer);
|
|
243
|
+
timer = null;
|
|
244
|
+
}
|
|
245
|
+
pendingFiles.clear();
|
|
246
|
+
for (const watcher of watchers) watcher.close();
|
|
247
|
+
watchers.length = 0;
|
|
248
|
+
for (const client of clients) client.close();
|
|
249
|
+
clients.clear();
|
|
250
|
+
if (wss) {
|
|
251
|
+
await new Promise((resolve) => {
|
|
252
|
+
wss?.close(() => resolve());
|
|
253
|
+
setTimeout(() => resolve(), 100);
|
|
254
|
+
});
|
|
255
|
+
wss = null;
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
broadcast
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
export {
|
|
262
|
+
createHmrDevServer
|
|
263
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type { HmrEvent, HmrPayload, HmrTransport, SafeReload } from './types';
|
|
2
|
+
export { createHmrRuntime } from './runtime';
|
|
3
|
+
export type { HmrRuntime, HmrRuntimeOptions, HmrBatchResult } from './runtime';
|
|
4
|
+
export { createVueHotAdapter } from './vue-adapter';
|
|
5
|
+
export type { VueHotAdapter, VueHotAdapterOptions, VueHotApiLike } from './vue-adapter';
|
|
6
|
+
export { createHmrClient } from './client';
|
|
7
|
+
export type { HmrClient, HmrClientOptions, HmrWebSocketLike } from './client';
|
|
8
|
+
export { createSafeReload } from './safe-reload';
|
|
9
|
+
export type { SafeReloadOptions } from './safe-reload';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// src/runtime.ts
|
|
2
|
+
function createHmrRuntime(options) {
|
|
3
|
+
const { applyModule, reload, onEvent } = options;
|
|
4
|
+
const checkSideEffects = options.checkSideEffects !== false;
|
|
5
|
+
const checkStateLoss = options.checkStateLoss !== false;
|
|
6
|
+
let lastId = -1;
|
|
7
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
8
|
+
const applied = /* @__PURE__ */ new Set();
|
|
9
|
+
function emit(event) {
|
|
10
|
+
onEvent?.(event);
|
|
11
|
+
}
|
|
12
|
+
function apply(payload) {
|
|
13
|
+
if (payload.id <= lastId) {
|
|
14
|
+
emit({ type: "apply", file: payload.file, result: "skipped" });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
lastId = payload.id;
|
|
18
|
+
emit({ type: "payload", payload });
|
|
19
|
+
if (payload.action === "reload") {
|
|
20
|
+
if (checkStateLoss && snapshots.has(payload.file)) {
|
|
21
|
+
emit({ type: "rule", rule: "HMR003", file: payload.file, message: "reload \u524D\u5B58\u5728\u672A\u6062\u590D\u72B6\u6001\u5FEB\u7167\u2014\u2014\u72B6\u6001\u5C06\u4E22\u5931" });
|
|
22
|
+
}
|
|
23
|
+
emit({ type: "apply", file: payload.file, result: "reload" });
|
|
24
|
+
reload();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
switch (payload.type) {
|
|
28
|
+
case "vue":
|
|
29
|
+
case "js": {
|
|
30
|
+
if (!payload.code) {
|
|
31
|
+
emit({ type: "rule", rule: "HMR003", file: payload.file, message: "payload \u7F3A code\u2014\u2014\u65E0\u6CD5\u70ED\u66FF\u6362\uFF0C\u964D\u7EA7\u5B89\u5168 reload" });
|
|
32
|
+
emit({ type: "apply", file: payload.file, result: "reload" });
|
|
33
|
+
reload();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
let ok = false;
|
|
37
|
+
try {
|
|
38
|
+
ok = applyModule(payload.file, payload.code);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
emit({ type: "error", file: payload.file, message: `\u6A21\u5757\u5E94\u7528\u5F02\u5E38\uFF1A${String(err)}` });
|
|
41
|
+
}
|
|
42
|
+
if (ok) {
|
|
43
|
+
applied.add(payload.file);
|
|
44
|
+
emit({ type: "apply", file: payload.file, result: "ok" });
|
|
45
|
+
} else {
|
|
46
|
+
emit({ type: "rule", rule: "HMR003", file: payload.file, message: "\u6A21\u5757\u5E94\u7528\u5931\u8D25\u2014\u2014\u72B6\u6001\u65E0\u6CD5\u4FDD\u7559\uFF0C\u964D\u7EA7\u5B89\u5168 reload" });
|
|
47
|
+
emit({ type: "apply", file: payload.file, result: "reload" });
|
|
48
|
+
reload();
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
case "native-binding": {
|
|
53
|
+
emit({ type: "rule", rule: "HMR002", file: payload.file, message: "\u539F\u751F binding \u53D8\u66F4\u2014\u2014\u89E6\u53D1\u5B89\u5168 reload" });
|
|
54
|
+
emit({ type: "apply", file: payload.file, result: "reload" });
|
|
55
|
+
reload();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
case "css":
|
|
59
|
+
case "asset": {
|
|
60
|
+
applied.add(payload.file);
|
|
61
|
+
emit({ type: "apply", file: payload.file, result: "ok" });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
default: {
|
|
65
|
+
emit({ type: "apply", file: payload.file, result: "skipped" });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function applyBatch(payloads) {
|
|
70
|
+
const latest = /* @__PURE__ */ new Map();
|
|
71
|
+
let merged = 0;
|
|
72
|
+
for (const p of payloads) {
|
|
73
|
+
if (latest.has(p.file)) merged += 1;
|
|
74
|
+
latest.set(p.file, p);
|
|
75
|
+
}
|
|
76
|
+
const ordered = Array.from(latest.values()).sort((a, b) => a.id - b.id);
|
|
77
|
+
let appliedCount = 0;
|
|
78
|
+
for (const p of ordered) {
|
|
79
|
+
const before = lastId;
|
|
80
|
+
apply(p);
|
|
81
|
+
if (lastId !== before) appliedCount += 1;
|
|
82
|
+
}
|
|
83
|
+
return { total: payloads.length, merged, applied: appliedCount };
|
|
84
|
+
}
|
|
85
|
+
function snapshotState(file, state) {
|
|
86
|
+
if (checkSideEffects && snapshots.has(file)) {
|
|
87
|
+
emit({ type: "rule", rule: "HMR001", file, message: "\u66FF\u6362\u524D\u5B58\u5728\u672A\u6E05\u7406\u7684\u6A21\u5757\u5B9E\u4F8B\uFF08\u526F\u4F5C\u7528\u672A dispose\uFF09" });
|
|
88
|
+
}
|
|
89
|
+
snapshots.set(file, state);
|
|
90
|
+
}
|
|
91
|
+
function restoreState(file) {
|
|
92
|
+
const state = snapshots.get(file);
|
|
93
|
+
snapshots.delete(file);
|
|
94
|
+
return state;
|
|
95
|
+
}
|
|
96
|
+
function reset() {
|
|
97
|
+
snapshots.clear();
|
|
98
|
+
applied.clear();
|
|
99
|
+
lastId = -1;
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
apply,
|
|
103
|
+
applyBatch,
|
|
104
|
+
snapshotState,
|
|
105
|
+
restoreState,
|
|
106
|
+
appliedFiles: () => Array.from(applied),
|
|
107
|
+
reset,
|
|
108
|
+
get lastAppliedId() {
|
|
109
|
+
return lastId;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/vue-adapter.ts
|
|
115
|
+
function createVueHotAdapter(options) {
|
|
116
|
+
const getHot = options.getHot ?? (() => void 0);
|
|
117
|
+
const { runtime, file } = options;
|
|
118
|
+
return {
|
|
119
|
+
get enabled() {
|
|
120
|
+
return getHot() !== void 0;
|
|
121
|
+
},
|
|
122
|
+
accept(cb) {
|
|
123
|
+
const hot = getHot();
|
|
124
|
+
if (!hot) return;
|
|
125
|
+
hot.accept(() => {
|
|
126
|
+
cb?.();
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
dispose(snapshot) {
|
|
130
|
+
const hot = getHot();
|
|
131
|
+
if (!hot) return;
|
|
132
|
+
hot.dispose(() => {
|
|
133
|
+
runtime.snapshotState(file, snapshot());
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
applyWithState(snapshot, restore) {
|
|
137
|
+
const hot = getHot();
|
|
138
|
+
if (!hot) return;
|
|
139
|
+
hot.dispose(() => {
|
|
140
|
+
runtime.snapshotState(file, snapshot());
|
|
141
|
+
});
|
|
142
|
+
hot.accept(() => {
|
|
143
|
+
const state = runtime.restoreState(file);
|
|
144
|
+
restore(state ?? {});
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
invalidate() {
|
|
148
|
+
const hot = getHot();
|
|
149
|
+
if (!hot) return;
|
|
150
|
+
hot.invalidate();
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/client.ts
|
|
156
|
+
function createHmrClient(options) {
|
|
157
|
+
const { url, runtime, onEvent } = options;
|
|
158
|
+
const createSocket = options.createSocket ?? ((u) => new WebSocket(u));
|
|
159
|
+
const maxAttempts = options.reconnect?.maxAttempts ?? -1;
|
|
160
|
+
const baseDelayMs = options.reconnect?.baseDelayMs ?? 500;
|
|
161
|
+
let socket = null;
|
|
162
|
+
let isOpen = false;
|
|
163
|
+
let closedByUser = false;
|
|
164
|
+
let attempt = 0;
|
|
165
|
+
let reconnectTimer = null;
|
|
166
|
+
function emit(event) {
|
|
167
|
+
onEvent?.(event);
|
|
168
|
+
}
|
|
169
|
+
function open() {
|
|
170
|
+
let sock;
|
|
171
|
+
try {
|
|
172
|
+
sock = createSocket(url);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
emit({ type: "error", message: `WebSocket \u521B\u5EFA\u5931\u8D25\uFF1A${String(err)}` });
|
|
175
|
+
scheduleReconnect();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
socket = sock;
|
|
179
|
+
sock.onopen = () => {
|
|
180
|
+
attempt = 0;
|
|
181
|
+
isOpen = true;
|
|
182
|
+
emit({ type: "connected" });
|
|
183
|
+
};
|
|
184
|
+
sock.onmessage = (ev) => {
|
|
185
|
+
try {
|
|
186
|
+
const data = JSON.parse(String(ev.data));
|
|
187
|
+
if (Array.isArray(data)) runtime.applyBatch(data);
|
|
188
|
+
else runtime.apply(data);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
emit({ type: "error", message: `payload \u89E3\u6790\u5931\u8D25\uFF1A${String(err)}` });
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
sock.onclose = (ev) => {
|
|
194
|
+
isOpen = false;
|
|
195
|
+
emit({ type: "disconnected", reason: ev.reason });
|
|
196
|
+
if (!closedByUser) scheduleReconnect();
|
|
197
|
+
};
|
|
198
|
+
sock.onerror = () => {
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function scheduleReconnect() {
|
|
202
|
+
if (closedByUser) return;
|
|
203
|
+
if (maxAttempts >= 0 && attempt >= maxAttempts) {
|
|
204
|
+
emit({ type: "error", message: `\u91CD\u8FDE\u8D85\u8FC7\u6700\u5927\u6B21\u6570\uFF08${maxAttempts}\uFF09` });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const delayMs = baseDelayMs * 2 ** attempt;
|
|
208
|
+
attempt += 1;
|
|
209
|
+
emit({ type: "reconnecting", attempt, delayMs });
|
|
210
|
+
reconnectTimer = setTimeout(() => {
|
|
211
|
+
open();
|
|
212
|
+
}, delayMs);
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
connect() {
|
|
216
|
+
closedByUser = false;
|
|
217
|
+
isOpen = false;
|
|
218
|
+
open();
|
|
219
|
+
},
|
|
220
|
+
disconnect() {
|
|
221
|
+
closedByUser = true;
|
|
222
|
+
if (reconnectTimer) {
|
|
223
|
+
clearTimeout(reconnectTimer);
|
|
224
|
+
reconnectTimer = null;
|
|
225
|
+
}
|
|
226
|
+
socket?.close();
|
|
227
|
+
socket = null;
|
|
228
|
+
isOpen = false;
|
|
229
|
+
},
|
|
230
|
+
get connected() {
|
|
231
|
+
return isOpen;
|
|
232
|
+
},
|
|
233
|
+
get attempt() {
|
|
234
|
+
return attempt;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/safe-reload.ts
|
|
240
|
+
function createSafeReload(options = {}) {
|
|
241
|
+
const storage = options.storage !== void 0 ? options.storage : typeof sessionStorage !== "undefined" ? sessionStorage : null;
|
|
242
|
+
const storageKey = options.storageKey ?? "__proteus_hmr_state__";
|
|
243
|
+
const reloadPage = options.reloadPage ?? (() => location.reload());
|
|
244
|
+
const collect = options.collect ?? (() => ({ url: location.pathname + location.search }));
|
|
245
|
+
return {
|
|
246
|
+
saveState() {
|
|
247
|
+
const state = collect();
|
|
248
|
+
if (storage) storage.set(storageKey, JSON.stringify(state));
|
|
249
|
+
return state;
|
|
250
|
+
},
|
|
251
|
+
restoreState(state) {
|
|
252
|
+
if (state) return state;
|
|
253
|
+
if (!storage) return void 0;
|
|
254
|
+
const raw = storage.get(storageKey);
|
|
255
|
+
if (!raw) return void 0;
|
|
256
|
+
try {
|
|
257
|
+
return JSON.parse(raw);
|
|
258
|
+
} catch {
|
|
259
|
+
return void 0;
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
reload() {
|
|
263
|
+
this.saveState();
|
|
264
|
+
reloadPage();
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
export {
|
|
269
|
+
createHmrClient,
|
|
270
|
+
createHmrRuntime,
|
|
271
|
+
createSafeReload,
|
|
272
|
+
createVueHotAdapter
|
|
273
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { HmrEvent, HmrPayload, SafeReload } from './types';
|
|
2
|
+
export interface HmrRuntimeOptions {
|
|
3
|
+
/** 模块应用器:vue/js 新代码 → 是否应用成功(false/抛错 → 安全 reload 兜底) */
|
|
4
|
+
applyModule: (file: string, code: string) => boolean;
|
|
5
|
+
/** 安全 reload(HMR002:native-binding 变更 / action=reload / 应用失败兜底) */
|
|
6
|
+
reload: SafeReload['reload'];
|
|
7
|
+
/** 可观测回调(原则 #3:HMR 过程可见) */
|
|
8
|
+
onEvent?: (event: HmrEvent) => void;
|
|
9
|
+
/** HMR001 规则开关(副作用未清理检测,默认开) */
|
|
10
|
+
checkSideEffects?: boolean;
|
|
11
|
+
/** HMR003 规则开关(状态丢失检测,默认开) */
|
|
12
|
+
checkStateLoss?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface HmrRuntime {
|
|
15
|
+
/** 应用一个 payload(幂等;按 id 防乱序/重复) */
|
|
16
|
+
apply(payload: HmrPayload): void;
|
|
17
|
+
/** 批量应用(★性能优化):同文件合并只保留最终状态 + 按 id 排序后逐个 apply */
|
|
18
|
+
applyBatch(payloads: HmrPayload[]): HmrBatchResult;
|
|
19
|
+
/** 模块替换前状态快照(dispose 回调中调用);前一个实例未恢复时触发 HMR001 */
|
|
20
|
+
snapshotState(file: string, state: Record<string, unknown>): void;
|
|
21
|
+
/** 新模块挂载时恢复状态(返回快照;无快照返回 undefined) */
|
|
22
|
+
restoreState(file: string): Record<string, unknown> | undefined;
|
|
23
|
+
/** 已应用模块文件列表 */
|
|
24
|
+
appliedFiles(): string[];
|
|
25
|
+
/** 重置(断开连接/会话结束时) */
|
|
26
|
+
reset(): void;
|
|
27
|
+
/** 最近应用 payload id */
|
|
28
|
+
readonly lastAppliedId: number;
|
|
29
|
+
}
|
|
30
|
+
/** 批量应用结果(★性能可观测) */
|
|
31
|
+
export interface HmrBatchResult {
|
|
32
|
+
/** 输入 payload 总数 */
|
|
33
|
+
total: number;
|
|
34
|
+
/** 同文件合并丢弃数(只保留最终状态) */
|
|
35
|
+
merged: number;
|
|
36
|
+
/** 实际应用数 */
|
|
37
|
+
applied: number;
|
|
38
|
+
}
|
|
39
|
+
export declare function createHmrRuntime(options: HmrRuntimeOptions): HmrRuntime;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SafeReload } from './types';
|
|
2
|
+
export interface SafeReloadOptions {
|
|
3
|
+
/** 状态收集器(缺省:location 快照;M3 原生侧:Router G-32 栈序列化 + 页面状态) */
|
|
4
|
+
collect?: () => Record<string, unknown>;
|
|
5
|
+
/** 存储(缺省 sessionStorage;可注入 mock 单测) */
|
|
6
|
+
storage?: {
|
|
7
|
+
get(key: string): string | null;
|
|
8
|
+
set(key: string, value: string): void;
|
|
9
|
+
} | null;
|
|
10
|
+
/** 页面刷新(缺省 location.reload;可注入) */
|
|
11
|
+
reloadPage?: () => void;
|
|
12
|
+
/** 存储键(缺省 __proteus_hmr_state__) */
|
|
13
|
+
storageKey?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function createSafeReload(options?: SafeReloadOptions): SafeReload;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { StylePlatform } from '@proteus-vue/runtime/style-safety';
|
|
2
|
+
export type StyleGateName = 'whitelist' | 'validator' | 'narrowing';
|
|
3
|
+
/** 单条闸门判定 */
|
|
4
|
+
export interface StyleGateStep {
|
|
5
|
+
gate: StyleGateName;
|
|
6
|
+
decision: 'pass' | 'reject' | 'narrow';
|
|
7
|
+
detail: string;
|
|
8
|
+
}
|
|
9
|
+
/** 样式闸门记录(可视化单元) */
|
|
10
|
+
export interface StyleGateRecord {
|
|
11
|
+
prop: string;
|
|
12
|
+
value: unknown;
|
|
13
|
+
/** 闸门链判定轨迹 */
|
|
14
|
+
gates: StyleGateStep[];
|
|
15
|
+
/** 最终决策:pass(原样)/ narrow(平台值映射)/ drop(拒绝丢弃) */
|
|
16
|
+
decision: 'pass' | 'narrow' | 'drop';
|
|
17
|
+
/** narrow 时的各端原生值映射 */
|
|
18
|
+
nativeValues?: Partial<Record<StylePlatform, unknown>>;
|
|
19
|
+
/** drop 时的拒绝原因 */
|
|
20
|
+
rejectReason?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface CollectStyleGateOptions {
|
|
23
|
+
/** 收窄平台(缺省 'web'——单平台决策链) */
|
|
24
|
+
platform?: StylePlatform;
|
|
25
|
+
/** 是否同时产出全部平台原生值映射(allPlatforms 时 nativeValues 全量五端) */
|
|
26
|
+
allPlatforms?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** 样式对象 → 闸门记录[](每 prop 一条完整决策链) */
|
|
29
|
+
export declare function collectStyleGateRecords(style: Record<string, unknown>, options?: CollectStyleGateOptions): StyleGateRecord[];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/style-gate/index.ts
|
|
2
|
+
import { ALLOWED_STYLE_PROPS, validateProp, narrowValue } from "@proteus-vue/runtime/style-safety";
|
|
3
|
+
var ALL_PLATFORMS = ["web", "skyline", "ios", "android", "harmony"];
|
|
4
|
+
function whitelistReason(prop, level) {
|
|
5
|
+
if (level === "SEMANTIC_ONLY") return `${prop} \u5FC5\u987B\u7528 p-* \u8BED\u4E49\u7EC4\u4EF6\uFF08STS003\uFF09`;
|
|
6
|
+
if (level === "FORBIDDEN") return `${prop} \u5DF2\u7981\u7528\uFF08STS004\uFF09`;
|
|
7
|
+
return `${prop} \u4E0D\u5728\u6837\u5F0F\u767D\u540D\u5355\uFF08STS001\uFF09`;
|
|
8
|
+
}
|
|
9
|
+
function collectStyleGateRecords(style, options = {}) {
|
|
10
|
+
const platform = options.platform ?? "web";
|
|
11
|
+
const records = [];
|
|
12
|
+
for (const [prop, value] of Object.entries(style)) {
|
|
13
|
+
const gates = [];
|
|
14
|
+
const level = ALLOWED_STYLE_PROPS[prop];
|
|
15
|
+
if (level === void 0 || level === "SEMANTIC_ONLY" || level === "FORBIDDEN") {
|
|
16
|
+
const reason = whitelistReason(prop, level ?? "");
|
|
17
|
+
gates.push({ gate: "whitelist", decision: "reject", detail: reason });
|
|
18
|
+
records.push({ prop, value, gates, decision: "drop", rejectReason: reason });
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
gates.push({ gate: "whitelist", decision: "pass", detail: "\u767D\u540D\u5355\u653E\u884C" });
|
|
22
|
+
const result = validateProp(prop, value, platform);
|
|
23
|
+
if (!result.valid) {
|
|
24
|
+
const reason = result.reason || `\u503C\u7C7B\u578B\u975E\u6CD5: ${String(value)}`;
|
|
25
|
+
gates.push({ gate: "validator", decision: "reject", detail: reason });
|
|
26
|
+
records.push({ prop, value, gates, decision: "drop", rejectReason: reason });
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
gates.push({ gate: "validator", decision: "pass", detail: "\u7C7B\u578B\u6821\u9A8C\u901A\u8FC7" });
|
|
30
|
+
const nativeValues = {};
|
|
31
|
+
let changedAny = false;
|
|
32
|
+
for (const p of ALL_PLATFORMS) {
|
|
33
|
+
if (!options.allPlatforms && p !== platform) continue;
|
|
34
|
+
const n = narrowValue(prop, value, p);
|
|
35
|
+
nativeValues[p] = n.valid ? n.value : value;
|
|
36
|
+
if (n.valid && n.value !== value) changedAny = true;
|
|
37
|
+
}
|
|
38
|
+
const changed = result.value !== value || changedAny;
|
|
39
|
+
if (changed) {
|
|
40
|
+
gates.push({
|
|
41
|
+
gate: "narrowing",
|
|
42
|
+
decision: "narrow",
|
|
43
|
+
detail: `\u5E73\u53F0\u6536\u7A84\uFF08${platform}\uFF09\uFF1A${JSON.stringify(result.value)}`
|
|
44
|
+
});
|
|
45
|
+
records.push({ prop, value, gates, decision: "narrow", nativeValues: options.allPlatforms ? nativeValues : { [platform]: result.value } });
|
|
46
|
+
} else {
|
|
47
|
+
gates.push({ gate: "narrowing", decision: "pass", detail: "\u65E0\u9700\u6536\u7A84\uFF08\u539F\u6837\u900F\u4F20\uFF09" });
|
|
48
|
+
records.push({ prop, value, gates, decision: "pass", nativeValues: options.allPlatforms ? nativeValues : { [platform]: result.value } });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return records;
|
|
52
|
+
}
|
|
53
|
+
export {
|
|
54
|
+
collectStyleGateRecords
|
|
55
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** HMR 可观测事件(原则 #3 编译透明:HMR 过程可见——DevTools 面板数据源) */
|
|
2
|
+
export type HmrEvent = {
|
|
3
|
+
type: 'connected';
|
|
4
|
+
} | {
|
|
5
|
+
type: 'disconnected';
|
|
6
|
+
reason?: string;
|
|
7
|
+
} | {
|
|
8
|
+
type: 'reconnecting';
|
|
9
|
+
attempt: number;
|
|
10
|
+
delayMs: number;
|
|
11
|
+
} | {
|
|
12
|
+
type: 'payload';
|
|
13
|
+
payload: HmrPayload;
|
|
14
|
+
} | {
|
|
15
|
+
type: 'apply';
|
|
16
|
+
file: string;
|
|
17
|
+
result: 'ok' | 'skipped' | 'reload';
|
|
18
|
+
} | {
|
|
19
|
+
type: 'rule';
|
|
20
|
+
rule: 'HMR001' | 'HMR002' | 'HMR003';
|
|
21
|
+
file: string;
|
|
22
|
+
message: string;
|
|
23
|
+
} | {
|
|
24
|
+
type: 'error';
|
|
25
|
+
file?: string;
|
|
26
|
+
message: string;
|
|
27
|
+
};
|
|
28
|
+
/** HMR payload:一次增量更新的传输单元 */
|
|
29
|
+
export interface HmrPayload {
|
|
30
|
+
/** 单调递增 id(客户端按序应用;乱序/重复丢弃) */
|
|
31
|
+
id: number;
|
|
32
|
+
/** 变更文件(相对工程根路径) */
|
|
33
|
+
file: string;
|
|
34
|
+
/** 变更类型 */
|
|
35
|
+
type: 'vue' | 'js' | 'css' | 'asset' | 'native-binding';
|
|
36
|
+
/** 新模块代码(vue/js 必带;css/asset 可选——资源替换由宿主处理) */
|
|
37
|
+
code?: string;
|
|
38
|
+
/** 编译时间戳 */
|
|
39
|
+
timestamp: number;
|
|
40
|
+
/** 动作:update(热替换)/ reload(整体刷新) */
|
|
41
|
+
action: 'update' | 'reload';
|
|
42
|
+
}
|
|
43
|
+
/** 传输通道(结构类型:WebSocket / SSE / 自研均可注入) */
|
|
44
|
+
export interface HmrTransport {
|
|
45
|
+
send(payload: HmrPayload): void;
|
|
46
|
+
onMessage(cb: (payload: HmrPayload) => void): void;
|
|
47
|
+
onStatus(cb: (online: boolean) => void): void;
|
|
48
|
+
close(): void;
|
|
49
|
+
}
|
|
50
|
+
/** 安全 reload 接口(HMR002:原生 binding 变更等不可热替换场景) */
|
|
51
|
+
export interface SafeReload {
|
|
52
|
+
/** 保存当前状态(路由栈 + 页面状态;M3 原生侧联动 Router G-32 栈序列化) */
|
|
53
|
+
saveState(): Record<string, unknown>;
|
|
54
|
+
/** 恢复状态(传入已保存状态;缺省读存储) */
|
|
55
|
+
restoreState(state?: Record<string, unknown>): Record<string, unknown> | undefined;
|
|
56
|
+
/** 安全 reload:保存 → reload(恢复由新会话完成) */
|
|
57
|
+
reload(): void;
|
|
58
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { HmrRuntime } from './runtime';
|
|
2
|
+
/** import.meta.hot 的形状(Vite 注入;结构类型,可注入 mock 单测) */
|
|
3
|
+
export interface VueHotApiLike {
|
|
4
|
+
accept(deps?: string[] | ((mod: unknown) => void), cb?: (mod: unknown) => void): void;
|
|
5
|
+
dispose(cb: (data: Record<string, unknown>) => void): void;
|
|
6
|
+
invalidate(): void;
|
|
7
|
+
}
|
|
8
|
+
export interface VueHotAdapterOptions {
|
|
9
|
+
/** HMR Runtime(snapshot/restore 状态保留) */
|
|
10
|
+
runtime: HmrRuntime;
|
|
11
|
+
/** 当前模块文件路径(相对工程根) */
|
|
12
|
+
file: string;
|
|
13
|
+
/** import.meta.hot 提供者(Vite 中传 import.meta.hot;缺省 no-op——非 dev 环境安全降级) */
|
|
14
|
+
getHot?: () => VueHotApiLike | undefined;
|
|
15
|
+
}
|
|
16
|
+
export interface VueHotAdapter {
|
|
17
|
+
/** 是否存在 hot 环境(Vite dev) */
|
|
18
|
+
readonly enabled: boolean;
|
|
19
|
+
/** accept:注册组件更新回调(新模块应用后触发) */
|
|
20
|
+
accept(cb?: () => void): void;
|
|
21
|
+
/** dispose:组件替换前状态快照(Flutter Hot Reload 体验) */
|
|
22
|
+
dispose(snapshot: () => Record<string, unknown>): void;
|
|
23
|
+
/** 组合:dispose 快照 + accept 恢复(状态保留一键式) */
|
|
24
|
+
applyWithState(snapshot: () => Record<string, unknown>, restore: (state: Record<string, unknown>) => void): void;
|
|
25
|
+
/** invalidate:声明不可热替换 → 触发整页刷新 */
|
|
26
|
+
invalidate(): void;
|
|
27
|
+
}
|
|
28
|
+
export declare function createVueHotAdapter(options: VueHotAdapterOptions): VueHotAdapter;
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@proteus-vue/hmr",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Proteus HMR 运行时(devtools-plus G-34 M1):HMR payload 协议 + 状态保留 + Vue import.meta.hot 适配 + WebSocket 客户端 + 安全 reload——开发期工具链,不随业务产物发布",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./dev-server": {
|
|
16
|
+
"types": "./dist/dev-server/index.d.ts",
|
|
17
|
+
"import": "./dist/dev-server/index.js",
|
|
18
|
+
"default": "./dist/dev-server/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./cdp": {
|
|
21
|
+
"types": "./dist/cdp/index.d.ts",
|
|
22
|
+
"import": "./dist/cdp/index.js",
|
|
23
|
+
"default": "./dist/cdp/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./style-gate": {
|
|
26
|
+
"types": "./dist/style-gate/index.d.ts",
|
|
27
|
+
"import": "./dist/style-gate/index.js",
|
|
28
|
+
"default": "./dist/style-gate/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -p tsconfig.build.json --emitDeclarationOnly && esbuild src/index.ts --bundle --format=esm --platform=node --outfile=dist/index.js && esbuild src/dev-server/index.ts --bundle --format=esm --platform=node --outfile=dist/dev-server/index.js --external:ws && esbuild src/cdp/index.ts --bundle --format=esm --platform=node --outfile=dist/cdp/index.js --external:@proteus-vue/runtime --external:ws && esbuild src/style-gate/index.ts --bundle --format=esm --platform=node --outfile=dist/style-gate/index.js --external:@proteus-vue/runtime"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@proteus-vue/runtime": "0.2.0-beta.0",
|
|
41
|
+
"ws": "^8.18.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^20.0.0"
|
|
45
|
+
}
|
|
46
|
+
}
|