ez-saga 18.0.6 → 18.0.7

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 CHANGED
@@ -142,4 +142,55 @@ function stateMapProps(state, props) {
142
142
  export default connect(stateMapProps)(View);
143
143
  ```
144
144
 
145
- We can invoke reducers and effects using dispatch, and we can obtain the results returned by effects.
145
+ We can invoke reducers and effects using dispatch, and we can obtain the results returned by effects.
146
+
147
+ # Vite Plugin for HMR
148
+
149
+ `ez-saga` provides a built-in Vite plugin to support Hot Module Replacement (HMR) for models. This allows you to modify effects and reducers during development without reloading the page.
150
+
151
+ ## Usage
152
+
153
+ 1. Import the plugin in your `vite.config.ts`:
154
+
155
+ ```typescript
156
+ import { defineConfig } from 'vite';
157
+ import ezSagaHmr from 'ez-saga/vite';
158
+
159
+ export default defineConfig({
160
+ plugins: [
161
+ ezSagaHmr(), // Add ez-saga HMR plugin
162
+ // other plugins...
163
+ ]
164
+ });
165
+ ```
166
+
167
+ 2. That's it!
168
+
169
+ The plugin automatically detects your model files and injects hot update logic. When you modify a model file, `ez-saga` will:
170
+ - Cancel old saga tasks.
171
+ - Re-register the model reducers (hot-swap logic).
172
+ - Restart the effects.
173
+
174
+ **Note**: This requires your model files to be exported using `export default` and contain a `name` property.
175
+
176
+ # Webpack Plugin for HMR
177
+
178
+ For Webpack 5 users, `ez-saga` also provides a compatible HMR plugin.
179
+
180
+ ## Usage
181
+
182
+ 1. Import the plugin in your `webpack.config.js`:
183
+
184
+ ```javascript
185
+ const EzSagaWebpackPlugin = require('ez-saga/webpack').default; // Notice the .default for CJS
186
+
187
+ module.exports = {
188
+ // ...
189
+ plugins: [
190
+ new EzSagaWebpackPlugin(),
191
+ // other plugins...
192
+ ]
193
+ };
194
+ ```
195
+
196
+ This plugin automatically configures the HMR loader for your model files.
@@ -24,15 +24,9 @@ function* handleEffect(modelName, effectFunc, tools, action) {
24
24
  /**
25
25
  * 获取注册model函数
26
26
  */
27
- function getRegistModelFunc(store, registedModel, allReducers, sagaMiddleware) {
28
- return function regist(reduxModel) {
29
- const model = {
30
- ...reduxModel,
31
- initialState: {}
32
- };
33
- if (registedModel[model.name]) {
34
- return;
35
- }
27
+ function getRegistModelFunc(store, registedModel, allReducers, sagaMiddleware, runningEffects) {
28
+ // 提取的公共模型安装逻辑
29
+ function installModel(model) {
36
30
  // 初始化模型属性
37
31
  if (!model.state)
38
32
  model.state = {};
@@ -57,14 +51,45 @@ function getRegistModelFunc(store, registedModel, allReducers, sagaMiddleware) {
57
51
  const newReducer = combineReducers(allReducers);
58
52
  store.replaceReducer(newReducer);
59
53
  // 注册 Effects
54
+ runningEffects[model.name] = [];
60
55
  for (const effectKey in model.effects) {
61
56
  const type = `${model.name}/${effectKey}`;
57
+ console.log('[ez-saga] regist effect:', type);
62
58
  const effectFunc = model.effects[effectKey];
63
- sagaMiddleware.run(function* () {
59
+ const task = sagaMiddleware.run(function* () {
64
60
  // 使用 takeEvery 的参数传递功能,将上下文传入 handleEffect
65
61
  // handleEffect(modelName, effectFunc, tools, action)
66
62
  yield takeEvery(type, handleEffect, model.name, effectFunc, opFun);
67
63
  });
64
+ runningEffects[model.name].push(task);
65
+ }
66
+ }
67
+ return function regist(reduxModel) {
68
+ const model = {
69
+ ...reduxModel,
70
+ initialState: {}
71
+ };
72
+ if (registedModel[model.name]) {
73
+ return;
74
+ }
75
+ // 安装模型
76
+ installModel(model);
77
+ // 开发环境:注册热更新事件监听
78
+ if (process.env.NODE_ENV !== 'production' && typeof window !== 'undefined') {
79
+ const eventName = `EZ_SAGA_UPDATE_${model.name}`;
80
+ const hmrHandler = (e) => {
81
+ const newModel = e.detail;
82
+ console.log('[ez-saga] Hot updating model:', newModel.name);
83
+ // 1. 取消旧任务
84
+ const tasks = runningEffects[model.name];
85
+ if (tasks && tasks.length) {
86
+ tasks.forEach(task => task.cancel());
87
+ }
88
+ runningEffects[model.name] = [];
89
+ // 2. 安装新模型 (包含更新 Reducer 和重启 Effects)
90
+ installModel(newModel);
91
+ };
92
+ window.addEventListener(eventName, hmrHandler);
68
93
  }
69
94
  };
70
95
  }
@@ -86,7 +111,9 @@ export default function create() {
86
111
  devTools: process.env.NODE_ENV !== 'production',
87
112
  preloadedState: {}
88
113
  });
89
- const regist = getRegistModelFunc(store, registedModel, allReducers, sagaMiddleware);
114
+ // 存储运行中的 Effect 任务,用于热更新取消
115
+ const runningEffects = {};
116
+ const regist = getRegistModelFunc(store, registedModel, allReducers, sagaMiddleware, runningEffects);
90
117
  return {
91
118
  store,
92
119
  sagaMiddleware,
@@ -1 +1 @@
1
- {"version":3,"file":"createApp.js","sourceRoot":"","sources":["../../src/redux/createApp.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"createApp.js","sourceRoot":"","sources":["../../src/redux/createApp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,oBAA8C,MAAM,YAAY,CAAC;AACxE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAG9E,OAAO,SAAS,MAAM,kBAAkB,CAAC;AACzC,OAAO,uBAAuB,MAAM,qBAAqB,CAAC;AAE1D,+BAA+B;AAC/B,MAAM,KAAK,GAAe,EAAE,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC;AAE5D;;;GAGG;AACH,QAAQ,CAAC,CAAC,YAAY,CACpB,SAAiB,EACjB,UAAkB,EAClB,KAAiB,EACjB,MAAqB;IAErB,oBAAoB;IACpB,MAAM,UAAU,CAAC,EAAE,IAAI,EAAE,GAAG,SAAS,YAAY,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;IAEjF,qCAAqC;IACrC,MAAM,GAAG,GAAQ,MAAM,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;IAEvD,oBAAoB;IACpB,MAAM,UAAU,CAAC,EAAE,IAAI,EAAE,GAAG,SAAS,YAAY,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;IAElF,2BAA2B;IAC3B,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;QACvB,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;AACH,CAAC;AAED;;GAEG;AACH,SAAS,kBAAkB,CACzB,KAAiC,EACjC,aAA4B,EAC5B,WAAgD,EAChD,cAAsC,EACtC,cAAsC;IAEtC,cAAc;IACd,SAAS,YAAY,CAAC,KAAqB;QACzC,UAAU;QACV,IAAI,CAAC,KAAK,CAAC,KAAK;YAAE,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC;QACnC,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC,KAAK,CAAC;QACjC,IAAI,CAAC,KAAK,CAAC,QAAQ;YAAE,KAAK,CAAC,QAAQ,GAAG,EAAE,CAAC;QACzC,0BAA0B;QAC1B,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS;YAAE,KAAK,CAAC,QAAQ,CAAC,SAAS,GAAG,SAAS,CAAC;QACpE,IAAI,CAAC,KAAK,CAAC,OAAO;YAAE,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC;QAEvC,4BAA4B;QAC5B,MAAM,UAAU,GAAG,WAAW,CAAC;YAC7B,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,YAAY,EAAE,KAAK,CAAC,YAAY;YAChC,QAAQ,EAAE,KAAK,CAAC,QAAe;SAChC,CAAC,CAAC;QAEH,aAAa;QACb,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC;QAC7C,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QAElC,wBAAwB;QACxB,MAAM,UAAU,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;QAChD,KAAK,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;QAEjC,aAAa;QACb,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;QAChC,KAAK,MAAM,SAAS,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,GAAG,KAAK,CAAC,IAAI,IAAI,SAAS,EAAE,CAAC;YAC1C,OAAO,CAAC,GAAG,CAAC,0BAA0B,EAAE,IAAI,CAAC,CAAC;YAC9C,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAC5C,MAAM,IAAI,GAAG,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC;gBACvC,2CAA2C;gBAC3C,qDAAqD;gBACrD,MAAM,SAAS,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC;YACrE,CAAC,CAAC,CAAC;YACH,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;IAED,OAAO,SAAS,MAAM,CAAC,UAAsB;QAC3C,MAAM,KAAK,GAAG;YACZ,GAAG,UAAU;YACb,YAAY,EAAE,EAAE;SACC,CAAC;QAEpB,IAAI,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,OAAO;QACT,CAAC;QAED,OAAO;QACP,YAAY,CAAC,KAAK,CAAC,CAAC;QAEpB,iBAAiB;QACjB,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;YAC3E,MAAM,SAAS,GAAG,kBAAkB,KAAK,CAAC,IAAI,EAAE,CAAC;YACjD,MAAM,UAAU,GAAG,CAAC,CAAM,EAAE,EAAE;gBAC5B,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAwB,CAAC;gBAC5C,OAAO,CAAC,GAAG,CAAC,+BAA+B,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;gBAC5D,WAAW;gBACX,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBACzC,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;oBAC1B,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;gBACvC,CAAC;gBACD,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;gBAChC,sCAAsC;gBACtC,YAAY,CAAC,QAAQ,CAAC,CAAC;YACzB,CAAC,CAAC;YACF,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;QACjD,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED,cAAc;AACd,MAAM,CAAC,OAAO,UAAU,MAAM;IAC5B,MAAM,WAAW,GAAG,EAAE,CAAC;IACvB,MAAM,aAAa,GAAkB,EAAE,CAAC;IAExC,MAAM,cAAc,GAAG,oBAAoB,EAAE,CAAC;IAC9C,MAAM,iBAAiB,GAAG,uBAAuB,CAAC,aAAa,CAAC,CAAC;IAEjE,mCAAmC;IACnC,8BAA8B;IAC9B,MAAM,KAAK,GAAG,cAAc,CAAC;QAC3B,OAAO,EAAE,SAAS,EAAE,aAAa;QACjC,UAAU,EAAE,CAAC,oBAAoB,EAAE,EAAE,CACnC,oBAAoB,CAAC;YACnB,KAAK,EAAE,KAAK,EAAE,0BAA0B;YACxC,iBAAiB,EAAE,KAAK,EAAE,4CAA4C;YACtE,cAAc,EAAE,KAAK,CAAC,mBAAmB;SAC1C,CAAC,CAAC,MAAM,CAAC,iBAAiB,EAAE,cAAc,CAAC;QAC9C,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY;QAC/C,cAAc,EAAE,EAAE;KACnB,CAAC,CAAC;IAEH,2BAA2B;IAC3B,MAAM,cAAc,GAA2B,EAAE,CAAC;IAElD,MAAM,MAAM,GAAG,kBAAkB,CAAC,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,cAAc,CAAC,CAAC;IAErG,OAAO;QACL,KAAK;QACL,cAAc;QACd,MAAM;KACP,CAAC;AACJ,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { Plugin } from 'vite';
2
+ /**
3
+ * ez-saga Vite 插件
4
+ *
5
+ * 自动为 ez-saga model 文件注入 HMR 代码。
6
+ * 约定:
7
+ * 1. Model 文件必须使用 export default 导出对象
8
+ * 2. 导出的对象必须包含 'name' 属性 (作为 Model 的唯一标识)
9
+ * 3. 项目中必须将 app 实例挂载到 window.app
10
+ */
11
+ export default function ezSagaHmr(): Plugin;
@@ -0,0 +1,59 @@
1
+ import MagicString from 'magic-string';
2
+ /**
3
+ * ez-saga Vite 插件
4
+ *
5
+ * 自动为 ez-saga model 文件注入 HMR 代码。
6
+ * 约定:
7
+ * 1. Model 文件必须使用 export default 导出对象
8
+ * 2. 导出的对象必须包含 'name' 属性 (作为 Model 的唯一标识)
9
+ * 3. 项目中必须将 app 实例挂载到 window.app
10
+ */
11
+ export default function ezSagaHmr() {
12
+ return {
13
+ name: 'vite-plugin-ez-saga-hmr',
14
+ apply: 'serve', // 仅在开发环境(serve)模式下应用
15
+ transform(code, id) {
16
+ // 过滤掉 node_modules 和非 JS/TS 文件
17
+ if (id.includes('node_modules') || !/\.(js|ts|tsx|jsx)$/.test(id)) {
18
+ return;
19
+ }
20
+ // 简单的特征检测:检查是否包含 ez-saga model 的关键属性
21
+ // 必须包含 name 属性,且通常包含 effects 或 reducers 或 state
22
+ // 宽容匹配:
23
+ // 1. export default ... (可能是对象字面量,也可能是变量)
24
+ // 2. name: ... (可能是字符串字面量,也可能是变量)
25
+ const hasDefaultExport = /export\s+default/.test(code);
26
+ const hasNameProp = /name\s*:/.test(code);
27
+ const hasModelKeywords = /(effects|reducers|state|initialState)\s*:\s*\{/.test(code);
28
+ if (hasDefaultExport && hasNameProp && hasModelKeywords) {
29
+ // console.log('ez-saga HMR: Injecting HMR code into', id);
30
+ // 注入 HMR 代码
31
+ // 使用 window 事件通信,解耦 app 实例引用
32
+ const hmrCode = `
33
+ if (import.meta.hot) {
34
+ import.meta.hot.accept((newModule) => {
35
+ if (newModule && newModule.default) {
36
+ const model = newModule.default;
37
+ if (model.name) {
38
+ // console.log('[ez-saga] Hot updating model:', model.name);
39
+ if (window && window.dispatchEvent) {
40
+ window.dispatchEvent(new CustomEvent('EZ_SAGA_UPDATE_' + model.name, {
41
+ detail: model
42
+ }));
43
+ }
44
+ }
45
+ }
46
+ });
47
+ }
48
+ `;
49
+ const s = new MagicString(code);
50
+ s.append(hmrCode);
51
+ return {
52
+ code: s.toString(),
53
+ map: s.generateMap({ hires: true })
54
+ };
55
+ }
56
+ }
57
+ };
58
+ }
59
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/vite/index.ts"],"names":[],"mappings":"AACA,OAAO,WAAW,MAAM,cAAc,CAAC;AAEvC;;;;;;;;GAQG;AAEH,MAAM,CAAC,OAAO,UAAU,SAAS;IAC7B,OAAO;QACH,IAAI,EAAE,yBAAyB;QAC/B,KAAK,EAAE,OAAO,EAAE,qBAAqB;QACrC,SAAS,CAAC,IAAI,EAAE,EAAE;YACd,+BAA+B;YAC/B,IAAI,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;gBAChE,OAAO;YACX,CAAC;YACD,qCAAqC;YACrC,gDAAgD;YAChD,QAAQ;YACR,0CAA0C;YAC1C,kCAAkC;YAClC,MAAM,gBAAgB,GAAG,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvD,MAAM,WAAW,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC1C,MAAM,gBAAgB,GAAG,gDAAgD,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAErF,IAAI,gBAAgB,IAAI,WAAW,IAAI,gBAAgB,EAAE,CAAC;gBACtD,2DAA2D;gBAC3D,YAAY;gBACZ,6BAA6B;gBAC7B,MAAM,OAAO,GAAG;;;;;;;;;;;;;;;;CAgB/B,CAAC;gBACc,MAAM,CAAC,GAAG,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC;gBAChC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBAElB,OAAO;oBACH,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE;oBAClB,GAAG,EAAE,CAAC,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;iBACtC,CAAC;YACN,CAAC;QACL,CAAC;KACJ,CAAC;AACN,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { Compiler } from 'webpack';
2
+ /**
3
+ * ez-saga Webpack Plugin (Webpack 5+)
4
+ *
5
+ * 自动配置 ez-saga-loader,实现 Model 热更新。
6
+ */
7
+ export default class EzSagaWebpackPlugin {
8
+ apply(compiler: Compiler): void;
9
+ }
@@ -0,0 +1,28 @@
1
+ import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ // ESM 环境下模拟 __dirname
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = path.dirname(__filename);
6
+ /**
7
+ * ez-saga Webpack Plugin (Webpack 5+)
8
+ *
9
+ * 自动配置 ez-saga-loader,实现 Model 热更新。
10
+ */
11
+ export default class EzSagaWebpackPlugin {
12
+ apply(compiler) {
13
+ // 获取 loader 的绝对路径
14
+ const loaderPath = path.resolve(__dirname, './loader.js');
15
+ compiler.hooks.afterEnvironment.tap('EzSagaWebpackPlugin', () => {
16
+ // 注入 loader规则
17
+ compiler.options.module.rules.push({
18
+ test: /\.(js|ts|tsx|jsx)$/,
19
+ exclude: /node_modules/,
20
+ enforce: 'post', // 在编译后执行,避免影响 ts-loader 输入
21
+ use: [{
22
+ loader: loaderPath
23
+ }]
24
+ });
25
+ });
26
+ }
27
+ }
28
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/webpack/index.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,sBAAsB;AACtB,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;AAE3C;;;;GAIG;AACH,MAAM,CAAC,OAAO,OAAO,mBAAmB;IACpC,KAAK,CAAC,QAAkB;QACpB,kBAAkB;QAClB,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;QAE1D,QAAQ,CAAC,KAAK,CAAC,gBAAgB,CAAC,GAAG,CAAC,qBAAqB,EAAE,GAAG,EAAE;YAC5D,cAAc;YACd,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;gBAC/B,IAAI,EAAE,oBAAoB;gBAC1B,OAAO,EAAE,cAAc;gBACvB,OAAO,EAAE,MAAM,EAAE,2BAA2B;gBAC5C,GAAG,EAAE,CAAC;wBACF,MAAM,EAAE,UAAU;qBACrB,CAAC;aACL,CAAC,CAAC;QACP,CAAC,CAAC,CAAC;IACP,CAAC;CACJ"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * ez-saga Webpack Loader
3
+ * 对应 src/vite/index.ts 的逻辑,用于 Webpack 场景
4
+ */
5
+ export default function (this: any, source: string): void;
@@ -0,0 +1,68 @@
1
+ import MagicString from 'magic-string';
2
+ /**
3
+ * ez-saga Webpack Loader
4
+ * 对应 src/vite/index.ts 的逻辑,用于 Webpack 场景
5
+ */
6
+ export default function (source) {
7
+ // 简单的特征检测
8
+ const hasDefaultExport = /export\s+default/.test(source) || /exports\.default\s*=/.test(source) || /module\.exports\s*=/.test(source);
9
+ // name属性检查 (宽松匹配: name: "foo" 或 name = "foo")
10
+ const hasNameProp = /(name\s*[:=])/.test(source);
11
+ // 关键字检查 (Loose due to compilation)
12
+ const hasModelKeywords = /(effects|reducers|state|initialState)\s*[:=]\s*\{/.test(source);
13
+ if (hasDefaultExport && hasNameProp && hasModelKeywords) {
14
+ const callback = this.async();
15
+ const s = new MagicString(source);
16
+ // Regular expressions to detect export patterns
17
+ const esmRegex = /export\s+default/;
18
+ const cjsDefaultRegex = /exports\.default\s*=/;
19
+ const cjsModuleRegex = /module\.exports\s*=/;
20
+ let match;
21
+ const modelVarName = '__ez_saga_model_def';
22
+ if ((match = esmRegex.exec(source)) !== null) {
23
+ // ESM: export default { ... }
24
+ const start = match.index;
25
+ const end = start + match[0].length;
26
+ s.overwrite(start, end, `const ${modelVarName} =`);
27
+ s.append(`\nexport default ${modelVarName};\n`);
28
+ }
29
+ else if ((match = cjsDefaultRegex.exec(source)) !== null) {
30
+ // CJS: exports.default = { ... }
31
+ const start = match.index;
32
+ const end = start + match[0].length;
33
+ s.overwrite(start, end, `const ${modelVarName} =`);
34
+ s.append(`\nexports.default = ${modelVarName};\n`);
35
+ }
36
+ else if ((match = cjsModuleRegex.exec(source)) !== null) {
37
+ // CJS: module.exports = { ... }
38
+ const start = match.index;
39
+ const end = start + match[0].length;
40
+ s.overwrite(start, end, `const ${modelVarName} =`);
41
+ s.append(`\nmodule.exports = ${modelVarName};\n`);
42
+ }
43
+ else {
44
+ // 匹配失败
45
+ this.callback(null, source);
46
+ return;
47
+ }
48
+ // 注入 HMR 逻辑
49
+ const hmrCode = `
50
+ if (module.hot) {
51
+ module.hot.accept();
52
+ if (${modelVarName} && ${modelVarName}.name) {
53
+ if (typeof window !== 'undefined' && window.dispatchEvent) {
54
+ window.dispatchEvent(new CustomEvent('EZ_SAGA_UPDATE_' + ${modelVarName}.name, {
55
+ detail: ${modelVarName}
56
+ }));
57
+ }
58
+ }
59
+ }
60
+ `;
61
+ s.append(hmrCode);
62
+ // 禁用 SourceMap
63
+ callback(null, s.toString());
64
+ return;
65
+ }
66
+ this.callback(null, source);
67
+ }
68
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/webpack/loader.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,9 +1,25 @@
1
1
  {
2
2
  "name": "ez-saga",
3
- "version": "18.0.6",
3
+ "version": "18.0.7",
4
4
  "description": "The ez-saga project is a project that imitates dva-js",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./vite": "./lib/vite/index.js",
10
+ "./webpack": "./lib/webpack/index.js",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "typesVersions": {
14
+ "*": {
15
+ "vite": [
16
+ "lib/vite/index.d.ts"
17
+ ],
18
+ "webpack": [
19
+ "lib/webpack/index.d.ts"
20
+ ]
21
+ }
22
+ },
7
23
  "sideEffects": false,
8
24
  "repository": {
9
25
  "type": "git",
@@ -29,6 +45,7 @@
29
45
  },
30
46
  "dependencies": {
31
47
  "@reduxjs/toolkit": "^2.11.2",
48
+ "magic-string": "^0.30.21",
32
49
  "redux": "^5.0.1",
33
50
  "redux-saga": "^1.4.2",
34
51
  "tslib": "^2.8.1"
@@ -38,7 +55,9 @@
38
55
  "eslint-plugin-node": "^11.1.0",
39
56
  "eslint-plugin-standard": "^4.1.0",
40
57
  "rimraf": "^6.1.2",
41
- "typescript": "^5.3.3"
58
+ "typescript": "^5.3.3",
59
+ "vite": "^7.3.0",
60
+ "webpack": "^5.104.1"
42
61
  },
43
62
  "engines": {
44
63
  "node": ">= 18.0.0",
@@ -1,5 +1,5 @@
1
1
  import { configureStore, createSlice, combineReducers } from '@reduxjs/toolkit';
2
- import createSagaMiddleware, { SagaMiddleware } from 'redux-saga';
2
+ import createSagaMiddleware, { SagaMiddleware, Task } from 'redux-saga';
3
3
  import { call, put, select, takeEvery, putResolve } from 'redux-saga/effects';
4
4
  import { Reducer, Action, Store } from 'redux';
5
5
  import { ReduxModel, ReduxApp, RegistedModel, ReduxSagaModel, EffectTool, PayloadAction, Effect } from './typeDeclare';
@@ -41,18 +41,11 @@ function getRegistModelFunc(
41
41
  store: Store<any, Action<string>>,
42
42
  registedModel: RegistedModel,
43
43
  allReducers: { [x: string]: Reducer<any, any>; },
44
- sagaMiddleware: SagaMiddleware<object>
44
+ sagaMiddleware: SagaMiddleware<object>,
45
+ runningEffects: Record<string, Task[]>
45
46
  ): (model: ReduxModel) => void {
46
- return function regist(reduxModel: ReduxModel): void {
47
- const model = {
48
- ...reduxModel,
49
- initialState: {}
50
- } as ReduxSagaModel;
51
-
52
- if (registedModel[model.name]) {
53
- return;
54
- }
55
-
47
+ // 提取的公共模型安装逻辑
48
+ function installModel(model: ReduxSagaModel) {
56
49
  // 初始化模型属性
57
50
  if (!model.state) model.state = {};
58
51
  model.initialState = model.state;
@@ -77,15 +70,49 @@ function getRegistModelFunc(
77
70
  store.replaceReducer(newReducer);
78
71
 
79
72
  // 注册 Effects
73
+ runningEffects[model.name] = [];
80
74
  for (const effectKey in model.effects) {
81
75
  const type = `${model.name}/${effectKey}`;
76
+ console.log('[ez-saga] regist effect:', type);
82
77
  const effectFunc = model.effects[effectKey];
83
-
84
- sagaMiddleware.run(function* () {
78
+ const task = sagaMiddleware.run(function* () {
85
79
  // 使用 takeEvery 的参数传递功能,将上下文传入 handleEffect
86
80
  // handleEffect(modelName, effectFunc, tools, action)
87
81
  yield takeEvery(type, handleEffect, model.name, effectFunc, opFun);
88
82
  });
83
+ runningEffects[model.name].push(task);
84
+ }
85
+ }
86
+
87
+ return function regist(reduxModel: ReduxModel): void {
88
+ const model = {
89
+ ...reduxModel,
90
+ initialState: {}
91
+ } as ReduxSagaModel;
92
+
93
+ if (registedModel[model.name]) {
94
+ return;
95
+ }
96
+
97
+ // 安装模型
98
+ installModel(model);
99
+
100
+ // 开发环境:注册热更新事件监听
101
+ if (process.env.NODE_ENV !== 'production' && typeof window !== 'undefined') {
102
+ const eventName = `EZ_SAGA_UPDATE_${model.name}`;
103
+ const hmrHandler = (e: any) => {
104
+ const newModel = e.detail as ReduxSagaModel;
105
+ console.log('[ez-saga] Hot updating model:', newModel.name);
106
+ // 1. 取消旧任务
107
+ const tasks = runningEffects[model.name];
108
+ if (tasks && tasks.length) {
109
+ tasks.forEach(task => task.cancel());
110
+ }
111
+ runningEffects[model.name] = [];
112
+ // 2. 安装新模型 (包含更新 Reducer 和重启 Effects)
113
+ installModel(newModel);
114
+ };
115
+ window.addEventListener(eventName, hmrHandler);
89
116
  }
90
117
  };
91
118
  }
@@ -112,7 +139,10 @@ export default function create(): ReduxApp {
112
139
  preloadedState: {}
113
140
  });
114
141
 
115
- const regist = getRegistModelFunc(store, registedModel, allReducers, sagaMiddleware);
142
+ // 存储运行中的 Effect 任务,用于热更新取消
143
+ const runningEffects: Record<string, Task[]> = {};
144
+
145
+ const regist = getRegistModelFunc(store, registedModel, allReducers, sagaMiddleware, runningEffects);
116
146
 
117
147
  return {
118
148
  store,
@@ -69,7 +69,7 @@ export interface RegistedModel {
69
69
  export interface ReduxApp {
70
70
  store: Store<any, ReduxAction>,
71
71
  sagaMiddleware: SagaMiddleware<object>;
72
- regist: (model: ReduxModel) => void
72
+ regist: (model: ReduxModel) => void;
73
73
  }
74
74
 
75
75
 
@@ -0,0 +1,63 @@
1
+ import { Plugin } from 'vite';
2
+ import MagicString from 'magic-string';
3
+
4
+ /**
5
+ * ez-saga Vite 插件
6
+ *
7
+ * 自动为 ez-saga model 文件注入 HMR 代码。
8
+ * 约定:
9
+ * 1. Model 文件必须使用 export default 导出对象
10
+ * 2. 导出的对象必须包含 'name' 属性 (作为 Model 的唯一标识)
11
+ * 3. 项目中必须将 app 实例挂载到 window.app
12
+ */
13
+
14
+ export default function ezSagaHmr(): Plugin {
15
+ return {
16
+ name: 'vite-plugin-ez-saga-hmr',
17
+ apply: 'serve', // 仅在开发环境(serve)模式下应用
18
+ transform(code, id) {
19
+ // 过滤掉 node_modules 和非 JS/TS 文件
20
+ if (id.includes('node_modules') || !/\.(js|ts|tsx|jsx)$/.test(id)) {
21
+ return;
22
+ }
23
+ // 简单的特征检测:检查是否包含 ez-saga model 的关键属性
24
+ // 必须包含 name 属性,且通常包含 effects 或 reducers 或 state
25
+ // 宽容匹配:
26
+ // 1. export default ... (可能是对象字面量,也可能是变量)
27
+ // 2. name: ... (可能是字符串字面量,也可能是变量)
28
+ const hasDefaultExport = /export\s+default/.test(code);
29
+ const hasNameProp = /name\s*:/.test(code);
30
+ const hasModelKeywords = /(effects|reducers|state|initialState)\s*:\s*\{/.test(code);
31
+
32
+ if (hasDefaultExport && hasNameProp && hasModelKeywords) {
33
+ // console.log('ez-saga HMR: Injecting HMR code into', id);
34
+ // 注入 HMR 代码
35
+ // 使用 window 事件通信,解耦 app 实例引用
36
+ const hmrCode = `
37
+ if (import.meta.hot) {
38
+ import.meta.hot.accept((newModule) => {
39
+ if (newModule && newModule.default) {
40
+ const model = newModule.default;
41
+ if (model.name) {
42
+ // console.log('[ez-saga] Hot updating model:', model.name);
43
+ if (window && window.dispatchEvent) {
44
+ window.dispatchEvent(new CustomEvent('EZ_SAGA_UPDATE_' + model.name, {
45
+ detail: model
46
+ }));
47
+ }
48
+ }
49
+ }
50
+ });
51
+ }
52
+ `;
53
+ const s = new MagicString(code);
54
+ s.append(hmrCode);
55
+
56
+ return {
57
+ code: s.toString(),
58
+ map: s.generateMap({ hires: true })
59
+ };
60
+ }
61
+ }
62
+ };
63
+ }
@@ -0,0 +1,31 @@
1
+ import { Compiler } from 'webpack';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ // ESM 环境下模拟 __dirname
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ /**
10
+ * ez-saga Webpack Plugin (Webpack 5+)
11
+ *
12
+ * 自动配置 ez-saga-loader,实现 Model 热更新。
13
+ */
14
+ export default class EzSagaWebpackPlugin {
15
+ apply(compiler: Compiler) {
16
+ // 获取 loader 的绝对路径
17
+ const loaderPath = path.resolve(__dirname, './loader.js');
18
+
19
+ compiler.hooks.afterEnvironment.tap('EzSagaWebpackPlugin', () => {
20
+ // 注入 loader规则
21
+ compiler.options.module.rules.push({
22
+ test: /\.(js|ts|tsx|jsx)$/,
23
+ exclude: /node_modules/,
24
+ enforce: 'post', // 在编译后执行,避免影响 ts-loader 输入
25
+ use: [{
26
+ loader: loaderPath
27
+ }]
28
+ });
29
+ });
30
+ }
31
+ }
@@ -0,0 +1,72 @@
1
+ import MagicString from 'magic-string';
2
+
3
+ /**
4
+ * ez-saga Webpack Loader
5
+ * 对应 src/vite/index.ts 的逻辑,用于 Webpack 场景
6
+ */
7
+ export default function (this: any, source: string) {
8
+ // 简单的特征检测
9
+ const hasDefaultExport = /export\s+default/.test(source) || /exports\.default\s*=/.test(source) || /module\.exports\s*=/.test(source);
10
+ // name属性检查 (宽松匹配: name: "foo" 或 name = "foo")
11
+ const hasNameProp = /(name\s*[:=])/.test(source);
12
+ // 关键字检查 (Loose due to compilation)
13
+ const hasModelKeywords = /(effects|reducers|state|initialState)\s*[:=]\s*\{/.test(source);
14
+
15
+ if (hasDefaultExport && hasNameProp && hasModelKeywords) {
16
+ const callback = this.async();
17
+ const s = new MagicString(source);
18
+
19
+ // Regular expressions to detect export patterns
20
+ const esmRegex = /export\s+default/;
21
+ const cjsDefaultRegex = /exports\.default\s*=/;
22
+ const cjsModuleRegex = /module\.exports\s*=/;
23
+
24
+ let match;
25
+ const modelVarName = '__ez_saga_model_def';
26
+
27
+ if ((match = esmRegex.exec(source)) !== null) {
28
+ // ESM: export default { ... }
29
+ const start = match.index;
30
+ const end = start + match[0].length;
31
+ s.overwrite(start, end, `const ${modelVarName} =`);
32
+ s.append(`\nexport default ${modelVarName};\n`);
33
+ } else if ((match = cjsDefaultRegex.exec(source)) !== null) {
34
+ // CJS: exports.default = { ... }
35
+ const start = match.index;
36
+ const end = start + match[0].length;
37
+ s.overwrite(start, end, `const ${modelVarName} =`);
38
+ s.append(`\nexports.default = ${modelVarName};\n`);
39
+ } else if ((match = cjsModuleRegex.exec(source)) !== null) {
40
+ // CJS: module.exports = { ... }
41
+ const start = match.index;
42
+ const end = start + match[0].length;
43
+ s.overwrite(start, end, `const ${modelVarName} =`);
44
+ s.append(`\nmodule.exports = ${modelVarName};\n`);
45
+ } else {
46
+ // 匹配失败
47
+ this.callback(null, source);
48
+ return;
49
+ }
50
+
51
+ // 注入 HMR 逻辑
52
+ const hmrCode = `
53
+ if (module.hot) {
54
+ module.hot.accept();
55
+ if (${modelVarName} && ${modelVarName}.name) {
56
+ if (typeof window !== 'undefined' && window.dispatchEvent) {
57
+ window.dispatchEvent(new CustomEvent('EZ_SAGA_UPDATE_' + ${modelVarName}.name, {
58
+ detail: ${modelVarName}
59
+ }));
60
+ }
61
+ }
62
+ }
63
+ `;
64
+ s.append(hmrCode);
65
+
66
+ // 禁用 SourceMap
67
+ callback(null, s.toString());
68
+ return;
69
+ }
70
+
71
+ this.callback(null, source);
72
+ }
package/tsconfig.json CHANGED
@@ -18,7 +18,8 @@
18
18
  },
19
19
  "declaration": true,
20
20
  "outDir": "lib",
21
- "allowSyntheticDefaultImports": true
21
+ "allowSyntheticDefaultImports": true,
22
+ "skipLibCheck": true
22
23
  },
23
24
  "include": [
24
25
  "src/**/*",