@graftland/hmr 0.2.0-beta.0 → 0.2.0-beta.2
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 +62 -16
- package/README.zh-CN.md +24 -7
- package/README.zh-TW.md +24 -7
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +88 -12
- package/dist/index.js.map +1 -1
- package/dist/internal/changes.js +78 -36
- package/dist/internal/changes.js.map +1 -1
- package/dist/internal/digest.js +6 -0
- package/dist/internal/digest.js.map +1 -0
- package/dist/internal/modules.js +184 -92
- package/dist/internal/modules.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +356 -166
- package/src/internal/changes.ts +225 -123
- package/src/internal/digest.ts +5 -0
- package/src/internal/modules.ts +309 -248
package/README.md
CHANGED
|
@@ -16,9 +16,12 @@ npm install @graftland/hmr effect
|
|
|
16
16
|
|
|
17
17
|
## Watching files
|
|
18
18
|
|
|
19
|
-
`watch`
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
`watch` merges native event paths before checking observed files. An event alone
|
|
20
|
+
does not trigger a reload: content or a local resolution must actually change.
|
|
21
|
+
It polls file metadata and resolutions once per second, using stat to avoid
|
|
22
|
+
reading unchanged files, and audits contents at least every ten seconds while
|
|
23
|
+
idle. Events force a content check even when stat is unchanged. These checks
|
|
24
|
+
recover missed events, including:
|
|
22
25
|
|
|
23
26
|
- the startup window of the macOS directory watcher
|
|
24
27
|
- a save between an import and the watcher installation
|
|
@@ -35,8 +38,13 @@ How the checks work:
|
|
|
35
38
|
- An unchanged failed attempt stays acknowledged until the next change. Polling
|
|
36
39
|
does not retry it again and again.
|
|
37
40
|
- The check respects `recursive`. It stops when the consumer closes the stream.
|
|
38
|
-
-
|
|
39
|
-
|
|
41
|
+
- Recursive watching installs nonrecursive directory watches from the observed
|
|
42
|
+
graph. It does not recursively enumerate unrelated directories or
|
|
43
|
+
node_modules. Missing paths use the nearest existing ancestor; loaded dist
|
|
44
|
+
modules remain observable. The directory set is reconciled once per second.
|
|
45
|
+
- Reads run outside the session lock with bounded concurrency. Results are
|
|
46
|
+
accepted only if the observation version still matches. Import baselines store
|
|
47
|
+
SHA-256 digests of the bytes actually loaded, not full source strings.
|
|
40
48
|
|
|
41
49
|
`watch(paths, options)` accepts `filter(absolutePath)` in addition to
|
|
42
50
|
`recursive`, `onReady`, and `debounce`. The predicate filters both native events
|
|
@@ -53,6 +61,26 @@ The CLI uses this filter for JavaScript and TypeScript module files outside
|
|
|
53
61
|
`node_modules`. YAML configuration changes belong to the loader. They never
|
|
54
62
|
trigger an HMR reload of every entry.
|
|
55
63
|
|
|
64
|
+
## Incremental replacement
|
|
65
|
+
|
|
66
|
+
Public `hmr.reload` always forces a fresh import of every registered entry and
|
|
67
|
+
its tracked dependencies. It also advances the import view when there are no
|
|
68
|
+
entries, allowing a failed first import to be retried.
|
|
69
|
+
|
|
70
|
+
Automatic `watch` reloads invalidate changed module instances and their
|
|
71
|
+
transitive importers, including cycles. Entries sharing the same old Plugin
|
|
72
|
+
object are checked together. Clean dependencies and independent entries keep
|
|
73
|
+
their existing instances. All candidates pass import, export identity and
|
|
74
|
+
resolution checks before one `Graft.replace` publication. Failure leaves the
|
|
75
|
+
running plugins unchanged and retains the dirty set and failed observations for
|
|
76
|
+
the next change. Events arriving during a reload remain available for the next
|
|
77
|
+
pass.
|
|
78
|
+
|
|
79
|
+
Each module instance keeps its own resolution view. Late dynamic imports from
|
|
80
|
+
old code, including activation cleanup, do not switch to a global latest
|
|
81
|
+
version. Reused modules retain their dependency edges. Dependencies that have
|
|
82
|
+
never been imported cannot be discovered in advance.
|
|
83
|
+
|
|
56
84
|
## Failures
|
|
57
85
|
|
|
58
86
|
Debouncing does not make file writes atomic. If a reload meets incomplete code,
|
|
@@ -68,10 +96,9 @@ delivery barrier.
|
|
|
68
96
|
## When a restart is needed
|
|
69
97
|
|
|
70
98
|
CommonJS and native dependencies need a process restart. HMR tracks dependency
|
|
71
|
-
facts by
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
`restart-required`.
|
|
99
|
+
facts by module instance, including dynamic imports during plugin activation.
|
|
100
|
+
While a leased entry can reach such a dependency, reload returns
|
|
101
|
+
`Skipped / restart-required`.
|
|
75
102
|
|
|
76
103
|
Loading through `require` is not isolated by ESM URL versions. This includes
|
|
77
104
|
`require(esm)`. HMR is careful here. It treats a versioned module's import of
|
|
@@ -88,7 +115,7 @@ periodic status check suppresses unchanged reports. This warning does not mean
|
|
|
88
115
|
that a reload occurred.
|
|
89
116
|
|
|
90
117
|
Local file entry leases and their reachable dependency edges keep the requested
|
|
91
|
-
path and its resolved target, together with the importing
|
|
118
|
+
path and its resolved target, together with the importing module instance.
|
|
92
119
|
Reload checks this relationship before it imports, and again before it publishes
|
|
93
120
|
any replacement. The same owned graph supplies polling, including dependencies
|
|
94
121
|
from failed attempts awaiting retry; releasing the last lease removes its graph
|
|
@@ -104,6 +131,11 @@ They do not cover changes to package export maps or bare-package resolution.
|
|
|
104
131
|
|
|
105
132
|
`importerScoped(specifier)` is an Effect that needs `Scope`. Each call owns a
|
|
106
133
|
lease on the entry. The last release removes the entry from later reloads.
|
|
134
|
+
Reacquiring an entry in the same import view preserves its cached identity. The
|
|
135
|
+
current view retains the facts needed for those cache hits, without watching
|
|
136
|
+
unleased entries. A committed reload retires that entry cache. Instantiated
|
|
137
|
+
views retain source digests, dependency edges and native capability markers,
|
|
138
|
+
because old code can reconnect cached instances without replaying their imports.
|
|
107
139
|
|
|
108
140
|
`importerScoped(specifier, { observationScope })` separates the observation
|
|
109
141
|
lease from temporary importer resources. `ModuleResolver` passes this scope as
|
|
@@ -119,12 +151,13 @@ observation generation; removing or disabling the entry closes it.
|
|
|
119
151
|
|
|
120
152
|
How polling and restarts work with leases:
|
|
121
153
|
|
|
122
|
-
- Polling finds its file set from each live lease. It uses the running
|
|
123
|
-
|
|
154
|
+
- Polling finds its file set from each live lease. It uses the running module
|
|
155
|
+
instances and the latest uncommitted import attempt.
|
|
124
156
|
- It keeps failed imports and missing local dependencies, so it can retry them.
|
|
125
157
|
- It stops reading an entry, and the dependencies only that entry uses, when the
|
|
126
158
|
last owner closes.
|
|
127
|
-
- A successful replacement
|
|
159
|
+
- A successful replacement stops observing files no longer reachable from any
|
|
160
|
+
active or pending entry. Cached instance facts survive for old routing views.
|
|
128
161
|
- Only dependencies that can be reached from leased entries can require a
|
|
129
162
|
restart.
|
|
130
163
|
- The Promise-based `importer` keeps its registrations until the HMR session
|
|
@@ -147,9 +180,22 @@ a mapping that can be represented. Validation and alias publication share the
|
|
|
147
180
|
core replacement transaction.
|
|
148
181
|
|
|
149
182
|
Native watcher producers observe their complete Exit after source cleanup and
|
|
150
|
-
before entering background stream merging.
|
|
151
|
-
|
|
152
|
-
failures
|
|
183
|
+
before entering background stream merging. Only cancellation requested by the
|
|
184
|
+
observer with a matching cause is silent. Resource cleanup interruption and
|
|
185
|
+
mixed failures are logged with their full Cause. Ordinary backend failures still
|
|
186
|
+
propagate to the consumer without duplicate logging.
|
|
187
|
+
|
|
188
|
+
## Long development sessions
|
|
189
|
+
|
|
190
|
+
Node does not evict versioned ESM instances. Incremental reload reduces the
|
|
191
|
+
number created, but a long development session still accumulates module
|
|
192
|
+
instances. HMR reclaims unused reachability caches and uninstantiated views.
|
|
193
|
+
Instantiated routing views and their source digests, dependency edges and native
|
|
194
|
+
capability markers remain until the session closes: arbitrary old code can
|
|
195
|
+
reconnect cached modules, whose static imports Node will not replay. Observation
|
|
196
|
+
and polling still cover only active and pending leases. This does not give
|
|
197
|
+
process memory a hard upper bound. Restart the process when needed; HMR does not
|
|
198
|
+
supervise or restart a child process.
|
|
153
199
|
|
|
154
200
|
## Cancellation
|
|
155
201
|
|
package/README.zh-CN.md
CHANGED
|
@@ -16,7 +16,7 @@ npm install @graftland/hmr effect
|
|
|
16
16
|
## 监视文件
|
|
17
17
|
|
|
18
18
|
`watch`
|
|
19
|
-
|
|
19
|
+
先合并原生事件路径,再检查已观察文件。事件本身不触发重载,内容或本地解析必须实际变化。每秒轮询 stat 和解析,跳过元数据未变的文件读取;空闲时至少每十秒核查一次内容摘要,原生事件则强制检查内容。因此同尺寸、同时间戳且漏通知的保存仍会被发现。递归选项按观察图安装非递归目录监听,不遍历无关目录或 node_modules;实际加载的 dist 模块仍被观察,缺失路径使用最近的现存祖先目录。目录集合每秒更新。读取在会话锁外有限并发执行,确认结果时重新校验观察版本;导入基线保存实际读取字节的 SHA-256 摘要。 例如:
|
|
20
20
|
|
|
21
21
|
- macOS 目录监视器的启动窗口期
|
|
22
22
|
- import 与监视器安装之间发生的保存
|
|
@@ -45,6 +45,19 @@ hmr.watch([directory], {
|
|
|
45
45
|
CLI 对 `node_modules`
|
|
46
46
|
之外的 JavaScript 和 TypeScript 模块文件使用这个过滤器。YAML 配置的变更属于加载器处理,永远不会触发对所有条目的 HMR 重载。
|
|
47
47
|
|
|
48
|
+
## 增量替换
|
|
49
|
+
|
|
50
|
+
公共 `hmr.reload`
|
|
51
|
+
始终强制重新导入所有已注册入口及其受跟踪依赖;没有入口时也推进导入视图,以绕过首次失败导入的缓存。
|
|
52
|
+
|
|
53
|
+
`watch`
|
|
54
|
+
自动重载只失效发生变化的模块实例及其传递导入方,包括循环依赖。共享同一旧 Plugin 对象的入口一起检查;健康依赖与独立入口复用原实例。所有候选通过导入、导出身份和前后两次解析检查后,只调用一次
|
|
55
|
+
`Graft.replace`
|
|
56
|
+
发布。失败保持运行插件不变,保留 dirty 集合及失败观察,等待下一次变化;重载期间到达的事件留给下一轮。
|
|
57
|
+
|
|
58
|
+
模块实例保留自己的解析视图,旧代码(包括 activation 清理)晚到的 dynamic
|
|
59
|
+
import 不会切换到全局最新版。复用模块保留依赖边;从未执行过的动态导入无法提前发现。
|
|
60
|
+
|
|
48
61
|
## 失败
|
|
49
62
|
|
|
50
63
|
防抖不会让文件写入变成原子操作。如果重载遇到不完整的代码,它会发出
|
|
@@ -57,8 +70,8 @@ CLI 对 `node_modules`
|
|
|
57
70
|
|
|
58
71
|
## 何时需要重启
|
|
59
72
|
|
|
60
|
-
CommonJS 和原生依赖需要重启进程。HMR
|
|
61
|
-
`Skipped
|
|
73
|
+
CommonJS 和原生依赖需要重启进程。HMR 按模块实例跟踪依赖,包括入口导入结束后的动态导入。只要存活租约能到达这类依赖,重载就返回
|
|
74
|
+
`Skipped / restart-required`。
|
|
62
75
|
|
|
63
76
|
通过 `require` 加载的模块不受 ESM URL 版本的隔离,包括
|
|
64
77
|
`require(esm)`。HMR 在这里很谨慎:带版本的模块只要 import 了 `node:module`(或
|
|
@@ -69,27 +82,27 @@ import。
|
|
|
69
82
|
|
|
70
83
|
即使没有文件通知,监听也会报告已知的原生加载重启要求,包括启动后才获取该能力的插件。周期性状态检查不会重复报告相同状态;这条警告不表示已经执行重载。
|
|
71
84
|
|
|
72
|
-
|
|
85
|
+
本地文件入口及其可达依赖边会按模块实例保留请求路径、导入方和物理目标。入口和依赖共用检查逻辑及轮询数据;失败待重试的依赖图继续保留,最后一个租约释放后不再参与检查。重载在 import 之前检查这个关系,在发布任何替换之前再检查一次。如果符号链接的目标变了,重载会返回
|
|
73
86
|
`Skipped / restart-required`,并附上受影响的请求 URL,所有正在运行的插件保持不变。轮询也会检查这个关系,包括内容相同的目标和漏掉的原生事件。重新指定入口或 helper 的目标之后,请重启进程。HMR 不会使 Node 的解析缓存失效。这些检查覆盖跟踪依赖图中的本地文件请求,不覆盖包的 export
|
|
74
87
|
map 或裸包解析的变化。
|
|
75
88
|
|
|
76
89
|
## 入口租约
|
|
77
90
|
|
|
78
91
|
`importerScoped(specifier)` 是一个需要 `Scope`
|
|
79
|
-
的 Effect
|
|
92
|
+
的 Effect。每次调用都持有该入口的一个租约。最后一个租约释放后,该入口不再参与之后的重载。在同一导入视图中再次取得租约会保留缓存身份;当前视图保留缓存命中所需的观察信息,但不监听无租约入口。成功提交下一视图后回收入口缓存;已实例化视图的源码摘要、依赖边和原生能力标记保留到会话关闭,因为旧代码仍可重新触达缓存实例,而 Node 不会重放其静态导入。
|
|
80
93
|
|
|
81
94
|
`importerScoped(specifier, { observationScope })`
|
|
82
95
|
将观察租约与候选导入的临时资源分开。`ModuleResolver`
|
|
83
96
|
通过 importer 的第二个参数传递这个 scope。失败条目等待重试时应保留它,替换观察尝试或移除条目时关闭它;候选资源仍在失败时立即清理。省略该选项时使用调用方的 scope。
|
|
84
97
|
|
|
85
|
-
原生 watcher 在源资源清理完成后、进入后台合并边界之前观察完整 Exit
|
|
98
|
+
原生 watcher 在源资源清理完成后、进入后台合并边界之前观察完整 Exit。只有观察者主动发起且来源匹配的取消保持静默。资源清理自身产生的中断和混合失败都会记录完整 Cause;普通后端失败继续交给消费方处理,不重复记录。
|
|
86
99
|
|
|
87
100
|
CLI 把这个 importer 传给
|
|
88
101
|
`ModuleResolver`。它的入口 scope 在配置更新、激活失败或激活等待期间保持存活,并在入口被移除时关闭。
|
|
89
102
|
|
|
90
103
|
轮询和重启如何与租约配合:
|
|
91
104
|
|
|
92
|
-
-
|
|
105
|
+
- 轮询根据每个存活的租约确定文件集合,使用正在运行的模块实例 和最近一次未提交的 import 尝试。
|
|
93
106
|
- 它会保留失败的 import 和缺失的本地依赖,以便重试。
|
|
94
107
|
- 当最后一个所有者关闭时,它不再读取该入口以及仅被该入口使用的依赖。
|
|
95
108
|
- 成功的替换会让旧的依赖图退役。
|
|
@@ -108,6 +121,10 @@ CLI 把这个 importer 传给
|
|
|
108
121
|
|
|
109
122
|
在所有这些情况下,都不会替换任何东西。请重启应用,或恢复一个可以表示的映射。校验与别名发布共用核心的替换事务。
|
|
110
123
|
|
|
124
|
+
## 长时间开发会话
|
|
125
|
+
|
|
126
|
+
Node 不会淘汰带版本的 ESM 实例。增量重载减少新实例数量,但长会话仍会累积模块实例。HMR 回收不再使用的可达缓存和未实例化视图;已实例化的路由视图及其源码摘要、依赖边和原生能力标记保留到会话关闭,以支持旧代码重新触达缓存模块。观察和轮询仍仅覆盖活跃及待重试租约。因此进程内存没有硬上界,需要时请重启。HMR 不提供子进程 supervisor,也不宣称彻底消除泄漏。
|
|
127
|
+
|
|
111
128
|
## 取消
|
|
112
129
|
|
|
113
130
|
模块在取消期间失败时,HMR 会连同完整的 Cause 一起记录原始失败。它在待处理的中断掩盖已完成的 Exit 之前完成这一步。
|
package/README.zh-TW.md
CHANGED
|
@@ -16,7 +16,7 @@ npm install @graftland/hmr effect
|
|
|
16
16
|
## 監看檔案
|
|
17
17
|
|
|
18
18
|
`watch`
|
|
19
|
-
|
|
19
|
+
先合併原生事件路徑,再檢查已觀察檔案。事件本身不觸發重載,內容或本機解析必須實際變化。每秒輪詢 stat 和解析,略過中繼資料未變的檔案讀取;閒置時至少每十秒核查一次內容摘要,原生事件則強制檢查內容。因此同尺寸、同時間戳且漏通知的儲存仍會被發現。遞迴選項依觀察圖安裝非遞迴目錄監聽,不走訪無關目錄或 node_modules;實際載入的 dist 模組仍被觀察,缺少的路徑使用最近的現存祖先目錄。目錄集合每秒更新。讀取在工作階段鎖外有限並行執行,確認結果時重新驗證觀察版本;匯入基準保存實際讀取位元組的 SHA-256 摘要。 例如:
|
|
20
20
|
|
|
21
21
|
- macOS 目錄監看器的啟動空窗期
|
|
22
22
|
- import 與監看器安裝之間發生的儲存
|
|
@@ -43,6 +43,19 @@ hmr.watch([directory], {
|
|
|
43
43
|
CLI 對 `node_modules`
|
|
44
44
|
之外的 JavaScript 和 TypeScript 模組檔案使用這個過濾器。YAML 設定的變更屬於載入器處理,永遠不會觸發對所有項目的 HMR 重載。
|
|
45
45
|
|
|
46
|
+
## 增量取代
|
|
47
|
+
|
|
48
|
+
公開 `hmr.reload`
|
|
49
|
+
一律強制重新匯入所有已註冊入口及其受追蹤相依模組;沒有入口時也推進匯入視圖,以繞過首次失敗匯入的快取。
|
|
50
|
+
|
|
51
|
+
`watch`
|
|
52
|
+
自動重載只失效發生變化的模組實例及其傳遞匯入方,包括循環相依。共用同一舊 Plugin 物件的入口一起檢查;健康相依模組與獨立入口重用原實例。所有候選通過匯入、匯出身分及前後兩次解析檢查後,只呼叫一次
|
|
53
|
+
`Graft.replace`
|
|
54
|
+
發布。失敗保持執行中的外掛不變,保留 dirty 集合及失敗觀察,等待下一次變化;重載期間到達的事件留給下一輪。
|
|
55
|
+
|
|
56
|
+
模組實例保留自己的解析視圖,舊程式碼(包括 activation 清理)晚到的 dynamic
|
|
57
|
+
import 不會切換到全域最新版。重用模組保留相依邊;從未執行過的動態匯入無法提前發現。
|
|
58
|
+
|
|
46
59
|
## 失敗
|
|
47
60
|
|
|
48
61
|
防抖動不會讓檔案寫入變成原子操作。如果重載遇到不完整的程式碼,它會發出
|
|
@@ -55,8 +68,8 @@ CLI 對 `node_modules`
|
|
|
55
68
|
|
|
56
69
|
## 何時需要重新啟動
|
|
57
70
|
|
|
58
|
-
CommonJS
|
|
59
|
-
`Skipped
|
|
71
|
+
CommonJS 和原生相依模組需要重新啟動行程。HMR 依模組實例追蹤相依資訊,包括入口匯入結束後的動態匯入。只要存活租約能到達這類相依模組,重載就回傳
|
|
72
|
+
`Skipped / restart-required`。
|
|
60
73
|
|
|
61
74
|
透過 `require` 載入的模組不受 ESM URL 版本的隔離,包括
|
|
62
75
|
`require(esm)`。HMR 在這裡很謹慎:帶版本的模組只要 import 了 `node:module`(或
|
|
@@ -67,14 +80,14 @@ import。
|
|
|
67
80
|
|
|
68
81
|
即使沒有檔案通知,監聽也會回報已知的原生載入重新啟動要求,包括啟動後才取得該能力的外掛。週期性狀態檢查不會重複回報相同狀態;這條警告不表示已經執行重載。
|
|
69
82
|
|
|
70
|
-
|
|
83
|
+
本機檔案入口及其可達相依邊會依模組實例保留請求路徑、匯入方和實體目標。入口和相依檔案共用檢查邏輯及輪詢資料;失敗待重試的相依圖繼續保留,最後一個租約釋放後不再參與檢查。重載在 import 之前檢查這個關係,在發布任何取代之前再檢查一次。如果符號連結的目標變了,重載會回傳
|
|
71
84
|
`Skipped / restart-required`,並附上受影響的請求 URL,所有正在執行的外掛保持不變。輪詢也會檢查這個關係,包括內容相同的目標和漏掉的原生事件。重新指定入口或 helper 的目標之後,請重新啟動行程。HMR 不會使 Node 的解析快取失效。這些檢查涵蓋追蹤相依圖中的本機檔案請求,不涵蓋套件的 export
|
|
72
85
|
map 或裸套件解析的變化。
|
|
73
86
|
|
|
74
87
|
## 入口租約
|
|
75
88
|
|
|
76
89
|
`importerScoped(specifier)` 是一個需要 `Scope`
|
|
77
|
-
的 Effect
|
|
90
|
+
的 Effect。每次呼叫都持有該入口的一個租約。最後一個租約釋放後,該入口不再參與之後的重載。在同一匯入視圖中再次取得租約會保留快取身分;目前視圖保留快取命中所需的觀察資訊,但不監聽無租約入口。成功提交下一視圖後回收入口快取;已實例化視圖的原始碼摘要、相依邊和原生能力標記保留到工作階段關閉,因為舊程式碼仍可重新觸及快取實例,而 Node 不會重播其靜態匯入。
|
|
78
91
|
|
|
79
92
|
首次匯入失敗後,觀察租約仍由設定項目持有,與立即關閉的候選資源 scope 分離。修正模組後,即使原生通知遺失,也能透過輪詢復原。重試會取代觀察代次,移除或停用設定項目則關閉觀察 scope。
|
|
80
93
|
|
|
@@ -82,14 +95,14 @@ map 或裸套件解析的變化。
|
|
|
82
95
|
將觀察租約與候選匯入的暫時資源分開。`ModuleResolver`
|
|
83
96
|
透過 importer 的第二個參數傳遞這個 scope。失敗項目等待重試時應保留它,取代觀察嘗試或移除項目時關閉它;候選資源仍在失敗時立即清理。省略此選項時使用呼叫端的 scope。
|
|
84
97
|
|
|
85
|
-
原生 watcher 在來源資源清理完成後、進入背景合併邊界之前觀察完整 Exit
|
|
98
|
+
原生 watcher 在來源資源清理完成後、進入背景合併邊界之前觀察完整 Exit。只有觀察者主動發起且來源相符的取消不記錄錯誤。資源清理自身產生的中斷和混合失敗都會記錄完整 Cause;一般後端失敗繼續交給消費端處理,不重複記錄。
|
|
86
99
|
|
|
87
100
|
CLI 把這個 importer 傳給
|
|
88
101
|
`ModuleResolver`。它的入口 scope 在設定更新、啟用失敗或啟用等待期間保持存活,並在入口被移除時關閉。
|
|
89
102
|
|
|
90
103
|
輪詢和重新啟動如何與租約配合:
|
|
91
104
|
|
|
92
|
-
-
|
|
105
|
+
- 輪詢根據每個存活的租約決定檔案集合,使用正在執行的模組實例 和最近一次未提交的 import 嘗試。
|
|
93
106
|
- 它會保留失敗的 import 和缺少的本機相依套件,以便重試。
|
|
94
107
|
- 當最後一個擁有者關閉時,它不再讀取該入口以及僅被該入口使用的相依套件。
|
|
95
108
|
- 成功的取代會讓舊的相依圖退役。
|
|
@@ -108,6 +121,10 @@ CLI 把這個 importer 傳給
|
|
|
108
121
|
|
|
109
122
|
在所有這些情況下,都不會取代任何東西。請重新啟動應用程式,或還原成一個可以表示的對應。驗證與別名發布共用核心的取代交易。
|
|
110
123
|
|
|
124
|
+
## 長時間開發工作階段
|
|
125
|
+
|
|
126
|
+
Node 不會淘汰帶版本的 ESM 實例。增量重載減少新實例數量,但長工作階段仍會累積模組實例。HMR 回收不再使用的可達快取和未實例化視圖;已實例化的路由視圖及其原始碼摘要、相依邊和原生能力標記保留到工作階段關閉,以支援舊程式碼重新觸及快取模組。觀察和輪詢仍僅涵蓋活躍及待重試租約。因此行程記憶體沒有硬上界,需要時請重新啟動。HMR 不提供子行程 supervisor,也不宣稱徹底消除洩漏。
|
|
127
|
+
|
|
111
128
|
## 取消
|
|
112
129
|
|
|
113
130
|
模組在取消期間失敗時,HMR 會連同完整的 Cause 一起記錄原始失敗。它在待處理的中斷掩蓋已完成的 Exit 之前完成這一步。
|
package/dist/index.d.ts
CHANGED
|
@@ -54,13 +54,15 @@ export interface ImportOptions {
|
|
|
54
54
|
export interface Hmr {
|
|
55
55
|
/** Keeps imported entries registered until this HMR session closes. */
|
|
56
56
|
readonly importer: (specifier: string) => Promise<unknown>;
|
|
57
|
+
/** Uses the application context and Node cache, without watching or replacing the module. */
|
|
58
|
+
readonly importModule: (specifier: string) => Effect.Effect<unknown, HmrError>;
|
|
57
59
|
/** Registers an observation lease in observationScope, or the caller's scope when omitted. */
|
|
58
60
|
readonly importerScoped: (specifier: string, options?: ImportOptions) => Effect.Effect<unknown, HmrError, Scope.Scope>;
|
|
59
61
|
/** Imports every entry before replacing anything. Activation failures belong to Graft. */
|
|
60
62
|
readonly reload: Effect.Effect<ReloadResult, HmrError>;
|
|
61
63
|
/**
|
|
62
|
-
* Consuming this stream installs
|
|
63
|
-
* per second to recover missed events. Known native loading limitations emit
|
|
64
|
+
* Consuming this stream installs graph-driven directory watchers, polls stat once
|
|
65
|
+
* per second, and audits contents every ten seconds to recover missed events. Known native loading limitations emit
|
|
64
66
|
* Skipped even without a file event. The consumer owns the stream's lifetimes.
|
|
65
67
|
*/
|
|
66
68
|
readonly watch: (paths: ReadonlyArray<string>, options?: WatchOptions) => Stream.Stream<WatchResult, PlatformError.PlatformError, FileSystem.FileSystem>;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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"}
|
|
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,eAAe,sBAAsB,OAAO,gBAAgB;;WAE5D,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"}
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,18 @@ const make = (options) => Effect.gen(function* () {
|
|
|
44
44
|
});
|
|
45
45
|
const serial = (effect) => effect.pipe(preserveExit, semaphore.withPermits(1));
|
|
46
46
|
const hooks = yield* Effect.acquireRelease(Effect.sync(() => modules(options.base, options.include ?? ((url) => !String.includes("/node_modules/")(new URL(url).pathname)))), (hooks) => serial(Ref.set(closed, true).pipe(Effect.andThen(Effect.sync(hooks.close)))));
|
|
47
|
+
const cached = yield* Ref.make(HashMap.empty());
|
|
48
|
+
const retryDirty = yield* Ref.make(HashSet.empty());
|
|
49
|
+
const retryAll = yield* Ref.make(false);
|
|
50
|
+
const observations = Ref.get(owners).pipe(Effect.map((owners) => Array.flatMap(HashMap.toValues(owners), (owner) => Array.getSomes([owner.active, owner.pending]))));
|
|
51
|
+
const collect = Effect.gen(function* () {
|
|
52
|
+
const attempts = yield* observations;
|
|
53
|
+
const currentCache = yield* Ref.get(cached);
|
|
54
|
+
hooks.collect([...attempts, ...HashMap.toValues(currentCache)]);
|
|
55
|
+
const files = hooks.files(attempts);
|
|
56
|
+
yield* Ref.update(retryDirty, HashSet.filter((url) => HashMap.has(files, url)));
|
|
57
|
+
if (attempts.length === 0) yield* Ref.set(retryAll, false);
|
|
58
|
+
});
|
|
47
59
|
const state = yield* Ref.make({
|
|
48
60
|
generation: hooks.prepare(),
|
|
49
61
|
entries: HashMap.empty()
|
|
@@ -75,7 +87,20 @@ const make = (options) => Effect.gen(function* () {
|
|
|
75
87
|
}
|
|
76
88
|
return Option.some(request);
|
|
77
89
|
});
|
|
78
|
-
const changedResolutions =
|
|
90
|
+
const changedResolutions = Effect.gen(function* () {
|
|
91
|
+
const current = yield* Ref.get(owners);
|
|
92
|
+
const attempts = hooks.resolutions(Array.flatMap(HashMap.toValues(current), (owner) => Array.getSomes([owner.active, owner.pending])));
|
|
93
|
+
const requests = HashSet.fromIterable(Array.filterMap(attempts, (attempt) => attempt.request === void 0 ? Result.fail(void 0) : Result.succeed(attempt.request)));
|
|
94
|
+
const resolveAll = (requests) => Effect.forEach(requests, (request) => physicalTarget(request).pipe(Effect.map((target) => [request, target])), { concurrency: 8 }).pipe(Effect.map(HashMap.fromIterable));
|
|
95
|
+
const actual = yield* resolveAll(requests);
|
|
96
|
+
const remaining = yield* resolveAll(HashSet.fromIterable(Array.filterMap(attempts, (attempt) => attempt.request !== void 0 && Option.getOrUndefined(HashMap.get(actual, attempt.request)) === void 0 && !HashMap.has(actual, attempt.url) ? Result.succeed(attempt.url) : Result.fail(void 0))));
|
|
97
|
+
const targets = HashMap.union(actual, remaining);
|
|
98
|
+
return Array.fromIterable(HashSet.fromIterable(Array.filterMap(attempts, (attempt) => {
|
|
99
|
+
if (attempt.request === void 0) return Result.fail(void 0);
|
|
100
|
+
const actual = Option.getOrUndefined(HashMap.get(targets, attempt.request));
|
|
101
|
+
return actual === pathToFileURL(fileURLToPath(attempt.url)).href || actual === void 0 && Option.getOrUndefined(HashMap.get(targets, attempt.url)) === void 0 ? Result.fail(void 0) : Result.succeed(attempt.request);
|
|
102
|
+
})));
|
|
103
|
+
});
|
|
79
104
|
const load = Effect.fnUntraced(function* (specifier) {
|
|
80
105
|
const namespace = yield* Effect.tryPromise({
|
|
81
106
|
try: () => import(
|
|
@@ -134,6 +159,7 @@ const make = (options) => Effect.gen(function* () {
|
|
|
134
159
|
onSome: (request) => ({ request })
|
|
135
160
|
})
|
|
136
161
|
}));
|
|
162
|
+
if (Option.isSome(attempt)) yield* Ref.update(cached, HashMap.set(attempt.value.url, attempt.value));
|
|
137
163
|
yield* Ref.update(owners, HashMap.set(token, {
|
|
138
164
|
active: Exit.isSuccess(exit) ? attempt : Option.none(),
|
|
139
165
|
pending: Exit.isFailure(exit) ? attempt : Option.none()
|
|
@@ -157,7 +183,7 @@ const make = (options) => Effect.gen(function* () {
|
|
|
157
183
|
token,
|
|
158
184
|
exit: Exit.map(exit, (entry) => entry.namespace)
|
|
159
185
|
};
|
|
160
|
-
}).pipe(Effect.ensuring(Effect.sync(hooks.discard)))), ({ token }) => serial(Effect.gen(function* () {
|
|
186
|
+
}).pipe(Effect.ensuring(Effect.sync(hooks.discard)), Effect.ensuring(collect))), ({ token }) => serial(Effect.gen(function* () {
|
|
161
187
|
yield* Ref.update(owners, HashMap.remove(token));
|
|
162
188
|
yield* Ref.update(state, (current) => ({
|
|
163
189
|
...current,
|
|
@@ -169,12 +195,30 @@ const make = (options) => Effect.gen(function* () {
|
|
|
169
195
|
});
|
|
170
196
|
})
|
|
171
197
|
}));
|
|
172
|
-
}))).pipe(Effect.flatMap(({ exit }) => exit), preserveExit);
|
|
198
|
+
}).pipe(Effect.ensuring(collect)))).pipe(Effect.flatMap(({ exit }) => exit), preserveExit);
|
|
199
|
+
const importModule = (specifier) => serial(checkOpen.pipe(Effect.andThen(Effect.tryPromise({
|
|
200
|
+
try: () => import(
|
|
201
|
+
/* @vite-ignore */
|
|
202
|
+
hooks.rawRequest(specifier)
|
|
203
|
+
),
|
|
204
|
+
catch: (cause) => HmrError.make({
|
|
205
|
+
stage: "import",
|
|
206
|
+
specifier,
|
|
207
|
+
cause
|
|
208
|
+
})
|
|
209
|
+
}))));
|
|
173
210
|
const importerScoped = (specifier, options) => options === void 0 ? importWithLease(specifier) : importWithLease(specifier).pipe(Scope.provide(options.observationScope));
|
|
174
211
|
const importer = (specifier) => Ref.getUnsafe(closed) ? Promise.reject(closedError()) : run(importerScoped(specifier).pipe(Effect.provideService(Scope.Scope, imports)));
|
|
175
|
-
const
|
|
176
|
-
const
|
|
212
|
+
const unsupportedAttempts = (attempts) => Array.reduce(attempts, HashSet.empty(), (all, attempt) => HashSet.union(all, hooks.unsupported(attempt.generation, [attempt.url])));
|
|
213
|
+
const unsupportedModules = Ref.get(owners).pipe(Effect.map((owned) => unsupportedAttempts(Array.getSomes(Array.map(HashMap.toValues(owned), (owner) => owner.active)))));
|
|
214
|
+
const reloadChanged = (dirty) => serial(Effect.gen(function* () {
|
|
177
215
|
yield* checkOpen;
|
|
216
|
+
const fullRetry = yield* Ref.get(retryAll);
|
|
217
|
+
const force = dirty === void 0 || fullRetry;
|
|
218
|
+
const pendingDirty = yield* Ref.get(retryDirty);
|
|
219
|
+
const changedFiles = dirty === void 0 ? pendingDirty : HashSet.union(pendingDirty, dirty);
|
|
220
|
+
yield* Ref.set(retryDirty, changedFiles);
|
|
221
|
+
yield* Ref.set(retryAll, force);
|
|
178
222
|
const changed = yield* changedResolutions;
|
|
179
223
|
if (changed.length > 0) return WatchResult.Skipped({
|
|
180
224
|
reason: "restart-required",
|
|
@@ -186,10 +230,26 @@ const make = (options) => Effect.gen(function* () {
|
|
|
186
230
|
modules: Array.fromIterable(unsupported),
|
|
187
231
|
reason: "restart-required"
|
|
188
232
|
});
|
|
189
|
-
const
|
|
233
|
+
const owned = yield* Ref.get(owners);
|
|
234
|
+
const attempts = Array.flatMap(HashMap.toValues(owned), (owner) => Array.getSomes([owner.active, owner.pending]));
|
|
235
|
+
const active = Array.getSomes(Array.map(HashMap.toValues(owned), (owner) => owner.active));
|
|
236
|
+
let selected = entries;
|
|
237
|
+
let invalid = HashSet.empty();
|
|
238
|
+
if (!force) {
|
|
239
|
+
let affected = changedFiles;
|
|
240
|
+
while (true) {
|
|
241
|
+
invalid = hooks.invalidated(attempts, affected);
|
|
242
|
+
const plugins = HashSet.fromIterable(Array.filterMap(Array.fromIterable(entries), ([url, entry]) => HashSet.has(affected, url) || Array.some(Array.fromIterable(entry.leases), (token) => Option.exists(HashMap.get(owned, token), (owner) => Array.some(Array.getSomes([owner.active, owner.pending]), (attempt) => hooks.affected(attempt, invalid)))) ? Result.succeed(entry.plugin) : Result.fail(void 0)));
|
|
243
|
+
selected = HashMap.filter(entries, (entry) => HashSet.has(plugins, entry.plugin));
|
|
244
|
+
const expanded = HashSet.union(affected, HashSet.fromIterable(HashMap.keys(selected)));
|
|
245
|
+
if (HashSet.size(expanded) === HashSet.size(affected)) break;
|
|
246
|
+
affected = expanded;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const candidate = force ? hooks.prepare() : hooks.prepare(active, invalid);
|
|
190
250
|
hooks.start(candidate, true);
|
|
191
251
|
return yield* Effect.gen(function* () {
|
|
192
|
-
const loaded = yield* Effect.forEach(
|
|
252
|
+
const loaded = yield* Effect.forEach(selected, ([url, previous]) => Ref.update(owners, HashMap.map((owner, token) => HashSet.has(previous.leases, token) ? {
|
|
193
253
|
...owner,
|
|
194
254
|
pending: Option.map(owner.active, (attempt) => ({
|
|
195
255
|
...attempt,
|
|
@@ -209,16 +269,25 @@ const make = (options) => Effect.gen(function* () {
|
|
|
209
269
|
reason: "ambiguous-exports",
|
|
210
270
|
modules: Array.map(ambiguous, ({ url }) => url)
|
|
211
271
|
});
|
|
212
|
-
const
|
|
272
|
+
const replacements = HashMap.fromIterable(Array.map(candidates, ({ url, plugin, leases }) => [url, {
|
|
213
273
|
plugin,
|
|
214
274
|
leases
|
|
215
275
|
}]));
|
|
276
|
+
const next = HashMap.union(entries, replacements);
|
|
216
277
|
const pairs = Array.map(candidates, ({ previous, plugin }) => [previous, plugin]);
|
|
217
278
|
const changed = yield* changedResolutions;
|
|
218
279
|
if (changed.length > 0) return WatchResult.Skipped({
|
|
219
280
|
reason: "restart-required",
|
|
220
281
|
modules: changed
|
|
221
282
|
});
|
|
283
|
+
const unsafe = unsupportedAttempts(Array.map(active, (attempt) => HashMap.has(replacements, attempt.url) ? {
|
|
284
|
+
...attempt,
|
|
285
|
+
generation: candidate
|
|
286
|
+
} : attempt));
|
|
287
|
+
if (HashSet.size(unsafe) > 0) return WatchResult.Skipped({
|
|
288
|
+
reason: "restart-required",
|
|
289
|
+
modules: Array.fromIterable(unsafe)
|
|
290
|
+
});
|
|
222
291
|
const replacement = yield* Graft.replace(pairs).pipe(Effect.provideService(Graft.Graft, graft), Effect.result);
|
|
223
292
|
if (Result.isFailure(replacement)) return WatchResult.Skipped({
|
|
224
293
|
reason: "identity-conflict",
|
|
@@ -236,17 +305,24 @@ const make = (options) => Effect.gen(function* () {
|
|
|
236
305
|
entries: next,
|
|
237
306
|
generation: candidate
|
|
238
307
|
});
|
|
239
|
-
|
|
308
|
+
yield* Ref.update(cached, (cached) => HashMap.union(HashMap.filter(cached, (_, url) => HashMap.has(next, url)), HashMap.map(replacements, (_, url) => ({
|
|
309
|
+
url,
|
|
310
|
+
generation: candidate
|
|
311
|
+
}))));
|
|
312
|
+
yield* Ref.set(retryDirty, HashSet.empty());
|
|
313
|
+
yield* Ref.set(retryAll, false);
|
|
314
|
+
return WatchResult.Reloaded({ entries: Array.map(candidates, (candidate) => candidate.url) });
|
|
240
315
|
}).pipe(Effect.catchIf((error) => Schema.is(RestartRequired)(error.cause), (error) => Effect.succeed(WatchResult.Skipped({
|
|
241
316
|
modules: Array.fromIterable(HashSet.add(hooks.unsupported(candidate), error.cause.url)),
|
|
242
317
|
reason: "restart-required"
|
|
243
|
-
}))), Effect.ensuring(Effect.sync(hooks.discard)));
|
|
318
|
+
}))), Effect.ensuring(Effect.sync(hooks.discard)), Effect.ensuring(collect));
|
|
244
319
|
}));
|
|
245
320
|
return {
|
|
246
321
|
importer,
|
|
247
322
|
importerScoped,
|
|
248
|
-
|
|
249
|
-
|
|
323
|
+
importModule,
|
|
324
|
+
reload: reloadChanged(),
|
|
325
|
+
watch: (paths, watchOptions) => paths.length === 0 ? Stream.empty : changes((use) => Ref.get(owners).pipe(Effect.map((owners) => hooks.files([...Array.getSomes(Array.map(HashMap.toValues(owners), (owner) => owner.active)), ...Array.getSomes(Array.map(HashMap.toValues(owners), (owner) => owner.pending))])), Effect.flatMap(use), semaphore.withPermits(1)), paths, watchOptions).pipe(Stream.mapEffect((dirty) => Effect.map(Effect.exit(reloadChanged(dirty)), (exit) => Exit.isSuccess(exit) ? exit.value : WatchResult.Failed({ cause: exit.cause }))), Stream.merge(Stream.tick("1 second").pipe(Stream.mapEffect(() => unsupportedModules.pipe(semaphore.withPermits(1))), Stream.changes, Stream.filter((modules) => HashSet.size(modules) > 0), Stream.map((modules) => WatchResult.Skipped({
|
|
250
326
|
reason: "restart-required",
|
|
251
327
|
modules: Array.fromIterable(modules)
|
|
252
328
|
})))))
|