@graftland/hmr 0.1.0 → 0.2.0-beta.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 +150 -2
- package/README.zh-CN.md +120 -0
- package/README.zh-TW.md +120 -0
- package/dist/index.d.ts +39 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +187 -45
- package/dist/index.js.map +1 -1
- package/dist/internal/changes.js +59 -0
- package/dist/internal/changes.js.map +1 -0
- package/dist/internal/modules.js +141 -28
- package/dist/internal/modules.js.map +1 -1
- package/package.json +7 -5
- package/src/index.ts +451 -87
- package/src/internal/changes.ts +199 -0
- package/src/internal/modules.ts +335 -42
package/README.md
CHANGED
|
@@ -1,13 +1,161 @@
|
|
|
1
1
|
# @graftland/hmr
|
|
2
2
|
|
|
3
|
+
English |
|
|
4
|
+
[简体中文](https://github.com/graftland/graft/blob/main/packages/hmr/README.zh-CN.md)
|
|
5
|
+
|
|
|
6
|
+
[繁體中文](https://github.com/graftland/graft/blob/main/packages/hmr/README.zh-TW.md)
|
|
7
|
+
|
|
3
8
|
Reloads plugin modules on Node.js without restarting the process.
|
|
4
9
|
|
|
5
10
|
```sh
|
|
6
11
|
npm install @graftland/hmr effect
|
|
7
12
|
```
|
|
8
13
|
|
|
9
|
-
`Hmr.make` gives an importer for the loader
|
|
10
|
-
plugins with new code.
|
|
14
|
+
`Hmr.make` gives you an importer for the loader. It also gives you `reload` and
|
|
15
|
+
`watch`, which replace plugins with new code. It needs Node.js 22.19 or later.
|
|
16
|
+
|
|
17
|
+
## Watching files
|
|
18
|
+
|
|
19
|
+
`watch` uses native filesystem events, so reloads happen quickly. It also checks
|
|
20
|
+
the contents of loaded, versioned modules inside the watched paths once per
|
|
21
|
+
second. This catches changes that the native backend missed. For example:
|
|
22
|
+
|
|
23
|
+
- the startup window of the macOS directory watcher
|
|
24
|
+
- a save between an import and the watcher installation
|
|
25
|
+
|
|
26
|
+
How the checks work:
|
|
27
|
+
|
|
28
|
+
- A newly loaded module is compared with the bytes that its module hook read. So
|
|
29
|
+
adding a plugin through configuration does not count as a file change.
|
|
30
|
+
- Each disk observation is tied to an import attempt and to the bytes that the
|
|
31
|
+
attempt actually read.
|
|
32
|
+
- A newer committed or failed attempt makes older observations invalid. So a
|
|
33
|
+
save between a snapshot and an import cannot leave the running plugin on the
|
|
34
|
+
wrong version.
|
|
35
|
+
- An unchanged failed attempt stays acknowledged until the next change. Polling
|
|
36
|
+
does not retry it again and again.
|
|
37
|
+
- The check respects `recursive`. It stops when the consumer closes the stream.
|
|
38
|
+
- Its cost depends on the contents of those modules, not on the size of the
|
|
39
|
+
directory tree.
|
|
40
|
+
|
|
41
|
+
`watch(paths, options)` accepts `filter(absolutePath)` in addition to
|
|
42
|
+
`recursive`, `onReady`, and `debounce`. The predicate filters both native events
|
|
43
|
+
and periodic content checks. By default, every path is accepted. For example:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
hmr.watch([directory], {
|
|
47
|
+
recursive: true,
|
|
48
|
+
filter: (path) => path.endsWith(".mjs"),
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The CLI uses this filter for JavaScript and TypeScript module files outside
|
|
53
|
+
`node_modules`. YAML configuration changes belong to the loader. They never
|
|
54
|
+
trigger an HMR reload of every entry.
|
|
55
|
+
|
|
56
|
+
## Failures
|
|
57
|
+
|
|
58
|
+
Debouncing does not make file writes atomic. If a reload meets incomplete code,
|
|
59
|
+
it emits `Failed` with the original `Cause`. A later content change can start
|
|
60
|
+
recovery, even without another native event.
|
|
61
|
+
|
|
62
|
+
Watcher I/O failures and reconciliation I/O failures both fail the stream. So
|
|
63
|
+
consumers should handle both failure channels.
|
|
64
|
+
|
|
65
|
+
`onReady` reports that the watcher is installed. It is not an operating-system
|
|
66
|
+
delivery barrier.
|
|
67
|
+
|
|
68
|
+
## When a restart is needed
|
|
69
|
+
|
|
70
|
+
CommonJS and native dependencies need a process restart. HMR tracks dependency
|
|
71
|
+
facts by generation for the session. This includes dynamic imports during plugin
|
|
72
|
+
activation, after the entry import completes. Once HMR records such a
|
|
73
|
+
dependency, later reloads of that generation return `Skipped` with
|
|
74
|
+
`restart-required`.
|
|
75
|
+
|
|
76
|
+
Loading through `require` is not isolated by ESM URL versions. This includes
|
|
77
|
+
`require(esm)`. HMR is careful here. It treats a versioned module's import of
|
|
78
|
+
`node:module` (or `module`) as getting a native loading capability that it
|
|
79
|
+
cannot track. That dependency graph returns `Skipped / restart-required`, even
|
|
80
|
+
if the module only uses another utility from `node:module`. HMR records the
|
|
81
|
+
capability before use. This covers CommonJS cache hits, and it stays valid when
|
|
82
|
+
another entry gets the same capability. Use static or dynamic ESM imports for
|
|
83
|
+
dependencies that need hot reload.
|
|
84
|
+
|
|
85
|
+
Watchers report known native-loading restart requirements even without a file
|
|
86
|
+
notification, including when a plugin acquires the capability after startup. The
|
|
87
|
+
periodic status check suppresses unchanged reports. This warning does not mean
|
|
88
|
+
that a reload occurred.
|
|
89
|
+
|
|
90
|
+
Local file entry leases and their reachable dependency edges keep the requested
|
|
91
|
+
path and its resolved target, together with the importing parent and generation.
|
|
92
|
+
Reload checks this relationship before it imports, and again before it publishes
|
|
93
|
+
any replacement. The same owned graph supplies polling, including dependencies
|
|
94
|
+
from failed attempts awaiting retry; releasing the last lease removes its graph
|
|
95
|
+
from these checks. If a symlink target changed, reload returns
|
|
96
|
+
`Skipped / restart-required` with the affected request URLs. Every running
|
|
97
|
+
plugin stays unchanged. Polling checks this relationship too. This includes
|
|
98
|
+
targets with identical content and missed native events. Restart the process
|
|
99
|
+
after you retarget an entry or helper. HMR does not invalidate Node's resolution
|
|
100
|
+
caches. These checks cover local file requests in the tracked dependency graph.
|
|
101
|
+
They do not cover changes to package export maps or bare-package resolution.
|
|
102
|
+
|
|
103
|
+
## Entry leases
|
|
104
|
+
|
|
105
|
+
`importerScoped(specifier)` is an Effect that needs `Scope`. Each call owns a
|
|
106
|
+
lease on the entry. The last release removes the entry from later reloads.
|
|
107
|
+
|
|
108
|
+
`importerScoped(specifier, { observationScope })` separates the observation
|
|
109
|
+
lease from temporary importer resources. `ModuleResolver` passes this scope as
|
|
110
|
+
the importer's second argument. Keep it alive while failed entries await retry,
|
|
111
|
+
and close it when replacing the observation attempt or removing the entry.
|
|
112
|
+
Candidate resources can close immediately on failure without deleting that
|
|
113
|
+
observation. Omitting the option uses the caller's scope.
|
|
114
|
+
|
|
115
|
+
The CLI passes this importer to `ModuleResolver`. Each configuration entry owns
|
|
116
|
+
its observation scope independently of activation resources. A failed first
|
|
117
|
+
import remains observable after candidate cleanup. Retrying replaces the old
|
|
118
|
+
observation generation; removing or disabling the entry closes it.
|
|
119
|
+
|
|
120
|
+
How polling and restarts work with leases:
|
|
121
|
+
|
|
122
|
+
- Polling finds its file set from each live lease. It uses the running
|
|
123
|
+
generation and the latest uncommitted import attempt.
|
|
124
|
+
- It keeps failed imports and missing local dependencies, so it can retry them.
|
|
125
|
+
- It stops reading an entry, and the dependencies only that entry uses, when the
|
|
126
|
+
last owner closes.
|
|
127
|
+
- A successful replacement retires the old dependency graph.
|
|
128
|
+
- Only dependencies that can be reached from leased entries can require a
|
|
129
|
+
restart.
|
|
130
|
+
- The Promise-based `importer` keeps its registrations until the HMR session
|
|
131
|
+
closes.
|
|
132
|
+
|
|
133
|
+
## Replacement conflicts
|
|
134
|
+
|
|
135
|
+
Sometimes entries shared one Plugin object, but now they export different
|
|
136
|
+
objects. Then reload returns `Skipped` with reason `ambiguous-exports` and the
|
|
137
|
+
affected module URLs.
|
|
138
|
+
|
|
139
|
+
Two other cases return `Skipped` with reason `identity-conflict` and the
|
|
140
|
+
conflicting module URLs:
|
|
141
|
+
|
|
142
|
+
- Replacement identities overlap, such as `P → Q, Q → R`.
|
|
143
|
+
- A mapping does not match the existing runtime aliases.
|
|
144
|
+
|
|
145
|
+
In all of these cases, nothing is replaced. Restart the application, or restore
|
|
146
|
+
a mapping that can be represented. Validation and alias publication share the
|
|
147
|
+
core replacement transaction.
|
|
148
|
+
|
|
149
|
+
Native watcher producers observe their complete Exit after source cleanup and
|
|
150
|
+
before entering background stream merging. Mixed interruption failures are
|
|
151
|
+
logged there with the full Cause; pure interruption is silent. Ordinary backend
|
|
152
|
+
failures still propagate to the consumer without duplicate logging.
|
|
153
|
+
|
|
154
|
+
## Cancellation
|
|
155
|
+
|
|
156
|
+
When a module fails during cancellation, HMR records the original failure with
|
|
157
|
+
its full Cause. It does this before pending interruption can hide the completed
|
|
158
|
+
Exit.
|
|
11
159
|
|
|
12
160
|
## Documentation
|
|
13
161
|
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# @graftland/hmr
|
|
2
|
+
|
|
3
|
+
[English](https://github.com/graftland/graft/blob/main/packages/hmr/README.md)
|
|
4
|
+
| 简体中文 |
|
|
5
|
+
[繁體中文](https://github.com/graftland/graft/blob/main/packages/hmr/README.zh-TW.md)
|
|
6
|
+
|
|
7
|
+
在 Node.js 上热重载插件模块,无需重启进程。
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @graftland/hmr effect
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`Hmr.make` 为加载器提供一个 importer,同时提供 `reload` 和
|
|
14
|
+
`watch`,用新代码替换插件。需要 Node.js 22.19 或更高版本。
|
|
15
|
+
|
|
16
|
+
## 监视文件
|
|
17
|
+
|
|
18
|
+
`watch`
|
|
19
|
+
使用原生文件系统事件,所以重载很快。它还会每秒检查一次监视路径内已加载的、带版本的模块的内容,以捕获原生后端漏掉的变更。例如:
|
|
20
|
+
|
|
21
|
+
- macOS 目录监视器的启动窗口期
|
|
22
|
+
- import 与监视器安装之间发生的保存
|
|
23
|
+
|
|
24
|
+
检查的工作方式:
|
|
25
|
+
|
|
26
|
+
- 新加载的模块会与它的模块钩子读取的字节进行比较。所以通过配置添加插件,不会被算作文件变更。
|
|
27
|
+
- 每次磁盘观察都与一次 import 尝试以及该尝试实际读取的字节相关联。
|
|
28
|
+
- 更新的已提交或已失败的尝试会使更早的观察失效。所以在快照与 import 之间发生的保存,不会让正在运行的插件停留在错误的版本上。
|
|
29
|
+
- 未发生变化的失败尝试会保持已确认状态,直到下一次变更。轮询不会反复重试它。
|
|
30
|
+
- 检查遵循 `recursive`,并在消费者关闭流时停止。
|
|
31
|
+
- 它的开销取决于这些模块的内容,而不是目录树的大小。
|
|
32
|
+
|
|
33
|
+
首次导入失败后,观察租约仍由配置项持有,与立即关闭的候选资源 scope 分离。修正模块后,即使原生通知丢失,也能通过轮询恢复。重试替换观察代次,移除或禁用配置项则关闭观察 scope。
|
|
34
|
+
|
|
35
|
+
`watch(paths, options)` 除了 `recursive`、`onReady` 和 `debounce` 之外,还接受
|
|
36
|
+
`filter(absolutePath)`。这个谓词同时过滤原生事件和定期的内容检查。默认接受所有路径。例如:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
hmr.watch([directory], {
|
|
40
|
+
recursive: true,
|
|
41
|
+
filter: (path) => path.endsWith(".mjs"),
|
|
42
|
+
})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
CLI 对 `node_modules`
|
|
46
|
+
之外的 JavaScript 和 TypeScript 模块文件使用这个过滤器。YAML 配置的变更属于加载器处理,永远不会触发对所有条目的 HMR 重载。
|
|
47
|
+
|
|
48
|
+
## 失败
|
|
49
|
+
|
|
50
|
+
防抖不会让文件写入变成原子操作。如果重载遇到不完整的代码,它会发出
|
|
51
|
+
`Failed`,并带上原始的
|
|
52
|
+
`Cause`。之后的内容变更可以触发恢复,即使没有新的原生事件。
|
|
53
|
+
|
|
54
|
+
监视器 I/O 失败和对账 I/O 失败都会使流失败。所以消费者应该处理这两个失败通道。
|
|
55
|
+
|
|
56
|
+
`onReady` 表示监视器已安装。它不是操作系统层面的事件投递屏障。
|
|
57
|
+
|
|
58
|
+
## 何时需要重启
|
|
59
|
+
|
|
60
|
+
CommonJS 和原生依赖需要重启进程。HMR 按会话的 generation 跟踪依赖信息,其中包括入口 import 完成之后、插件激活期间的动态 import。一旦 HMR 记录了这样的依赖,该 generation 之后的重载都会返回
|
|
61
|
+
`Skipped`,原因是 `restart-required`。
|
|
62
|
+
|
|
63
|
+
通过 `require` 加载的模块不受 ESM URL 版本的隔离,包括
|
|
64
|
+
`require(esm)`。HMR 在这里很谨慎:带版本的模块只要 import 了 `node:module`(或
|
|
65
|
+
`module`),HMR 就认为它获得了一种无法跟踪的原生加载能力。该依赖图会返回
|
|
66
|
+
`Skipped / restart-required`,即使这个模块只用了 `node:module`
|
|
67
|
+
里的其他工具。HMR 在使用之前就记录这种能力,这样可以覆盖 CommonJS 缓存命中的情况,并且在另一个入口获得同样的能力时仍然有效。需要热重载的依赖请使用静态或动态的 ESM
|
|
68
|
+
import。
|
|
69
|
+
|
|
70
|
+
即使没有文件通知,监听也会报告已知的原生加载重启要求,包括启动后才获取该能力的插件。周期性状态检查不会重复报告相同状态;这条警告不表示已经执行重载。
|
|
71
|
+
|
|
72
|
+
本地文件入口及其可达依赖边会按代次保留请求路径、导入方和物理目标。入口和依赖共用检查逻辑及轮询数据;失败待重试的依赖图继续保留,最后一个租约释放后不再参与检查。重载在 import 之前检查这个关系,在发布任何替换之前再检查一次。如果符号链接的目标变了,重载会返回
|
|
73
|
+
`Skipped / restart-required`,并附上受影响的请求 URL,所有正在运行的插件保持不变。轮询也会检查这个关系,包括内容相同的目标和漏掉的原生事件。重新指定入口或 helper 的目标之后,请重启进程。HMR 不会使 Node 的解析缓存失效。这些检查覆盖跟踪依赖图中的本地文件请求,不覆盖包的 export
|
|
74
|
+
map 或裸包解析的变化。
|
|
75
|
+
|
|
76
|
+
## 入口租约
|
|
77
|
+
|
|
78
|
+
`importerScoped(specifier)` 是一个需要 `Scope`
|
|
79
|
+
的 Effect。每次调用都持有该入口的一个租约。最后一个租约释放后,该入口不再参与之后的重载。
|
|
80
|
+
|
|
81
|
+
`importerScoped(specifier, { observationScope })`
|
|
82
|
+
将观察租约与候选导入的临时资源分开。`ModuleResolver`
|
|
83
|
+
通过 importer 的第二个参数传递这个 scope。失败条目等待重试时应保留它,替换观察尝试或移除条目时关闭它;候选资源仍在失败时立即清理。省略该选项时使用调用方的 scope。
|
|
84
|
+
|
|
85
|
+
原生 watcher 在源资源清理完成后、进入后台合并边界之前观察完整 Exit。包含失败的中断会在这里记录完整 Cause,纯中断保持安静;普通后端失败继续交给消费方处理,不重复记录。
|
|
86
|
+
|
|
87
|
+
CLI 把这个 importer 传给
|
|
88
|
+
`ModuleResolver`。它的入口 scope 在配置更新、激活失败或激活等待期间保持存活,并在入口被移除时关闭。
|
|
89
|
+
|
|
90
|
+
轮询和重启如何与租约配合:
|
|
91
|
+
|
|
92
|
+
- 轮询根据每个存活的租约确定文件集合,使用正在运行的 generation 和最近一次未提交的 import 尝试。
|
|
93
|
+
- 它会保留失败的 import 和缺失的本地依赖,以便重试。
|
|
94
|
+
- 当最后一个所有者关闭时,它不再读取该入口以及仅被该入口使用的依赖。
|
|
95
|
+
- 成功的替换会让旧的依赖图退役。
|
|
96
|
+
- 只有从被租用的入口可以到达的依赖,才可能要求重启。
|
|
97
|
+
- 基于 Promise 的 `importer` 会保留它的注册,直到 HMR 会话关闭。
|
|
98
|
+
|
|
99
|
+
## 替换冲突
|
|
100
|
+
|
|
101
|
+
有时多个入口原本共享同一个 Plugin 对象,现在却导出了不同的对象。此时重载返回
|
|
102
|
+
`Skipped`,原因是 `ambiguous-exports`,并附上受影响的模块 URL。
|
|
103
|
+
|
|
104
|
+
另外两种情况会返回 `Skipped`,原因是 `identity-conflict`,并附上冲突的模块 URL:
|
|
105
|
+
|
|
106
|
+
- 替换的标识有重叠,例如 `P → Q, Q → R`。
|
|
107
|
+
- 映射与现有的运行时别名不一致。
|
|
108
|
+
|
|
109
|
+
在所有这些情况下,都不会替换任何东西。请重启应用,或恢复一个可以表示的映射。校验与别名发布共用核心的替换事务。
|
|
110
|
+
|
|
111
|
+
## 取消
|
|
112
|
+
|
|
113
|
+
模块在取消期间失败时,HMR 会连同完整的 Cause 一起记录原始失败。它在待处理的中断掩盖已完成的 Exit 之前完成这一步。
|
|
114
|
+
|
|
115
|
+
## 文档
|
|
116
|
+
|
|
117
|
+
- [热重载](https://graftland.github.io/graft/zh-cn/hot-reload/)
|
|
118
|
+
|
|
119
|
+
属于 [Graft](https://github.com/graftland/graft)。基于
|
|
120
|
+
[Apache 2.0](https://github.com/graftland/graft/blob/main/LICENSE) 许可。
|
package/README.zh-TW.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# @graftland/hmr
|
|
2
|
+
|
|
3
|
+
[English](https://github.com/graftland/graft/blob/main/packages/hmr/README.md) |
|
|
4
|
+
[简体中文](https://github.com/graftland/graft/blob/main/packages/hmr/README.zh-CN.md)
|
|
5
|
+
| 繁體中文
|
|
6
|
+
|
|
7
|
+
在 Node.js 上熱重載外掛模組,不需要重新啟動行程。
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @graftland/hmr effect
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`Hmr.make` 為載入器提供一個 importer,同時提供 `reload` 和
|
|
14
|
+
`watch`,用新程式碼取代外掛。需要 Node.js 22.19 或更新版本。
|
|
15
|
+
|
|
16
|
+
## 監看檔案
|
|
17
|
+
|
|
18
|
+
`watch`
|
|
19
|
+
使用原生檔案系統事件,所以重載很快。它還會每秒檢查一次監看路徑內已載入的、帶版本的模組的內容,以捕捉原生後端漏掉的變更。例如:
|
|
20
|
+
|
|
21
|
+
- macOS 目錄監看器的啟動空窗期
|
|
22
|
+
- import 與監看器安裝之間發生的儲存
|
|
23
|
+
|
|
24
|
+
檢查的運作方式:
|
|
25
|
+
|
|
26
|
+
- 新載入的模組會與它的模組鉤子讀取的位元組進行比較。所以透過設定新增外掛,不會被算作檔案變更。
|
|
27
|
+
- 每次磁碟觀察都與一次 import 嘗試,以及該嘗試實際讀取的位元組相關聯。
|
|
28
|
+
- 較新的已提交或已失敗的嘗試會使較舊的觀察失效。所以在快照與 import 之間發生的儲存,不會讓正在執行的外掛停留在錯誤的版本上。
|
|
29
|
+
- 未發生變化的失敗嘗試會保持已確認狀態,直到下一次變更。輪詢不會反覆重試它。
|
|
30
|
+
- 檢查遵循 `recursive`,並在消費者關閉串流時停止。
|
|
31
|
+
- 它的開銷取決於這些模組的內容,而不是目錄樹的大小。
|
|
32
|
+
|
|
33
|
+
`watch(paths, options)` 除了 `recursive`、`onReady` 和 `debounce` 之外,還接受
|
|
34
|
+
`filter(absolutePath)`。這個謂詞同時過濾原生事件和定期的內容檢查。預設接受所有路徑。例如:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
hmr.watch([directory], {
|
|
38
|
+
recursive: true,
|
|
39
|
+
filter: (path) => path.endsWith(".mjs"),
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
CLI 對 `node_modules`
|
|
44
|
+
之外的 JavaScript 和 TypeScript 模組檔案使用這個過濾器。YAML 設定的變更屬於載入器處理,永遠不會觸發對所有項目的 HMR 重載。
|
|
45
|
+
|
|
46
|
+
## 失敗
|
|
47
|
+
|
|
48
|
+
防抖動不會讓檔案寫入變成原子操作。如果重載遇到不完整的程式碼,它會發出
|
|
49
|
+
`Failed`,並帶上原始的
|
|
50
|
+
`Cause`。之後的內容變更可以觸發復原,即使沒有新的原生事件。
|
|
51
|
+
|
|
52
|
+
監看器 I/O 失敗和對帳 I/O 失敗都會使串流失敗。所以消費者應該處理這兩個失敗通道。
|
|
53
|
+
|
|
54
|
+
`onReady` 表示監看器已安裝。它不是作業系統層級的事件投遞屏障。
|
|
55
|
+
|
|
56
|
+
## 何時需要重新啟動
|
|
57
|
+
|
|
58
|
+
CommonJS 和原生相依套件需要重新啟動行程。HMR 依工作階段的 generation 追蹤相依資訊,其中包括入口 import 完成之後、外掛啟用期間的動態 import。一旦 HMR 記錄了這樣的相依套件,該 generation 之後的重載都會回傳
|
|
59
|
+
`Skipped`,原因是 `restart-required`。
|
|
60
|
+
|
|
61
|
+
透過 `require` 載入的模組不受 ESM URL 版本的隔離,包括
|
|
62
|
+
`require(esm)`。HMR 在這裡很謹慎:帶版本的模組只要 import 了 `node:module`(或
|
|
63
|
+
`module`),HMR 就認為它取得了一種無法追蹤的原生載入能力。該相依圖會回傳
|
|
64
|
+
`Skipped / restart-required`,即使這個模組只用了 `node:module`
|
|
65
|
+
裡的其他工具。HMR 在使用之前就記錄這種能力,這樣可以涵蓋 CommonJS 快取命中的情況,並且在另一個入口取得同樣的能力時仍然有效。需要熱重載的相依套件請使用靜態或動態的 ESM
|
|
66
|
+
import。
|
|
67
|
+
|
|
68
|
+
即使沒有檔案通知,監聽也會回報已知的原生載入重新啟動要求,包括啟動後才取得該能力的外掛。週期性狀態檢查不會重複回報相同狀態;這條警告不表示已經執行重載。
|
|
69
|
+
|
|
70
|
+
本機檔案入口及其可達相依邊會按代次保留請求路徑、匯入方和實體目標。入口和相依檔案共用檢查邏輯及輪詢資料;失敗待重試的相依圖繼續保留,最後一個租約釋放後不再參與檢查。重載在 import 之前檢查這個關係,在發布任何取代之前再檢查一次。如果符號連結的目標變了,重載會回傳
|
|
71
|
+
`Skipped / restart-required`,並附上受影響的請求 URL,所有正在執行的外掛保持不變。輪詢也會檢查這個關係,包括內容相同的目標和漏掉的原生事件。重新指定入口或 helper 的目標之後,請重新啟動行程。HMR 不會使 Node 的解析快取失效。這些檢查涵蓋追蹤相依圖中的本機檔案請求,不涵蓋套件的 export
|
|
72
|
+
map 或裸套件解析的變化。
|
|
73
|
+
|
|
74
|
+
## 入口租約
|
|
75
|
+
|
|
76
|
+
`importerScoped(specifier)` 是一個需要 `Scope`
|
|
77
|
+
的 Effect。每次呼叫都持有該入口的一個租約。最後一個租約釋放後,該入口不再參與之後的重載。
|
|
78
|
+
|
|
79
|
+
首次匯入失敗後,觀察租約仍由設定項目持有,與立即關閉的候選資源 scope 分離。修正模組後,即使原生通知遺失,也能透過輪詢復原。重試會取代觀察代次,移除或停用設定項目則關閉觀察 scope。
|
|
80
|
+
|
|
81
|
+
`importerScoped(specifier, { observationScope })`
|
|
82
|
+
將觀察租約與候選匯入的暫時資源分開。`ModuleResolver`
|
|
83
|
+
透過 importer 的第二個參數傳遞這個 scope。失敗項目等待重試時應保留它,取代觀察嘗試或移除項目時關閉它;候選資源仍在失敗時立即清理。省略此選項時使用呼叫端的 scope。
|
|
84
|
+
|
|
85
|
+
原生 watcher 在來源資源清理完成後、進入背景合併邊界之前觀察完整 Exit。包含失敗的中斷會在這裡記錄完整 Cause,純中斷不記錄錯誤;一般後端失敗繼續交給消費端處理,不重複記錄。
|
|
86
|
+
|
|
87
|
+
CLI 把這個 importer 傳給
|
|
88
|
+
`ModuleResolver`。它的入口 scope 在設定更新、啟用失敗或啟用等待期間保持存活,並在入口被移除時關閉。
|
|
89
|
+
|
|
90
|
+
輪詢和重新啟動如何與租約配合:
|
|
91
|
+
|
|
92
|
+
- 輪詢根據每個存活的租約決定檔案集合,使用正在執行的 generation 和最近一次未提交的 import 嘗試。
|
|
93
|
+
- 它會保留失敗的 import 和缺少的本機相依套件,以便重試。
|
|
94
|
+
- 當最後一個擁有者關閉時,它不再讀取該入口以及僅被該入口使用的相依套件。
|
|
95
|
+
- 成功的取代會讓舊的相依圖退役。
|
|
96
|
+
- 只有從被租用的入口可以到達的相依套件,才可能要求重新啟動。
|
|
97
|
+
- 以 Promise 為基礎的 `importer` 會保留它的註冊,直到 HMR 工作階段關閉。
|
|
98
|
+
|
|
99
|
+
## 取代衝突
|
|
100
|
+
|
|
101
|
+
有時多個入口原本共用同一個 Plugin 物件,現在卻匯出了不同的物件。此時重載回傳
|
|
102
|
+
`Skipped`,原因是 `ambiguous-exports`,並附上受影響的模組 URL。
|
|
103
|
+
|
|
104
|
+
另外兩種情況會回傳 `Skipped`,原因是 `identity-conflict`,並附上衝突的模組 URL:
|
|
105
|
+
|
|
106
|
+
- 取代的識別有重疊,例如 `P → Q, Q → R`。
|
|
107
|
+
- 對應與現有的執行期別名不一致。
|
|
108
|
+
|
|
109
|
+
在所有這些情況下,都不會取代任何東西。請重新啟動應用程式,或還原成一個可以表示的對應。驗證與別名發布共用核心的取代交易。
|
|
110
|
+
|
|
111
|
+
## 取消
|
|
112
|
+
|
|
113
|
+
模組在取消期間失敗時,HMR 會連同完整的 Cause 一起記錄原始失敗。它在待處理的中斷掩蓋已完成的 Exit 之前完成這一步。
|
|
114
|
+
|
|
115
|
+
## 文件
|
|
116
|
+
|
|
117
|
+
- [熱重載](https://graftland.github.io/graft/zh-tw/hot-reload/)
|
|
118
|
+
|
|
119
|
+
屬於 [Graft](https://github.com/graftland/graft)。依
|
|
120
|
+
[Apache 2.0](https://github.com/graftland/graft/blob/main/LICENSE) 授權。
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Graft } from "@graftland/graft";
|
|
2
|
-
import { Cause, Duration, Effect, FileSystem, PlatformError, Schema, Scope, Stream } from "effect";
|
|
2
|
+
import { Cause, Data, Duration, Effect, FileSystem, PlatformError, Schema, Scope, Stream } from "effect";
|
|
3
3
|
declare const HmrError_base: Schema.Class<HmrError, Schema.TaggedStruct<"HmrError", {
|
|
4
4
|
readonly stage: Schema.Literals<readonly ["import", "exports", "closed"]>;
|
|
5
5
|
readonly specifier: Schema.String;
|
|
@@ -10,33 +10,60 @@ declare const HmrError_base: Schema.Class<HmrError, Schema.TaggedStruct<"HmrErro
|
|
|
10
10
|
export declare class HmrError extends HmrError_base {
|
|
11
11
|
get message(): string;
|
|
12
12
|
}
|
|
13
|
-
export type
|
|
13
|
+
export type WatchResult = Data.TaggedEnum<{
|
|
14
|
+
Reloaded: {
|
|
15
|
+
readonly entries: ReadonlyArray<string>;
|
|
16
|
+
};
|
|
17
|
+
Skipped: {
|
|
18
|
+
readonly modules: ReadonlyArray<string>;
|
|
19
|
+
readonly reason: "restart-required" | "ambiguous-exports" | "identity-conflict";
|
|
20
|
+
};
|
|
21
|
+
Failed: {
|
|
22
|
+
readonly cause: Cause.Cause<HmrError>;
|
|
23
|
+
};
|
|
24
|
+
}>;
|
|
25
|
+
export declare const WatchResult: Data.TaggedEnum.Constructor<{
|
|
26
|
+
readonly _tag: "Failed";
|
|
27
|
+
readonly cause: Cause.Cause<HmrError>;
|
|
28
|
+
} | {
|
|
14
29
|
readonly _tag: "Reloaded";
|
|
15
30
|
readonly entries: ReadonlyArray<string>;
|
|
16
31
|
} | {
|
|
17
32
|
readonly _tag: "Skipped";
|
|
18
33
|
readonly modules: ReadonlyArray<string>;
|
|
19
|
-
readonly reason: "restart-required";
|
|
20
|
-
}
|
|
21
|
-
export type
|
|
34
|
+
readonly reason: "restart-required" | "ambiguous-exports" | "identity-conflict";
|
|
35
|
+
}>;
|
|
36
|
+
export type ReloadResult = Exclude<WatchResult, {
|
|
22
37
|
readonly _tag: "Failed";
|
|
23
|
-
|
|
24
|
-
};
|
|
38
|
+
}>;
|
|
25
39
|
export interface Options {
|
|
26
40
|
/** Application module URL, usually import.meta.url; used for bare and relative resolution. */
|
|
27
41
|
readonly base: string;
|
|
28
42
|
/** Files to version. Defaults to file URLs outside node_modules. Entries are always included. */
|
|
29
43
|
readonly include?: (url: string) => boolean;
|
|
30
44
|
}
|
|
45
|
+
export interface WatchOptions extends FileSystem.WatchOptions {
|
|
46
|
+
readonly debounce?: Duration.Input;
|
|
47
|
+
/** Receives an absolute file path; applies to native events and reconciliation. */
|
|
48
|
+
readonly filter?: (path: string) => boolean;
|
|
49
|
+
}
|
|
50
|
+
/** Separates retry observations from an importer's temporary resource scope. */
|
|
51
|
+
export interface ImportOptions {
|
|
52
|
+
readonly observationScope: Scope.Scope;
|
|
53
|
+
}
|
|
31
54
|
export interface Hmr {
|
|
32
|
-
/**
|
|
55
|
+
/** Keeps imported entries registered until this HMR session closes. */
|
|
33
56
|
readonly importer: (specifier: string) => Promise<unknown>;
|
|
57
|
+
/** Registers an observation lease in observationScope, or the caller's scope when omitted. */
|
|
58
|
+
readonly importerScoped: (specifier: string, options?: ImportOptions) => Effect.Effect<unknown, HmrError, Scope.Scope>;
|
|
34
59
|
/** Imports every entry before replacing anything. Activation failures belong to Graft. */
|
|
35
60
|
readonly reload: Effect.Effect<ReloadResult, HmrError>;
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Consuming this stream installs watchers and checks loaded module contents once
|
|
63
|
+
* per second to recover missed events. Known native loading limitations emit
|
|
64
|
+
* Skipped even without a file event. The consumer owns the stream's lifetimes.
|
|
65
|
+
*/
|
|
66
|
+
readonly watch: (paths: ReadonlyArray<string>, options?: WatchOptions) => Stream.Stream<WatchResult, PlatformError.PlatformError, FileSystem.FileSystem>;
|
|
40
67
|
}
|
|
41
68
|
/**
|
|
42
69
|
* Installs Node module hooks until the enclosing scope closes. Module evaluation
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;WAsCmB;;qBAFN,iBAAiB;MAQhB;;YAKF,cAAc,KAAK;EAC9B;aAAqB,SAAS;;EAC9B;aACU,SAAS;aACT;;EAGV;aAAmB,OAAO,MAAM,MAAM;;;qBAG1B,aAAW,KAAA,WAAA;;WAHG,OAAA,MAAM,MAAM;;;WANR,SAAA;;;WAEX,SAAA;WAEjB;;YAOS,eAAe,QAAQ;WAAwB;;iBAE1C;;WAEP;;WAEA,WAAW;;iBAGJ,qBAAqB,WAAW;WACvC,WAAW,SAAS;;WAEpB,UAAU;;;iBAIH;WACP,kBAAkB,MAAM;;iBAGjB;;WAEP,WAAW,sBAAsB;;WAEjC,iBACR,mBACA,UAAU,kBACN,OAAO,gBAAgB,UAAU,MAAM;;WAEnC,QAAQ,OAAO,OAAO,cAAc;;;;;;WAMpC,QACR,OAAO,uBACP,UAAU,iBACN,OAAO,OACX,aACA,cAAc,eACd,WAAW;;;;;;qBAQA,OAAI,SACP,YACP,OAAO,OAAO,YAAY,MAAM,QAAQ,MAAM"}
|