@graftland/hmr 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
202
+
package/README.md CHANGED
@@ -1,3 +1,165 @@
1
- # Temporary Holding Version
1
+ # @graftland/hmr
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+
8
+ Reloads plugin modules on Node.js without restarting the process.
9
+
10
+ ```sh
11
+ npm install @graftland/hmr effect
12
+ ```
13
+
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.
159
+
160
+ ## Documentation
161
+
162
+ - [Hot reload](https://graftland.github.io/graft/hot-reload/)
163
+
164
+ Part of [Graft](https://github.com/graftland/graft). Licensed under
165
+ [Apache 2.0](https://github.com/graftland/graft/blob/main/LICENSE).
@@ -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) 许可。
@@ -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) 授權。