electron-ohos-hotreload 1.0.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/bin/eohr.js +134 -0
- package/docs/design-manual.md +445 -0
- package/docs/quick-start.md +54 -0
- package/docs/user-manual.md +766 -0
- package/package.json +40 -0
- package/src/commands/clean.js +97 -0
- package/src/commands/init.js +92 -0
- package/src/commands/log.js +35 -0
- package/src/commands/push.js +104 -0
- package/src/commands/setup.js +197 -0
- package/src/commands/status.js +57 -0
- package/src/commands/strategy.js +63 -0
- package/src/commands/watch.js +302 -0
- package/src/core/Bundler.js +86 -0
- package/src/core/Config.js +192 -0
- package/src/core/Dashboard.js +127 -0
- package/src/core/FileWatcher.js +201 -0
- package/src/core/HdcBridge.js +175 -0
- package/src/core/StatusPoller.js +72 -0
- package/src/utils.js +74 -0
- package/templates/hot-reload-agent.js +186 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* eohr watch — 启动热加载循环
|
|
4
|
+
*
|
|
5
|
+
* 默认手动模式: 文件监听只记录变更,不自动推送。
|
|
6
|
+
* 开发者改完代码后按配置的组合键推送+重载。
|
|
7
|
+
* 触发键在 .eohr.config.json 的 triggerKey 中配置,默认 "ctrl+shift+x"。
|
|
8
|
+
*
|
|
9
|
+
* --auto 模式: 文件保存即自动推送。
|
|
10
|
+
*
|
|
11
|
+
* 支持的 triggerKey 格式:
|
|
12
|
+
* "ctrl+shift+x" Ctrl+Shift+X (默认)
|
|
13
|
+
* "ctrl+r" Ctrl+R
|
|
14
|
+
* "ctrl+s" Ctrl+S (注意: 终端可能拦截)
|
|
15
|
+
* "f5" F5
|
|
16
|
+
* "space" 空格键
|
|
17
|
+
* "enter" 回车键
|
|
18
|
+
*
|
|
19
|
+
* 组合键用 + 连接,修饰键: ctrl, alt, shift
|
|
20
|
+
* 主键: a-z, 0-9, f1-f12, space, enter
|
|
21
|
+
*/
|
|
22
|
+
var fs = require("fs");
|
|
23
|
+
var path = require("path");
|
|
24
|
+
var os = require("os");
|
|
25
|
+
var Config = require("../core/Config");
|
|
26
|
+
var HdcBridge = require("../core/HdcBridge");
|
|
27
|
+
var Bundler = require("../core/Bundler");
|
|
28
|
+
var FileWatcher = require("../core/FileWatcher");
|
|
29
|
+
var StatusPoller = require("../core/StatusPoller");
|
|
30
|
+
var Dashboard = require("../core/Dashboard");
|
|
31
|
+
var utils = require("../utils");
|
|
32
|
+
var c = utils.c;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 解析 triggerKey 配置,返回 { match: fn, display: string }
|
|
36
|
+
*
|
|
37
|
+
* raw 模式下终端按键编码:
|
|
38
|
+
* Ctrl+A~Z → \x01 ~ \x1a
|
|
39
|
+
* Ctrl+Shift+X → 终端不直接支持,多数终端会发 Ctrl+X (\x18)
|
|
40
|
+
* 所以 ctrl+shift+x 实际匹配 ctrl+x (\x18),显示为 Ctrl+Shift+X
|
|
41
|
+
* Alt+X → \x1b + x
|
|
42
|
+
* F5 → \x1b[15~
|
|
43
|
+
* Space → \x20
|
|
44
|
+
* Enter → \r
|
|
45
|
+
*/
|
|
46
|
+
function parseTriggerKey(triggerKey) {
|
|
47
|
+
triggerKey = (triggerKey || "ctrl+shift+x").toLowerCase().trim();
|
|
48
|
+
|
|
49
|
+
var parts = triggerKey.split("+");
|
|
50
|
+
var hasCtrl = parts.indexOf("ctrl") >= 0;
|
|
51
|
+
var hasAlt = parts.indexOf("alt") >= 0;
|
|
52
|
+
var hasShift = parts.indexOf("shift") >= 0;
|
|
53
|
+
var mainKey = parts[parts.length - 1]; // 最后一段是主键
|
|
54
|
+
|
|
55
|
+
// 构建显示名称
|
|
56
|
+
var displayParts = [];
|
|
57
|
+
if (hasCtrl) displayParts.push("Ctrl");
|
|
58
|
+
if (hasAlt) displayParts.push("Alt");
|
|
59
|
+
if (hasShift) displayParts.push("Shift");
|
|
60
|
+
|
|
61
|
+
var keyDisplayMap = {
|
|
62
|
+
"space": "Space", "enter": "Enter", "tab": "Tab", "esc": "Esc",
|
|
63
|
+
"f1": "F1", "f2": "F2", "f3": "F3", "f4": "F4", "f5": "F5",
|
|
64
|
+
"f6": "F6", "f7": "F7", "f8": "F8", "f9": "F9", "f10": "F10",
|
|
65
|
+
"f11": "F11", "f12": "F12",
|
|
66
|
+
};
|
|
67
|
+
displayParts.push(keyDisplayMap[mainKey] || mainKey.toUpperCase());
|
|
68
|
+
var display = displayParts.join("+");
|
|
69
|
+
|
|
70
|
+
// 构建匹配函数
|
|
71
|
+
var matcher;
|
|
72
|
+
|
|
73
|
+
if (hasCtrl) {
|
|
74
|
+
// Ctrl+字母: raw 模式下 \x01 (Ctrl+A) ~ \x1a (Ctrl+Z)
|
|
75
|
+
if (mainKey.length === 1 && mainKey >= "a" && mainKey <= "z") {
|
|
76
|
+
var ctrlCode = mainKey.charCodeAt(0) - 96; // a=1, b=2, ... z=26
|
|
77
|
+
var ctrlChar = String.fromCharCode(ctrlCode);
|
|
78
|
+
|
|
79
|
+
if (hasAlt) {
|
|
80
|
+
// Ctrl+Alt+X: \x1b + ctrlChar
|
|
81
|
+
matcher = function (data) { return data === "\x1b" + ctrlChar; };
|
|
82
|
+
} else {
|
|
83
|
+
// Ctrl+X or Ctrl+Shift+X (终端不区分 shift,都发 ctrl+x)
|
|
84
|
+
matcher = function (data) { return data === ctrlChar; };
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
// Ctrl + 非字母键,终端一般不支持
|
|
88
|
+
matcher = function () { return false; };
|
|
89
|
+
}
|
|
90
|
+
} else if (hasAlt) {
|
|
91
|
+
// Alt+X: \x1b + x
|
|
92
|
+
if (mainKey.length === 1) {
|
|
93
|
+
matcher = function (data) { return data === "\x1b" + mainKey; };
|
|
94
|
+
} else {
|
|
95
|
+
matcher = function () { return false; };
|
|
96
|
+
}
|
|
97
|
+
} else if (mainKey === "space") {
|
|
98
|
+
matcher = function (data) { return data === " " || data === "\x20"; };
|
|
99
|
+
} else if (mainKey === "enter") {
|
|
100
|
+
matcher = function (data) { return data === "\r" || data === "\n"; };
|
|
101
|
+
} else if (mainKey === "tab") {
|
|
102
|
+
matcher = function (data) { return data === "\t"; };
|
|
103
|
+
} else if (mainKey === "esc") {
|
|
104
|
+
matcher = function (data) { return data === "\x1b" && data.length === 1; };
|
|
105
|
+
} else if (mainKey.match(/^f\d{1,2}$/)) {
|
|
106
|
+
// F1~F12 转义序列
|
|
107
|
+
var fSeqs = {
|
|
108
|
+
"f1": ["\x1bOP", "\x1b[11~"],
|
|
109
|
+
"f2": ["\x1bOQ", "\x1b[12~"],
|
|
110
|
+
"f3": ["\x1bOR", "\x1b[13~"],
|
|
111
|
+
"f4": ["\x1bOS", "\x1b[14~"],
|
|
112
|
+
"f5": ["\x1b[15~", "\x1b[[E"],
|
|
113
|
+
"f6": ["\x1b[17~"],
|
|
114
|
+
"f7": ["\x1b[18~"],
|
|
115
|
+
"f8": ["\x1b[19~"],
|
|
116
|
+
"f9": ["\x1b[20~"],
|
|
117
|
+
"f10": ["\x1b[21~"],
|
|
118
|
+
"f11": ["\x1b[23~"],
|
|
119
|
+
"f12": ["\x1b[24~"],
|
|
120
|
+
};
|
|
121
|
+
var seqs = fSeqs[mainKey] || [];
|
|
122
|
+
matcher = function (data) {
|
|
123
|
+
for (var i = 0; i < seqs.length; i++) {
|
|
124
|
+
if (data.indexOf(seqs[i]) >= 0) return true;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
};
|
|
128
|
+
} else if (mainKey.length === 1) {
|
|
129
|
+
// 单字符
|
|
130
|
+
matcher = function (data) { return data.trim().toLowerCase() === mainKey; };
|
|
131
|
+
} else {
|
|
132
|
+
matcher = function () { return false; };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { match: matcher, display: display };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function run(opts) {
|
|
139
|
+
opts = opts || {};
|
|
140
|
+
var config = Config.load(opts);
|
|
141
|
+
var autoMode = !!opts.auto;
|
|
142
|
+
var dash = new Dashboard();
|
|
143
|
+
var hdc = new HdcBridge(config.bundleName);
|
|
144
|
+
var bundler = new Bundler(config);
|
|
145
|
+
var watcher = new FileWatcher(config);
|
|
146
|
+
var poller = new StatusPoller(hdc, config);
|
|
147
|
+
|
|
148
|
+
// 解析触发键
|
|
149
|
+
var triggerKey = opts.triggerKey || config.triggerKey || "ctrl+shift+x";
|
|
150
|
+
var parsed = parseTriggerKey(triggerKey);
|
|
151
|
+
|
|
152
|
+
var pendingChanges = [];
|
|
153
|
+
|
|
154
|
+
dash.banner(config);
|
|
155
|
+
dash.info("hdc: " + hdc.hdcPath);
|
|
156
|
+
dash.info(autoMode ? "模式: auto (保存即推送)" : "模式: manual (按 " + parsed.display + " 推送)");
|
|
157
|
+
|
|
158
|
+
// 1. 检查设备
|
|
159
|
+
hdc.isDeviceConnected().then(function (connected) {
|
|
160
|
+
if (!connected) {
|
|
161
|
+
dash.error("设备未连接,请检查 hdc list targets");
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return hdc.mkdir(config.physicalOverrideDir);
|
|
166
|
+
}).then(function () {
|
|
167
|
+
var cfgJson = JSON.stringify({
|
|
168
|
+
strategy: config.strategy,
|
|
169
|
+
softTimeout: config.softTimeout,
|
|
170
|
+
});
|
|
171
|
+
var cfgTmp = path.join(os.tmpdir(), "eohr-hot-config.json");
|
|
172
|
+
fs.writeFileSync(cfgTmp, cfgJson);
|
|
173
|
+
return hdc.pushOverride(cfgTmp, "hot-config.json", config);
|
|
174
|
+
}).then(function () {
|
|
175
|
+
dash.info("首次推送...");
|
|
176
|
+
return doReloadCycle();
|
|
177
|
+
}).then(function () {
|
|
178
|
+
// 文件监听
|
|
179
|
+
watcher.onChange(function (file) {
|
|
180
|
+
if (autoMode) {
|
|
181
|
+
dash.fileChanged(file);
|
|
182
|
+
doReloadCycle().catch(function (err) {
|
|
183
|
+
dash.error("reload 失败: " + err.message);
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
pendingChanges.push(path.basename(file));
|
|
187
|
+
dash.filePending(file, parsed.display);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
watcher.start();
|
|
191
|
+
|
|
192
|
+
// 状态轮询
|
|
193
|
+
poller.on("status", function (status) {
|
|
194
|
+
dash.statusUpdate(status);
|
|
195
|
+
});
|
|
196
|
+
poller.start();
|
|
197
|
+
|
|
198
|
+
if (autoMode) {
|
|
199
|
+
dash.watchingAuto();
|
|
200
|
+
} else {
|
|
201
|
+
dash.watchingManual(parsed.display);
|
|
202
|
+
startKeyboardInput();
|
|
203
|
+
}
|
|
204
|
+
}).catch(function (err) {
|
|
205
|
+
dash.error(err.message);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// 键盘交互: raw 模式捕获按键
|
|
210
|
+
function startKeyboardInput() {
|
|
211
|
+
process.stdin.setRawMode(true);
|
|
212
|
+
process.stdin.resume();
|
|
213
|
+
process.stdin.setEncoding("utf8");
|
|
214
|
+
|
|
215
|
+
process.stdin.on("data", function (data) {
|
|
216
|
+
// Ctrl+C
|
|
217
|
+
if (data === "\x03") {
|
|
218
|
+
console.log();
|
|
219
|
+
dash.info("停止热加载");
|
|
220
|
+
watcher.stop();
|
|
221
|
+
poller.stop();
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 触发键
|
|
226
|
+
if (parsed.match(data)) {
|
|
227
|
+
if (pendingChanges.length > 0) {
|
|
228
|
+
dash.info("推送 " + pendingChanges.length + " 个文件变更: " + pendingChanges.join(", "));
|
|
229
|
+
pendingChanges = [];
|
|
230
|
+
}
|
|
231
|
+
doReloadCycle().catch(function (err) {
|
|
232
|
+
dash.error("reload 失败: " + err.message);
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 'q' 退出 (不区分大小写)
|
|
238
|
+
if (data === "q" || data === "Q") {
|
|
239
|
+
dash.info("停止");
|
|
240
|
+
watcher.stop();
|
|
241
|
+
poller.stop();
|
|
242
|
+
process.exit(0);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Ctrl+C 兜底
|
|
249
|
+
process.on("SIGINT", function () {
|
|
250
|
+
console.log();
|
|
251
|
+
dash.info("停止热加载");
|
|
252
|
+
watcher.stop();
|
|
253
|
+
poller.stop();
|
|
254
|
+
process.exit(0);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
/** 打包 → 推送 → 重载 */
|
|
258
|
+
function doReloadCycle() {
|
|
259
|
+
var startTime = Date.now();
|
|
260
|
+
|
|
261
|
+
dash.bundling();
|
|
262
|
+
return bundler.bundle().then(function (result) {
|
|
263
|
+
if (!result.changed) {
|
|
264
|
+
console.log(c.dim + "(代码未变化)" + c.reset);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
dash.bundled(Date.now() - startTime);
|
|
269
|
+
bundler.lastHash = result.hash;
|
|
270
|
+
|
|
271
|
+
var tmpFile = path.join(os.tmpdir(), "eohr-entry.js");
|
|
272
|
+
fs.writeFileSync(tmpFile, result.code);
|
|
273
|
+
|
|
274
|
+
dash.sending();
|
|
275
|
+
return hdc.pushOverride(tmpFile, "entry.js", config).then(function () {
|
|
276
|
+
dash.sent();
|
|
277
|
+
|
|
278
|
+
if (config.strategy === "hard") {
|
|
279
|
+
// hard: 主机侧停应用 → 等 1 秒 → 重启应用
|
|
280
|
+
dash.info("重启应用...");
|
|
281
|
+
return hdc.forceStop().then(function () {
|
|
282
|
+
return new Promise(function (resolve) { setTimeout(resolve, 1000); });
|
|
283
|
+
}).then(function () {
|
|
284
|
+
return hdc.startAbility();
|
|
285
|
+
}).then(function () {
|
|
286
|
+
dash.reloadTriggered("hard (restart)");
|
|
287
|
+
poller.reset();
|
|
288
|
+
});
|
|
289
|
+
} else {
|
|
290
|
+
// soft / hard-first: touch 触发,agent 自己处理
|
|
291
|
+
var trigger = config.physicalOverrideDir + "/.reload-trigger";
|
|
292
|
+
return hdc.touch(trigger).then(function () {
|
|
293
|
+
dash.reloadTriggered(config.strategy);
|
|
294
|
+
poller.reset();
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
module.exports = run;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Bundler.js — esbuild 打包器
|
|
4
|
+
*
|
|
5
|
+
* 将开发者的入口文件 (TS/JS) + 依赖打包成单文件 entry.js
|
|
6
|
+
* 设备侧 agent 只需 require(entry.js) 即可运行开发者代码
|
|
7
|
+
*
|
|
8
|
+
* 设计:
|
|
9
|
+
* - electron 标记为 external,运行时由 Electron 提供
|
|
10
|
+
* - Node.js 内置模块 (fs, path, ...) 自动 external
|
|
11
|
+
* - 项目 node_modules 中已有的依赖打进 bundle
|
|
12
|
+
* - 项目 node_modules 中没有的依赖也标记 external(设备端运行时 require)
|
|
13
|
+
* - sourcemap 内联,便于调试
|
|
14
|
+
*/
|
|
15
|
+
var fs = require("fs");
|
|
16
|
+
var path = require("path");
|
|
17
|
+
var utils = require("../utils");
|
|
18
|
+
var stripBOM = utils.stripBOM;
|
|
19
|
+
var simpleHash = utils.simpleHash;
|
|
20
|
+
|
|
21
|
+
function Bundler(config) {
|
|
22
|
+
this.config = config;
|
|
23
|
+
this.lastHash = null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 主打包入口 */
|
|
27
|
+
Bundler.prototype.bundle = function () {
|
|
28
|
+
var self = this;
|
|
29
|
+
var projectRoot = this.config.hapRoot;
|
|
30
|
+
var entryFile = this.config.entry;
|
|
31
|
+
|
|
32
|
+
// 解析入口文件绝对路径
|
|
33
|
+
var entryAbs = path.isAbsolute(entryFile) ? entryFile : path.join(projectRoot, entryFile);
|
|
34
|
+
|
|
35
|
+
if (!fs.existsSync(entryAbs)) {
|
|
36
|
+
return Promise.reject(new Error("入口文件不存在: " + entryAbs));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// esbuild 打包
|
|
40
|
+
return self.esbuildBundle(entryAbs, projectRoot).then(function (output) {
|
|
41
|
+
var code = stripBOM(output);
|
|
42
|
+
var hash = simpleHash(code);
|
|
43
|
+
return {
|
|
44
|
+
code: code,
|
|
45
|
+
hash: hash,
|
|
46
|
+
changed: hash !== self.lastHash,
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** esbuild 打包 */
|
|
52
|
+
Bundler.prototype.esbuildBundle = function (entryAbs, resolveDir) {
|
|
53
|
+
var esbuild = require("esbuild");
|
|
54
|
+
|
|
55
|
+
// Node.js 内置模块始终 external
|
|
56
|
+
var builtins = [
|
|
57
|
+
"fs", "path", "os", "crypto", "http", "https", "net", "child_process",
|
|
58
|
+
"stream", "url", "util", "events", "buffer", "querystring", "zlib",
|
|
59
|
+
"assert", "timers", "cluster", "dgram", "dns", "module", "readline",
|
|
60
|
+
"repl", "tls", "vm", "worker_threads", "perf_hooks", "process",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
// electron 始终 external
|
|
64
|
+
var external = ["electron"].concat(builtins);
|
|
65
|
+
|
|
66
|
+
return esbuild.build({
|
|
67
|
+
entryPoints: [entryAbs],
|
|
68
|
+
bundle: true,
|
|
69
|
+
platform: "node",
|
|
70
|
+
format: "cjs",
|
|
71
|
+
target: "node18",
|
|
72
|
+
external: external,
|
|
73
|
+
sourcemap: "inline",
|
|
74
|
+
write: false,
|
|
75
|
+
logLevel: "warning",
|
|
76
|
+
minify: false,
|
|
77
|
+
absWorkingDir: resolveDir,
|
|
78
|
+
}).then(function (result) {
|
|
79
|
+
if (!result.outputFiles || result.outputFiles.length === 0) {
|
|
80
|
+
throw new Error("esbuild produced no output");
|
|
81
|
+
}
|
|
82
|
+
return result.outputFiles[0].text;
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
module.exports = Bundler;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Config.js — 配置管理
|
|
4
|
+
*
|
|
5
|
+
* 自动从工程文件探测:
|
|
6
|
+
* - AppScope/app.json5 → bundleName
|
|
7
|
+
* - build-profile.json5 → hapRoot
|
|
8
|
+
* - web_engine/.../resfile/resources/app/ → appDir
|
|
9
|
+
*
|
|
10
|
+
* 合并优先级: CLI 参数 > .eohr.config.json > 自动探测 > 默认值
|
|
11
|
+
*/
|
|
12
|
+
var fs = require("fs");
|
|
13
|
+
var path = require("path");
|
|
14
|
+
var utils = require("../utils");
|
|
15
|
+
var readJson5 = utils.readJson5;
|
|
16
|
+
|
|
17
|
+
var CONFIG_FILE = ".eohr.config.json";
|
|
18
|
+
|
|
19
|
+
var DEFAULTS = {
|
|
20
|
+
entry: "src/main.ts",
|
|
21
|
+
strategy: "hard",
|
|
22
|
+
triggerKey: "ctrl+shift+x",
|
|
23
|
+
watch: null,
|
|
24
|
+
ignore: ["node_modules/**", "dist/**", ".git/**"],
|
|
25
|
+
overrideDir: "/data/storage/el2/base/files/hot",
|
|
26
|
+
resultDir: "/data/storage/el2/base/files",
|
|
27
|
+
userId: 100,
|
|
28
|
+
softTimeout: 5000,
|
|
29
|
+
pollInterval: 2000,
|
|
30
|
+
statusPollInterval: 1000,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** 向上查找 build-profile.json5 所在目录 */
|
|
34
|
+
function findHapRoot(startDir) {
|
|
35
|
+
var dir = startDir || process.cwd();
|
|
36
|
+
for (var i = 0; i < 10; i++) {
|
|
37
|
+
if (fs.existsSync(path.join(dir, "build-profile.json5"))) return dir;
|
|
38
|
+
var parent = path.dirname(dir);
|
|
39
|
+
if (parent === dir) break;
|
|
40
|
+
dir = parent;
|
|
41
|
+
}
|
|
42
|
+
return startDir || process.cwd();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 从 AppScope/app.json5 读 bundleName */
|
|
46
|
+
function detectBundleName(hapRoot) {
|
|
47
|
+
var appJson5 = path.join(hapRoot, "AppScope", "app.json5");
|
|
48
|
+
try {
|
|
49
|
+
var app = readJson5(appJson5);
|
|
50
|
+
return app.app && app.app.bundleName;
|
|
51
|
+
} catch (e) { return null; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 探测 app 目录 (resfile/resources/app/) */
|
|
55
|
+
function detectAppDir(hapRoot) {
|
|
56
|
+
var candidates = [
|
|
57
|
+
path.join(hapRoot, "web_engine", "src", "main", "resources", "resfile", "resources", "app"),
|
|
58
|
+
path.join(hapRoot, "electron", "src", "main", "resources", "resfile", "resources", "app"),
|
|
59
|
+
];
|
|
60
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
61
|
+
if (fs.existsSync(candidates[i])) return candidates[i];
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 探测开发者入口文件 */
|
|
67
|
+
function detectEntry(hapRoot, appDir) {
|
|
68
|
+
// 1) app 目录下的 main.ts/main.js
|
|
69
|
+
if (appDir) {
|
|
70
|
+
var candidates = ["main.ts", "main.js", "index.ts", "index.js"];
|
|
71
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
72
|
+
var p = path.join(appDir, candidates[i]);
|
|
73
|
+
if (fs.existsSync(p)) return path.relative(hapRoot, p).replace(/\\/g, "/");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// 2) 工程根目录 src/main.ts
|
|
77
|
+
var srcCandidates = ["src/main.ts", "src/main.js", "src/index.ts", "src/index.js"];
|
|
78
|
+
for (var j = 0; j < srcCandidates.length; j++) {
|
|
79
|
+
var sp = path.join(hapRoot, srcCandidates[j]);
|
|
80
|
+
if (fs.existsSync(sp)) return srcCandidates[j];
|
|
81
|
+
}
|
|
82
|
+
return DEFAULTS.entry;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 构建物理路径 */
|
|
86
|
+
function buildPhysicalPaths(config) {
|
|
87
|
+
var bn = config.bundleName;
|
|
88
|
+
var uid = config.userId;
|
|
89
|
+
config.physicalOverrideDir = "/data/app/el2/" + uid + "/base/" + bn + "/files/hot";
|
|
90
|
+
config.physicalResultDir = "/data/app/el2/" + uid + "/base/" + bn + "/files";
|
|
91
|
+
return config;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 加载配置
|
|
96
|
+
* @param {object} cliOpts - CLI 参数覆盖
|
|
97
|
+
* @returns {object} 合并后的配置
|
|
98
|
+
*/
|
|
99
|
+
function load(cliOpts) {
|
|
100
|
+
cliOpts = cliOpts || {};
|
|
101
|
+
var hapRoot = cliOpts.hapRoot || findHapRoot(cliOpts.cwd);
|
|
102
|
+
var bundleName = null;
|
|
103
|
+
var appDir = null;
|
|
104
|
+
|
|
105
|
+
// 先读 .eohr.config.json
|
|
106
|
+
var userConfig = {};
|
|
107
|
+
var configPath = path.join(hapRoot, CONFIG_FILE);
|
|
108
|
+
if (fs.existsSync(configPath)) {
|
|
109
|
+
try { userConfig = JSON.parse(fs.readFileSync(configPath, "utf8")); } catch (e) {}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 自动探测
|
|
113
|
+
bundleName = cliOpts.bundleName || userConfig.bundleName || detectBundleName(hapRoot);
|
|
114
|
+
appDir = cliOpts.appDir || userConfig.appDir || detectAppDir(hapRoot);
|
|
115
|
+
|
|
116
|
+
// 入口文件探测
|
|
117
|
+
var entry;
|
|
118
|
+
if (cliOpts.entry) {
|
|
119
|
+
entry = cliOpts.entry;
|
|
120
|
+
} else if (userConfig.entry) {
|
|
121
|
+
entry = userConfig.entry;
|
|
122
|
+
} else {
|
|
123
|
+
entry = detectEntry(hapRoot, appDir);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 合并
|
|
127
|
+
var config = Object.assign({}, DEFAULTS, userConfig, {
|
|
128
|
+
hapRoot: hapRoot,
|
|
129
|
+
bundleName: bundleName,
|
|
130
|
+
appDir: appDir,
|
|
131
|
+
entry: entry,
|
|
132
|
+
}, cliOpts);
|
|
133
|
+
|
|
134
|
+
// 相对路径转绝对
|
|
135
|
+
if (config.appDir && !path.isAbsolute(config.appDir)) {
|
|
136
|
+
config.appDir = path.join(hapRoot, config.appDir);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 自动生成 watch 列表:app 目录 + hapRoot/src
|
|
140
|
+
if (!config.watch) {
|
|
141
|
+
config.watch = [];
|
|
142
|
+
if (config.appDir) {
|
|
143
|
+
var appRel = path.relative(hapRoot, config.appDir).replace(/\\/g, "/");
|
|
144
|
+
if (appRel) {
|
|
145
|
+
config.watch.push(appRel + "/**/*.ts");
|
|
146
|
+
config.watch.push(appRel + "/**/*.js");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
config.watch.push("src/**/*.ts");
|
|
150
|
+
config.watch.push("src/**/*.js");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 物理路径
|
|
154
|
+
buildPhysicalPaths(config);
|
|
155
|
+
|
|
156
|
+
return config;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 保存配置到 .eohr.config.json */
|
|
160
|
+
function save(config) {
|
|
161
|
+
var configPath = path.join(config.hapRoot, CONFIG_FILE);
|
|
162
|
+
var out = {
|
|
163
|
+
bundleName: config.bundleName,
|
|
164
|
+
hapRoot: config.hapRoot,
|
|
165
|
+
appDir: config.appDir,
|
|
166
|
+
entry: config.entry,
|
|
167
|
+
strategy: config.strategy,
|
|
168
|
+
triggerKey: config.triggerKey,
|
|
169
|
+
watch: config.watch,
|
|
170
|
+
ignore: config.ignore,
|
|
171
|
+
overrideDir: config.overrideDir,
|
|
172
|
+
physicalOverrideDir: config.physicalOverrideDir,
|
|
173
|
+
resultDir: config.resultDir,
|
|
174
|
+
physicalResultDir: config.physicalResultDir,
|
|
175
|
+
userId: config.userId,
|
|
176
|
+
softTimeout: config.softTimeout,
|
|
177
|
+
pollInterval: config.pollInterval,
|
|
178
|
+
};
|
|
179
|
+
fs.writeFileSync(configPath, JSON.stringify(out, null, 2));
|
|
180
|
+
return configPath;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
load: load,
|
|
185
|
+
save: save,
|
|
186
|
+
findHapRoot: findHapRoot,
|
|
187
|
+
detectBundleName: detectBundleName,
|
|
188
|
+
detectAppDir: detectAppDir,
|
|
189
|
+
detectEntry: detectEntry,
|
|
190
|
+
CONFIG_FILE: CONFIG_FILE,
|
|
191
|
+
DEFAULTS: DEFAULTS,
|
|
192
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Dashboard.js — 终端实时输出面板
|
|
4
|
+
*
|
|
5
|
+
* 显示: 工程信息、文件变更、打包进度、推送进度、重载状态
|
|
6
|
+
*/
|
|
7
|
+
var path = require("path");
|
|
8
|
+
var utils = require("../utils");
|
|
9
|
+
var c = utils.c;
|
|
10
|
+
var fmtMs = utils.fmtMs;
|
|
11
|
+
|
|
12
|
+
function Dashboard() {
|
|
13
|
+
this.reloadCount = 0;
|
|
14
|
+
this.errorCount = 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Dashboard.prototype.banner = function (config) {
|
|
18
|
+
console.log();
|
|
19
|
+
console.log(c.bold + c.cyan + " Electron OHOS Hot-Reload" + c.reset + c.dim + " v1.0.0" + c.reset);
|
|
20
|
+
console.log(c.dim + " ──────────────────────────────────────" + c.reset);
|
|
21
|
+
console.log(" Bundle: " + (config.bundleName || "(unknown)"));
|
|
22
|
+
console.log(" Entry: " + (config.entry || "(none)"));
|
|
23
|
+
console.log(" Strategy: " + (config.strategy || "hard"));
|
|
24
|
+
console.log(" HAP: " + config.hapRoot);
|
|
25
|
+
console.log();
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
Dashboard.prototype.watchingAuto = function () {
|
|
29
|
+
console.log(c.cyan + " [watching]" + c.reset + " auto 模式, 保存即推送");
|
|
30
|
+
console.log(c.dim + " Press Ctrl+C to stop" + c.reset);
|
|
31
|
+
console.log();
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
Dashboard.prototype.watchingManual = function (keyName) {
|
|
35
|
+
console.log(c.cyan + " [ready]" + c.reset + " manual 模式");
|
|
36
|
+
console.log(c.dim + " ──────────────────────────────────────" + c.reset);
|
|
37
|
+
console.log(" " + c.bold + keyName + c.reset + " 推送并重载");
|
|
38
|
+
console.log(" " + c.bold + "s" + c.reset + " 查看设备状态");
|
|
39
|
+
console.log(" " + c.bold + "q" + c.reset + " / Ctrl+C 退出");
|
|
40
|
+
console.log();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
Dashboard.prototype.filePending = function (file, keyName) {
|
|
44
|
+
var name = path.relative(process.cwd(), file);
|
|
45
|
+
console.log(c.gray + " [pending] " + c.reset + name + c.dim + " (按 " + (keyName || "触发键") + " 推送)" + c.reset);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
Dashboard.prototype.fileChanged = function (file) {
|
|
49
|
+
console.log(c.gray + " [file] " + c.reset + path.relative(process.cwd(), file));
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
Dashboard.prototype.bundling = function () {
|
|
53
|
+
process.stdout.write(c.blue + " [bundle]" + c.reset + " esbuild... ");
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
Dashboard.prototype.bundled = function (ms) {
|
|
57
|
+
console.log(c.dim + "(" + fmtMs(ms) + ")" + c.reset);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
Dashboard.prototype.sending = function () {
|
|
61
|
+
process.stdout.write(c.blue + " [send]" + c.reset + " hdc file send... ");
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
Dashboard.prototype.sent = function () {
|
|
65
|
+
console.log(c.green + "ok" + c.reset);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
Dashboard.prototype.reloadTriggered = function (strategy) {
|
|
69
|
+
this.reloadCount++;
|
|
70
|
+
console.log(c.yellow + " [reload]" + c.reset + " " + strategy + " triggered, waiting for status...");
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
Dashboard.prototype.statusUpdate = function (status) {
|
|
74
|
+
var icon = c.gray + "○" + c.reset;
|
|
75
|
+
var color = c.gray;
|
|
76
|
+
|
|
77
|
+
switch (status.status) {
|
|
78
|
+
case "agent-ready":
|
|
79
|
+
icon = c.cyan + "●" + c.reset;
|
|
80
|
+
color = c.cyan;
|
|
81
|
+
break;
|
|
82
|
+
case "soft-reloading":
|
|
83
|
+
case "hard-reloading":
|
|
84
|
+
icon = c.yellow + "●" + c.reset;
|
|
85
|
+
color = c.yellow;
|
|
86
|
+
break;
|
|
87
|
+
case "soft-ok":
|
|
88
|
+
case "loaded":
|
|
89
|
+
icon = c.green + "●" + c.reset;
|
|
90
|
+
color = c.green;
|
|
91
|
+
break;
|
|
92
|
+
case "soft-fail":
|
|
93
|
+
case "init-fail":
|
|
94
|
+
icon = c.red + "●" + c.reset;
|
|
95
|
+
color = c.red;
|
|
96
|
+
this.errorCount++;
|
|
97
|
+
break;
|
|
98
|
+
case "waiting":
|
|
99
|
+
icon = c.gray + "○" + c.reset;
|
|
100
|
+
break;
|
|
101
|
+
case "hard-reloading":
|
|
102
|
+
icon = c.yellow + "●" + c.reset;
|
|
103
|
+
color = c.yellow;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
var msg = " " + icon + " " + color + status.status + c.reset;
|
|
108
|
+
if (status.error) {
|
|
109
|
+
msg += c.red + " " + status.error + c.reset;
|
|
110
|
+
}
|
|
111
|
+
console.log(msg);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
Dashboard.prototype.error = function (msg) {
|
|
115
|
+
this.errorCount++;
|
|
116
|
+
console.error(c.red + " [error]" + c.reset + " " + msg);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
Dashboard.prototype.info = function (msg) {
|
|
120
|
+
console.log(c.cyan + " [info]" + c.reset + " " + msg);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
Dashboard.prototype.stats_ = function () {
|
|
124
|
+
return "Reloads: " + this.reloadCount + " | Errors: " + this.errorCount;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
module.exports = Dashboard;
|