@nocobase/flow-engine 2.0.0-beta.8 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/lib/BlockScopedFlowEngine.js +0 -1
  2. package/lib/FlowDefinition.d.ts +2 -0
  3. package/lib/JSRunner.d.ts +6 -0
  4. package/lib/JSRunner.js +32 -2
  5. package/lib/ViewScopedFlowEngine.js +3 -0
  6. package/lib/acl/Acl.js +13 -3
  7. package/lib/components/FlowContextSelector.js +155 -10
  8. package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
  9. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -15
  10. package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +24 -4
  11. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +5 -1
  12. package/lib/components/variables/VariableInput.js +9 -4
  13. package/lib/components/variables/VariableTag.js +46 -39
  14. package/lib/components/variables/utils.d.ts +7 -0
  15. package/lib/components/variables/utils.js +42 -2
  16. package/lib/data-source/index.d.ts +7 -27
  17. package/lib/data-source/index.js +81 -51
  18. package/lib/executor/FlowExecutor.d.ts +2 -1
  19. package/lib/executor/FlowExecutor.js +163 -22
  20. package/lib/flowContext.d.ts +230 -7
  21. package/lib/flowContext.js +2267 -148
  22. package/lib/flowEngine.d.ts +21 -0
  23. package/lib/flowEngine.js +56 -8
  24. package/lib/flowI18n.js +6 -4
  25. package/lib/flowSettings.js +17 -11
  26. package/lib/index.d.ts +7 -1
  27. package/lib/index.js +21 -0
  28. package/lib/locale/en-US.json +9 -2
  29. package/lib/locale/index.d.ts +14 -0
  30. package/lib/locale/zh-CN.json +8 -1
  31. package/lib/models/CollectionFieldModel.d.ts +1 -0
  32. package/lib/models/CollectionFieldModel.js +3 -2
  33. package/lib/models/flowModel.js +12 -1
  34. package/lib/provider.js +5 -5
  35. package/lib/resources/baseRecordResource.d.ts +5 -0
  36. package/lib/resources/baseRecordResource.js +24 -0
  37. package/lib/resources/multiRecordResource.d.ts +1 -0
  38. package/lib/resources/multiRecordResource.js +11 -4
  39. package/lib/resources/singleRecordResource.js +2 -0
  40. package/lib/resources/sqlResource.d.ts +4 -3
  41. package/lib/resources/sqlResource.js +8 -3
  42. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +12 -2
  43. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +2 -2
  44. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.d.ts +16 -0
  45. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +125 -0
  46. package/lib/runjs-context/contexts/JSItemRunJSContext.js +12 -2
  47. package/lib/runjs-context/contexts/base.js +706 -41
  48. package/lib/runjs-context/contributions.d.ts +33 -0
  49. package/lib/runjs-context/contributions.js +88 -0
  50. package/lib/runjs-context/helpers.js +12 -1
  51. package/lib/runjs-context/setup.js +6 -0
  52. package/lib/runjs-context/snippets/global/api-request.snippet.js +3 -3
  53. package/lib/runjs-context/snippets/global/import-esm.snippet.js +2 -3
  54. package/lib/runjs-context/snippets/global/query-selector.snippet.js +8 -3
  55. package/lib/runjs-context/snippets/global/require-amd.snippet.js +1 -1
  56. package/lib/runjs-context/snippets/index.d.ts +11 -1
  57. package/lib/runjs-context/snippets/index.js +61 -40
  58. package/lib/runjs-context/snippets/scene/block/add-event-listener.snippet.js +10 -7
  59. package/lib/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.js +3 -3
  60. package/lib/runjs-context/snippets/scene/block/chartjs-bar.snippet.js +2 -2
  61. package/lib/runjs-context/snippets/scene/block/echarts-init.snippet.js +2 -2
  62. package/lib/runjs-context/snippets/scene/block/render-iframe.snippet.js +2 -2
  63. package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +1 -1
  64. package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +1 -1
  65. package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +1 -1
  66. package/lib/runjs-context/snippets/scene/block/resource-example.snippet.js +5 -5
  67. package/lib/runjs-context/snippets/scene/block/three-users-orbit.snippet.js +6 -6
  68. package/lib/runjs-context/snippets/scene/block/vue-component.snippet.js +3 -4
  69. package/lib/runjs-context/snippets/scene/detail/color-by-value.snippet.js +1 -1
  70. package/lib/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.js +20 -3
  71. package/lib/runjs-context/snippets/scene/detail/format-number.snippet.js +1 -1
  72. package/lib/runjs-context/snippets/scene/detail/innerHTML-value.snippet.js +1 -1
  73. package/lib/runjs-context/snippets/scene/detail/percentage-bar.snippet.js +3 -3
  74. package/lib/runjs-context/snippets/scene/detail/relative-time.snippet.js +3 -3
  75. package/lib/runjs-context/snippets/scene/detail/status-tag.snippet.js +2 -2
  76. package/lib/runjs-context/snippets/scene/form/cascade-select.snippet.js +1 -1
  77. package/lib/runjs-context/snippets/scene/form/render-basic.snippet.js +2 -2
  78. package/lib/runjs-context/snippets/scene/table/cell-open-dialog.snippet.js +6 -3
  79. package/lib/runjs-context/snippets/scene/table/concat-fields.snippet.js +3 -1
  80. package/lib/runjsLibs.d.ts +28 -0
  81. package/lib/runjsLibs.js +532 -0
  82. package/lib/scheduler/ModelOperationScheduler.d.ts +2 -0
  83. package/lib/scheduler/ModelOperationScheduler.js +25 -21
  84. package/lib/types.d.ts +27 -0
  85. package/lib/utils/associationObjectVariable.d.ts +2 -2
  86. package/lib/utils/createCollectionContextMeta.js +1 -0
  87. package/lib/utils/createEphemeralContext.js +2 -2
  88. package/lib/utils/dateVariable.d.ts +16 -0
  89. package/lib/utils/dateVariable.js +380 -0
  90. package/lib/utils/exceptions.d.ts +7 -0
  91. package/lib/utils/exceptions.js +10 -0
  92. package/lib/utils/index.d.ts +8 -3
  93. package/lib/utils/index.js +45 -0
  94. package/lib/utils/params-resolvers.js +16 -9
  95. package/lib/utils/resolveModuleUrl.d.ts +58 -0
  96. package/lib/utils/resolveModuleUrl.js +65 -0
  97. package/lib/utils/resolveRunJSObjectValues.d.ts +16 -0
  98. package/lib/utils/resolveRunJSObjectValues.js +61 -0
  99. package/lib/utils/runjsModuleLoader.d.ts +58 -0
  100. package/lib/utils/runjsModuleLoader.js +422 -0
  101. package/lib/utils/runjsTemplateCompat.d.ts +35 -0
  102. package/lib/utils/runjsTemplateCompat.js +743 -0
  103. package/lib/utils/runjsValue.d.ts +29 -0
  104. package/lib/utils/runjsValue.js +275 -0
  105. package/lib/utils/safeGlobals.d.ts +18 -8
  106. package/lib/utils/safeGlobals.js +164 -17
  107. package/lib/utils/schema-utils.d.ts +10 -0
  108. package/lib/utils/schema-utils.js +61 -0
  109. package/lib/views/createViewMeta.d.ts +0 -7
  110. package/lib/views/createViewMeta.js +19 -70
  111. package/lib/views/index.d.ts +1 -2
  112. package/lib/views/index.js +4 -3
  113. package/lib/views/useDialog.js +7 -2
  114. package/lib/views/useDrawer.js +7 -2
  115. package/lib/views/usePage.d.ts +4 -0
  116. package/lib/views/usePage.js +43 -6
  117. package/lib/views/usePopover.js +4 -1
  118. package/lib/views/viewEvents.d.ts +17 -0
  119. package/lib/views/viewEvents.js +90 -0
  120. package/package.json +4 -4
  121. package/src/BlockScopedFlowEngine.ts +2 -5
  122. package/src/JSRunner.ts +44 -2
  123. package/src/ViewScopedFlowEngine.ts +4 -0
  124. package/src/__tests__/JSRunner.test.ts +64 -0
  125. package/src/__tests__/createViewMeta.popup.test.ts +62 -1
  126. package/src/__tests__/flowContext.test.ts +693 -1
  127. package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
  128. package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
  129. package/src/__tests__/flowRunJSContextDefine.test.ts +63 -0
  130. package/src/__tests__/flowRuntimeContext.test.ts +2 -1
  131. package/src/__tests__/flowSettings.open.test.tsx +123 -19
  132. package/src/__tests__/runjsContext.test.ts +10 -7
  133. package/src/__tests__/runjsContextImplementations.test.ts +34 -3
  134. package/src/__tests__/runjsContextRuntime.test.ts +3 -3
  135. package/src/__tests__/runjsContributions.test.ts +89 -0
  136. package/src/__tests__/runjsExternalLibs.test.ts +242 -0
  137. package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
  138. package/src/__tests__/runjsLocales.test.ts +4 -1
  139. package/src/__tests__/runjsPreprocessDefault.test.ts +49 -0
  140. package/src/__tests__/runjsRuntimeFeatures.test.ts +166 -0
  141. package/src/__tests__/runjsSnippets.test.ts +40 -3
  142. package/src/acl/Acl.tsx +3 -3
  143. package/src/components/FlowContextSelector.tsx +208 -12
  144. package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
  145. package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
  146. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +109 -16
  147. package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +41 -7
  148. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +13 -2
  149. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +157 -5
  150. package/src/components/variables/VariableInput.tsx +12 -4
  151. package/src/components/variables/VariableTag.tsx +54 -45
  152. package/src/components/variables/__tests__/FlowContextSelector.test.tsx +260 -3
  153. package/src/components/variables/__tests__/VariableTag.test.tsx +50 -0
  154. package/src/components/variables/__tests__/utils.test.ts +81 -3
  155. package/src/components/variables/utils.ts +67 -6
  156. package/src/data-source/index.ts +85 -110
  157. package/src/executor/FlowExecutor.ts +200 -23
  158. package/src/executor/__tests__/flowExecutor.test.ts +66 -0
  159. package/src/flowContext.ts +2986 -211
  160. package/src/flowEngine.ts +59 -8
  161. package/src/flowI18n.ts +7 -5
  162. package/src/flowSettings.ts +18 -12
  163. package/src/index.ts +14 -1
  164. package/src/locale/en-US.json +9 -2
  165. package/src/locale/zh-CN.json +8 -1
  166. package/src/models/CollectionFieldModel.tsx +3 -1
  167. package/src/models/__tests__/dispatchEvent.when.test.ts +554 -0
  168. package/src/models/__tests__/flowModel.test.ts +20 -4
  169. package/src/models/flowModel.tsx +13 -1
  170. package/src/provider.tsx +7 -6
  171. package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
  172. package/src/resources/__tests__/sqlResource.test.ts +60 -0
  173. package/src/resources/baseRecordResource.ts +31 -0
  174. package/src/resources/multiRecordResource.ts +11 -4
  175. package/src/resources/singleRecordResource.ts +3 -0
  176. package/src/resources/sqlResource.ts +11 -6
  177. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +10 -0
  178. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +6 -2
  179. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +106 -0
  180. package/src/runjs-context/contexts/JSItemRunJSContext.ts +10 -0
  181. package/src/runjs-context/contexts/base.ts +715 -44
  182. package/src/runjs-context/contributions.ts +88 -0
  183. package/src/runjs-context/helpers.ts +11 -1
  184. package/src/runjs-context/setup.ts +6 -0
  185. package/src/runjs-context/snippets/global/api-request.snippet.ts +3 -3
  186. package/src/runjs-context/snippets/global/import-esm.snippet.ts +2 -3
  187. package/src/runjs-context/snippets/global/query-selector.snippet.ts +8 -3
  188. package/src/runjs-context/snippets/global/require-amd.snippet.ts +1 -1
  189. package/src/runjs-context/snippets/index.ts +75 -41
  190. package/src/runjs-context/snippets/scene/block/add-event-listener.snippet.ts +11 -13
  191. package/src/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.ts +3 -3
  192. package/src/runjs-context/snippets/scene/block/chartjs-bar.snippet.ts +2 -2
  193. package/src/runjs-context/snippets/scene/block/echarts-init.snippet.ts +2 -2
  194. package/src/runjs-context/snippets/scene/block/render-iframe.snippet.ts +2 -2
  195. package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +1 -1
  196. package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +1 -1
  197. package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +1 -1
  198. package/src/runjs-context/snippets/scene/block/resource-example.snippet.ts +6 -11
  199. package/src/runjs-context/snippets/scene/block/three-users-orbit.snippet.ts +6 -6
  200. package/src/runjs-context/snippets/scene/block/vue-component.snippet.ts +3 -4
  201. package/src/runjs-context/snippets/scene/detail/color-by-value.snippet.ts +1 -1
  202. package/src/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.ts +20 -3
  203. package/src/runjs-context/snippets/scene/detail/format-number.snippet.ts +1 -1
  204. package/src/runjs-context/snippets/scene/detail/innerHTML-value.snippet.ts +1 -1
  205. package/src/runjs-context/snippets/scene/detail/percentage-bar.snippet.ts +3 -3
  206. package/src/runjs-context/snippets/scene/detail/relative-time.snippet.ts +3 -3
  207. package/src/runjs-context/snippets/scene/detail/status-tag.snippet.ts +2 -2
  208. package/src/runjs-context/snippets/scene/form/cascade-select.snippet.ts +1 -1
  209. package/src/runjs-context/snippets/scene/form/render-basic.snippet.ts +3 -8
  210. package/src/runjs-context/snippets/scene/table/cell-open-dialog.snippet.ts +6 -3
  211. package/src/runjs-context/snippets/scene/table/concat-fields.snippet.ts +3 -1
  212. package/src/runjsLibs.ts +622 -0
  213. package/src/scheduler/ModelOperationScheduler.ts +27 -21
  214. package/src/types.ts +38 -1
  215. package/src/utils/__tests__/dateVariable.test.ts +101 -0
  216. package/src/utils/__tests__/params-resolvers.test.ts +40 -0
  217. package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
  218. package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
  219. package/src/utils/__tests__/runjsValue.test.ts +44 -0
  220. package/src/utils/__tests__/safeGlobals.test.ts +57 -2
  221. package/src/utils/__tests__/utils.test.ts +95 -0
  222. package/src/utils/associationObjectVariable.ts +2 -2
  223. package/src/utils/createCollectionContextMeta.ts +1 -0
  224. package/src/utils/createEphemeralContext.ts +5 -4
  225. package/src/utils/dateVariable.ts +397 -0
  226. package/src/utils/exceptions.ts +11 -0
  227. package/src/utils/index.ts +37 -3
  228. package/src/utils/params-resolvers.ts +23 -9
  229. package/src/utils/resolveModuleUrl.ts +91 -0
  230. package/src/utils/resolveRunJSObjectValues.ts +46 -0
  231. package/src/utils/runjsModuleLoader.ts +553 -0
  232. package/src/utils/runjsTemplateCompat.ts +828 -0
  233. package/src/utils/runjsValue.ts +287 -0
  234. package/src/utils/safeGlobals.ts +188 -17
  235. package/src/utils/schema-utils.ts +79 -0
  236. package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
  237. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +35 -8
  238. package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
  239. package/src/views/createViewMeta.ts +22 -75
  240. package/src/views/index.tsx +1 -2
  241. package/src/views/useDialog.tsx +8 -1
  242. package/src/views/useDrawer.tsx +8 -1
  243. package/src/views/usePage.tsx +51 -5
  244. package/src/views/usePopover.tsx +4 -1
  245. package/src/views/viewEvents.ts +55 -0
@@ -10,6 +10,7 @@
10
10
  import { uid as genUid } from 'uid/secure';
11
11
  import type { FlowEngine } from '../flowEngine';
12
12
  import type { FlowModel } from '../models/flowModel';
13
+ import { FlowExitAllException } from '../utils/exceptions';
13
14
 
14
15
  type LifecycleType =
15
16
  | 'created'
@@ -36,6 +37,8 @@ export interface LifecycleEvent {
36
37
  error?: any;
37
38
  inputArgs?: Record<string, any>;
38
39
  result?: any;
40
+ flowKey?: string;
41
+ stepKey?: string;
39
42
  }
40
43
 
41
44
  type ScheduledItem = {
@@ -162,37 +165,37 @@ export class ModelOperationScheduler {
162
165
  const emitter = this.engine.emitter;
163
166
  if (!emitter || typeof emitter.on !== 'function') return;
164
167
 
165
- const onCreated = (e: LifecycleEvent) => {
166
- this.processLifecycleEvent(e.uid, { ...e, type: 'created' });
168
+ const onCreated = async (e: LifecycleEvent) => {
169
+ await this.processLifecycleEvent(e.uid, { ...e, type: 'created' });
167
170
  };
168
171
  emitter.on('model:created', onCreated);
169
172
  this.unbindHandlers.push(() => emitter.off('model:created', onCreated));
170
173
 
171
- const onMounted = (e: LifecycleEvent) => {
172
- this.processLifecycleEvent(e.uid, { ...e, type: 'mounted' });
174
+ const onMounted = async (e: LifecycleEvent) => {
175
+ await this.processLifecycleEvent(e.uid, { ...e, type: 'mounted' });
173
176
  };
174
177
  emitter.on('model:mounted', onMounted);
175
178
  this.unbindHandlers.push(() => emitter.off('model:mounted', onMounted));
176
179
 
177
- const onGenericBeforeStart = (e: LifecycleEvent) => {
178
- this.processLifecycleEvent(e.uid, { ...e, type: 'event:beforeRender:start' });
180
+ const onGenericBeforeStart = async (e: LifecycleEvent) => {
181
+ await this.processLifecycleEvent(e.uid, { ...e, type: 'event:beforeRender:start' });
179
182
  };
180
183
  emitter.on('model:event:beforeRender:start', onGenericBeforeStart);
181
184
  this.unbindHandlers.push(() => emitter.off('model:event:beforeRender:start', onGenericBeforeStart));
182
185
 
183
- const onGenericBeforeEnd = (e: LifecycleEvent) => {
184
- this.processLifecycleEvent(e.uid, { ...e, type: 'event:beforeRender:end' });
186
+ const onGenericBeforeEnd = async (e: LifecycleEvent) => {
187
+ await this.processLifecycleEvent(e.uid, { ...e, type: 'event:beforeRender:end' });
185
188
  };
186
189
  emitter.on('model:event:beforeRender:end', onGenericBeforeEnd);
187
190
  this.unbindHandlers.push(() => emitter.off('model:event:beforeRender:end', onGenericBeforeEnd));
188
191
 
189
- const onUnmounted = (e: LifecycleEvent) => {
190
- this.processLifecycleEvent(e.uid, { ...e, type: 'unmounted' });
192
+ const onUnmounted = async (e: LifecycleEvent) => {
193
+ await this.processLifecycleEvent(e.uid, { ...e, type: 'unmounted' });
191
194
  };
192
195
  emitter.on('model:unmounted', onUnmounted);
193
196
  this.unbindHandlers.push(() => emitter.off('model:unmounted', onUnmounted));
194
197
 
195
- const onDestroyed = (e: LifecycleEvent) => {
198
+ const onDestroyed = async (e: LifecycleEvent) => {
196
199
  const targetBucket = this.itemsByTargetUid.get(e.uid);
197
200
  const event = { ...e, type: 'destroyed' as const };
198
201
  if (targetBucket && targetBucket.size) {
@@ -201,7 +204,7 @@ export class ModelOperationScheduler {
201
204
  const it = this.itemsById.get(id);
202
205
  if (!it) continue;
203
206
  if (this.shouldTrigger(it.options.when, event)) {
204
- void this.tryExecuteOnce(id, event);
207
+ await this.tryExecuteOnce(id, event);
205
208
  } else {
206
209
  this.internalCancel(id);
207
210
  }
@@ -220,14 +223,14 @@ export class ModelOperationScheduler {
220
223
  if (this.subscribedEventNames.has(name)) return;
221
224
  this.subscribedEventNames.add(name);
222
225
  const emitter = this.engine.emitter;
223
- const onStart = (e: LifecycleEvent) => {
224
- this.processLifecycleEvent(e.uid, { ...e, type: `event:${name}:start` as const });
226
+ const onStart = async (e: LifecycleEvent) => {
227
+ await this.processLifecycleEvent(e.uid, { ...e, type: `event:${name}:start` as LifecycleType });
225
228
  };
226
- const onEnd = (e: LifecycleEvent) => {
227
- this.processLifecycleEvent(e.uid, { ...e, type: `event:${name}:end` as const });
229
+ const onEnd = async (e: LifecycleEvent) => {
230
+ await this.processLifecycleEvent(e.uid, { ...e, type: `event:${name}:end` as LifecycleType });
228
231
  };
229
- const onError = (e: LifecycleEvent) => {
230
- this.processLifecycleEvent(e.uid, { ...e, type: `event:${name}:error` as const });
232
+ const onError = async (e: LifecycleEvent) => {
233
+ await this.processLifecycleEvent(e.uid, { ...e, type: `event:${name}:error` as LifecycleType });
231
234
  };
232
235
  emitter.on(`model:event:${name}:start`, onStart);
233
236
  emitter.on(`model:event:${name}:end`, onEnd);
@@ -239,12 +242,12 @@ export class ModelOperationScheduler {
239
242
 
240
243
  private parseEventWhen(when?: ScheduleWhen): { name: string; phase: 'start' | 'end' | 'error' } | null {
241
244
  if (!when || typeof when !== 'string') return null;
242
- const m = /^event:([^:]+):(start|end|error)$/.exec(when);
245
+ const m = /^event:(.+):(start|end|error)$/.exec(when);
243
246
  if (!m) return null;
244
247
  return { name: m[1], phase: m[2] as 'start' | 'end' | 'error' };
245
248
  }
246
249
 
247
- private processLifecycleEvent(targetUid: string, event: LifecycleEvent) {
250
+ private async processLifecycleEvent(targetUid: string, event: LifecycleEvent) {
248
251
  const targetBucket = this.itemsByTargetUid.get(targetUid);
249
252
  if (!targetBucket || targetBucket.size === 0) return;
250
253
  const ids = Array.from(targetBucket.keys());
@@ -253,7 +256,7 @@ export class ModelOperationScheduler {
253
256
  if (!item) continue;
254
257
  const should = this.shouldTrigger(item.options.when, event);
255
258
  if (!should) continue;
256
- void this.tryExecuteOnce(id, event);
259
+ await this.tryExecuteOnce(id, event);
257
260
  }
258
261
  }
259
262
 
@@ -271,6 +274,9 @@ export class ModelOperationScheduler {
271
274
  if (!model) return;
272
275
  await Promise.resolve(item.fn(model));
273
276
  } catch (err) {
277
+ if (err instanceof FlowExitAllException) {
278
+ throw err;
279
+ }
274
280
  this.engine.logger?.error?.(
275
281
  { err, id, fromUid: item.fromUid, toUid: item.toUid, when: item.options.when },
276
282
  'ModelOperationScheduler: operation execution failed',
package/src/types.ts CHANGED
@@ -172,6 +172,18 @@ export interface ActionDefinition<TModel extends FlowModel = FlowModel, TCtx ext
172
172
  * - StepDefinition.hideInSettings can override the ActionDefinition value.
173
173
  */
174
174
  hideInSettings?: boolean | ((ctx: TCtx) => boolean | Promise<boolean>);
175
+ /**
176
+ * Whether to disable this step/action in settings menus.
177
+ * - Supports static boolean and dynamic decision based on runtime context.
178
+ * - StepDefinition.disabledInSettings can override the ActionDefinition value.
179
+ */
180
+ disabledInSettings?: boolean | ((ctx: TCtx) => boolean | Promise<boolean>);
181
+ /**
182
+ * Optional reason shown when this step/action is disabled in settings menus.
183
+ * - Supports static string and dynamic resolver based on runtime context.
184
+ * - StepDefinition.disabledReasonInSettings can override the ActionDefinition value.
185
+ */
186
+ disabledReasonInSettings?: string | ((ctx: TCtx) => string | Promise<string>);
175
187
  /**
176
188
  * 在执行 Action 前为 ctx 定义临时属性。
177
189
  * - 仅支持 PropertyOptions 形态(例如:{ foo: { value: 5 } });
@@ -213,12 +225,37 @@ export type FlowEventName =
213
225
  // fallback to any string for extensibility
214
226
  | (string & {});
215
227
 
228
+ /**
229
+ * 事件流的执行时机(phase)。
230
+ *
231
+ * 说明:
232
+ * - 缺省(phase 未配置)表示保持现有行为;
233
+ * - 当配置了 phase 时,运行时会将其映射为 `scheduleModelOperation` 的 `when` 锚点;
234
+ * - phase 同时适用于动态事件流(实例级)与静态流(内置)。
235
+ */
236
+ export type FlowEventPhase =
237
+ | 'beforeAllFlows'
238
+ | 'afterAllFlows'
239
+ | 'beforeFlow'
240
+ | 'afterFlow'
241
+ | 'beforeStep'
242
+ | 'afterStep';
243
+
216
244
  /**
217
245
  * Flow 事件类型(供 FlowDefinitionOptions.on 使用)。
218
246
  */
219
247
  export type FlowEvent<TModel extends FlowModel = FlowModel> =
220
248
  | FlowEventName
221
- | { eventName: FlowEventName; defaultParams?: Record<string, any> };
249
+ | {
250
+ eventName: FlowEventName;
251
+ defaultParams?: Record<string, any>;
252
+ /** 动态事件流的执行时机(默认 beforeAllFlows) */
253
+ phase?: FlowEventPhase;
254
+ /** phase 为 beforeFlow/afterFlow/beforeStep/afterStep 时使用 */
255
+ flowKey?: string;
256
+ /** phase 为 beforeStep/afterStep 时使用 */
257
+ stepKey?: string;
258
+ };
222
259
 
223
260
  /**
224
261
  * 事件分发选项。
@@ -0,0 +1,101 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { describe, expect, it } from 'vitest';
11
+ import {
12
+ decodeBase64Url,
13
+ encodeBase64Url,
14
+ isCompleteCtxDatePath,
15
+ isCtxDateExpression,
16
+ parseCtxDateExpression,
17
+ resolveCtxDatePath,
18
+ serializeCtxDateValue,
19
+ } from '../dateVariable';
20
+
21
+ describe('dateVariable utils', () => {
22
+ it('encodes and decodes base64url', () => {
23
+ const raw = '2026-02-12 10:11:12';
24
+ const encoded = encodeBase64Url(raw);
25
+ const decoded = decodeBase64Url(encoded);
26
+ expect(decoded).toBe(raw);
27
+ });
28
+
29
+ it('detects ctx.date expression', () => {
30
+ expect(isCtxDateExpression('{{ ctx.date.preset.today }}')).toBe(true);
31
+ expect(isCtxDateExpression('{{ ctx.user.name }}')).toBe(false);
32
+ expect(isCtxDateExpression('')).toBe(false);
33
+ });
34
+
35
+ it('serializes preset/relative/exact single/range', () => {
36
+ expect(serializeCtxDateValue({ type: 'today' })).toBe('{{ ctx.date.preset.today }}');
37
+ expect(serializeCtxDateValue({ type: 'next', unit: 'day', number: 12 })).toBe(
38
+ '{{ ctx.date.relative.next.day.n12 }}',
39
+ );
40
+
41
+ const single = serializeCtxDateValue('2026-02-12');
42
+ expect(single?.startsWith('{{ ctx.date.exact.single.date.v')).toBe(true);
43
+
44
+ const range = serializeCtxDateValue(['2026-02-12', '2026-02-20']);
45
+ expect(range?.startsWith('{{ ctx.date.exact.range.date.v')).toBe(true);
46
+ expect(range?.includes('.v')).toBe(true);
47
+ });
48
+
49
+ it('parses expression back to ui value', () => {
50
+ expect(parseCtxDateExpression('{{ ctx.date.preset.today }}')).toEqual({ type: 'today' });
51
+ expect(parseCtxDateExpression('{{ ctx.date.relative.past.month.n2 }}')).toEqual({
52
+ type: 'past',
53
+ unit: 'month',
54
+ number: 2,
55
+ });
56
+
57
+ const singleExpr = serializeCtxDateValue('2026-02-12')!;
58
+ expect(parseCtxDateExpression(singleExpr)).toBe('2026-02-12');
59
+
60
+ const rangeExpr = serializeCtxDateValue(['2026-02-12', '2026-02-20'])!;
61
+ expect(parseCtxDateExpression(rangeExpr)).toEqual(['2026-02-12', '2026-02-20']);
62
+ });
63
+
64
+ it('resolves preset/relative/exact path', () => {
65
+ expect(typeof resolveCtxDatePath(['date', 'preset', 'now'])).toBe('string');
66
+
67
+ const today = resolveCtxDatePath(['date', 'preset', 'today']);
68
+ expect(typeof today).toBe('string');
69
+ expect(today).toMatch(/^\d{4}-\d{2}-\d{2}$/);
70
+
71
+ const rel = resolveCtxDatePath(['date', 'relative', 'next', 'day', 'n12']);
72
+ expect(typeof rel).toBe('string');
73
+ expect(rel).toMatch(/^\d{4}-\d{2}-\d{2}$/);
74
+
75
+ const singleExpr = serializeCtxDateValue('2026-02-12')!;
76
+ const token = singleExpr.replace('{{ ctx.date.exact.single.date.', '').replace(' }}', '');
77
+ expect(resolveCtxDatePath(['date', 'exact', 'single', 'date', token])).toBe('2026-02-12');
78
+
79
+ const rangeExpr = serializeCtxDateValue(['2026-02-12', '2026-02-20'])!;
80
+ const parts = rangeExpr.replace('{{ ctx.date.exact.range.date.', '').replace(' }}', '').split('.');
81
+ expect(resolveCtxDatePath(['date', 'exact', 'range', 'date', parts[0], parts[1]])).toEqual([
82
+ '2026-02-12',
83
+ '2026-02-20',
84
+ ]);
85
+ });
86
+
87
+ it('validates complete ctx.date path', () => {
88
+ expect(isCompleteCtxDatePath(['date', 'preset', 'today'])).toBe(true);
89
+ expect(isCompleteCtxDatePath(['date', 'relative', 'next', 'day', 'n12'])).toBe(true);
90
+ expect(isCompleteCtxDatePath(['date', 'exact', 'single', 'date', 'vabc'])).toBe(true);
91
+ expect(isCompleteCtxDatePath(['date', 'exact', 'range', 'date', 'vabc', 'vdef'])).toBe(true);
92
+ expect(isCompleteCtxDatePath(['date', 'relative', 'next', 'day'])).toBe(false);
93
+ expect(isCompleteCtxDatePath(['user', 'name'])).toBe(false);
94
+ });
95
+
96
+ it('handles invalid base64 and path gracefully', () => {
97
+ expect(decodeBase64Url('@@@')).toBeUndefined();
98
+ expect(parseCtxDateExpression('{{ ctx.date.exact.single.date.v@@@ }}')).toBeUndefined();
99
+ expect(resolveCtxDatePath(['date', 'exact', 'single', 'date', 'v@@@'])).toBeUndefined();
100
+ });
101
+ });
@@ -491,6 +491,46 @@ describe('resolveExpressions', () => {
491
491
  ],
492
492
  });
493
493
  });
494
+
495
+ test('should resolve dot-only path with dashed keys', async () => {
496
+ ctx.defineProperty('formValues', {
497
+ value: {
498
+ 'oho-test': {
499
+ 'o2m-users': [1, 2],
500
+ },
501
+ },
502
+ });
503
+
504
+ const params = '{{ctx.formValues.oho-test.o2m-users}}';
505
+
506
+ const result = await resolveExpressions(params, ctx);
507
+
508
+ expect(result).toEqual([1, 2]);
509
+ });
510
+
511
+ test('should resolve dashed keys inside template strings', async () => {
512
+ ctx.defineProperty('formValues', {
513
+ value: {
514
+ 'oho-test': {
515
+ 'o2m-users': 'X',
516
+ },
517
+ },
518
+ });
519
+
520
+ const params = 'prefix {{ctx.formValues.oho-test.o2m-users}} suffix';
521
+
522
+ const result = await resolveExpressions(params, ctx);
523
+
524
+ expect(result).toEqual('prefix X suffix');
525
+ });
526
+
527
+ test('should not treat subtraction as dashed key path', async () => {
528
+ const params = '{{ctx.aa.bb-ctx.cc}}';
529
+
530
+ const result = await resolveExpressions(params, ctx);
531
+
532
+ expect(result).toEqual(5);
533
+ });
494
534
  });
495
535
 
496
536
  // 测试高级功能:多表达式模板字符串
@@ -0,0 +1,38 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { beforeEach, describe, expect, it } from 'vitest';
11
+ import { runjsRequireAsync } from '../runjsModuleLoader';
12
+ import { __resetRunJSSafeGlobalsRegistryForTests, createSafeWindow } from '../safeGlobals';
13
+
14
+ beforeEach(() => {
15
+ __resetRunJSSafeGlobalsRegistryForTests();
16
+ });
17
+
18
+ describe('runjsRequireAsync auto whitelist', () => {
19
+ it('should allow safeWindow to access globals introduced during requireAsync', async () => {
20
+ const key = '__nb_require_async_added_global__';
21
+ delete (window as any)[key];
22
+
23
+ const safeWin: any = createSafeWindow();
24
+ expect(() => safeWin[key]).toThrow(/not allowed/);
25
+
26
+ const requirejs: any = (deps: string[], onLoad: (...args: any[]) => void) => {
27
+ // Simulate a remote library attaching itself to the real window.
28
+ (window as any)[key] = { ok: true };
29
+ onLoad(undefined);
30
+ };
31
+
32
+ await runjsRequireAsync(requirejs, 'https://example.com/fake-lib.js');
33
+
34
+ expect(safeWin[key]).toEqual({ ok: true });
35
+
36
+ delete (window as any)[key];
37
+ });
38
+ });
@@ -0,0 +1,159 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { describe, it, expect, vi } from 'vitest';
11
+ import * as jsxTransform from '../../utils/jsxTransform';
12
+ import { prepareRunJsCode, preprocessRunJsTemplates } from '../runjsTemplateCompat';
13
+
14
+ describe('runjsTemplateCompat', () => {
15
+ describe('preprocessRunJsTemplates', () => {
16
+ it('hoists bare {{ }} placeholders into top-level resolved vars', () => {
17
+ const src = `const a = {{ctx.user.id}};`;
18
+ const out = preprocessRunJsTemplates(src);
19
+ expect(out).toContain(`const __runjs_ctx_tpl_0 = await ctx.resolveJsonTemplate("{{ctx.user.id}}");`);
20
+ expect(out).toContain(`const a = __runjs_ctx_tpl_0;`);
21
+ });
22
+
23
+ it('replaces string literals containing {{ }} via split/join, without injecting await into nested functions', () => {
24
+ const src = `const s = '{{ctx.user.id}}';`;
25
+ const out = preprocessRunJsTemplates(src);
26
+ expect(out).toContain(`const __runjs_ctx_tpl_0 = await ctx.resolveJsonTemplate("{{ctx.user.id}}");`);
27
+ expect(out).toContain(`__runjs_templateValueToString(__runjs_ctx_tpl_0, "{{ctx.user.id}}")`);
28
+ expect(out).toContain(`.split("{{ctx.user.id}}").join(`);
29
+ });
30
+
31
+ it('does not transform arguments inside explicit ctx.resolveJsonTemplate(...) call', () => {
32
+ const src = `const v = await ctx.resolveJsonTemplate('{{ctx.user.id}}');\nconst s = '{{ctx.user.id}}';`;
33
+ const out = preprocessRunJsTemplates(src);
34
+ // inside call: keep raw
35
+ expect(out).toContain(`await ctx.resolveJsonTemplate('{{ctx.user.id}}')`);
36
+ // outside call: transform
37
+ expect(out).toContain(`__runjs_templateValueToString(__runjs_ctx_tpl_0, "{{ctx.user.id}}")`);
38
+ });
39
+
40
+ it('keeps template markers in comments unchanged', () => {
41
+ const src = `// {{ctx.user.id}}\nconst a = 1;`;
42
+ const out = preprocessRunJsTemplates(src);
43
+ expect(out).toBe(src);
44
+ });
45
+
46
+ it('supports template literals containing {{ }}', () => {
47
+ const src = 'const s = `hi {{ctx.user.name}}`;';
48
+ const out = preprocessRunJsTemplates(src);
49
+ expect(out).toContain(`const __runjs_ctx_tpl_0 = await ctx.resolveJsonTemplate("{{ctx.user.name}}");`);
50
+ expect(out).toContain(`\`hi {{ctx.user.name}}\``);
51
+ expect(out).toContain(`.split("{{ctx.user.name}}").join(`);
52
+ });
53
+
54
+ it('does not rewrite non-ctx {{ }} patterns (e.g. JSX style object) while still rewriting ctx placeholders', () => {
55
+ const src = `const id = {{ctx.user.id}};\nctx.render(<div style={{ width: '100%' }} />);`;
56
+ const out = preprocessRunJsTemplates(src);
57
+ expect(out).toContain(`const __runjs_ctx_tpl_0 = await ctx.resolveJsonTemplate("{{ctx.user.id}}");`);
58
+ expect(out).toContain(`style={{ width: '100%' }}`);
59
+ expect(out).not.toContain(`resolveJsonTemplate("{{ width: '100%' }}")`);
60
+ });
61
+
62
+ it('avoids injecting await into non-async nested function bodies', () => {
63
+ const src = `
64
+ function f() {
65
+ return {{ctx.user.id}};
66
+ }
67
+ return f();
68
+ `.trim();
69
+ const out = preprocessRunJsTemplates(src);
70
+ // The only await should be in the top-level preamble
71
+ expect(out).toContain(`const __runjs_ctx_tpl_0 = await ctx.resolveJsonTemplate("{{ctx.user.id}}");`);
72
+ expect(out).toContain(`return __runjs_ctx_tpl_0;`);
73
+ expect(out).not.toContain(`return (await ctx.resolveJsonTemplate`);
74
+ });
75
+
76
+ it('is tolerant to already-preprocessed code (idempotent heuristic)', () => {
77
+ const src =
78
+ `const __runjs_ctx_tpl_0 = await ctx.resolveJsonTemplate("{{ctx.user.id}}");\n` +
79
+ `const s = ("{{ctx.user.id}}").split("{{ctx.user.id}}").join(__runjs_templateValueToString(__runjs_ctx_tpl_0, "{{ctx.user.id}}"));`;
80
+ const out = preprocessRunJsTemplates(src);
81
+ expect(out).toBe(src);
82
+ });
83
+
84
+ it('rewrites object literal string keys via computed property to keep syntax valid', () => {
85
+ const src = `const o = { '{{ctx.user.id}}': 1 };`;
86
+ const out = preprocessRunJsTemplates(src);
87
+ expect(out).toContain(`const __runjs_ctx_tpl_0 = await ctx.resolveJsonTemplate("{{ctx.user.id}}");`);
88
+ expect(out).toContain(`{ [`);
89
+ expect(out).toContain(`]: 1 }`);
90
+ expect(out).toContain(`.split("{{ctx.user.id}}").join(`);
91
+ });
92
+ });
93
+
94
+ describe('prepareRunJsCode', () => {
95
+ it('preprocesses templates and compiles JSX', async () => {
96
+ const src = `
97
+ const name = '{{ctx.user.name}}';
98
+ ctx.render(<div className="x">{name}</div>);
99
+ `.trim();
100
+ const out = await prepareRunJsCode(src, { preprocessTemplates: true });
101
+ expect(out).toMatch(/ctx\.React\.createElement/);
102
+ expect(out).toMatch(/ctx\.resolveJsonTemplate/);
103
+ });
104
+
105
+ it('injects ctx.libs ensure preamble for member access', async () => {
106
+ const src = `return ctx.libs.lodash;`;
107
+ const out = await prepareRunJsCode(src, { preprocessTemplates: false });
108
+ expect(out).toContain(`/* __runjs_ensure_libs */`);
109
+ expect(out).toContain(`await ctx.__ensureLibs(["lodash"]);`);
110
+ });
111
+
112
+ it('injects ctx.libs ensure preamble for bracket access with string literal', async () => {
113
+ const src = `return ctx.libs['lodash'];`;
114
+ const out = await prepareRunJsCode(src, { preprocessTemplates: false });
115
+ expect(out).toContain(`await ctx.__ensureLibs(["lodash"]);`);
116
+ });
117
+
118
+ it('injects ctx.libs ensure preamble for object destructuring', async () => {
119
+ const src = `const { lodash } = ctx.libs;\nreturn lodash;`;
120
+ const out = await prepareRunJsCode(src, { preprocessTemplates: false });
121
+ expect(out).toContain(`await ctx.__ensureLibs(["lodash"]);`);
122
+ });
123
+
124
+ it('does not inject ctx.libs preamble when ctx.libs only appears in string/comment', async () => {
125
+ const src = `// ctx.libs.lodash\nconst s = "ctx.libs.lodash";\nreturn s;`;
126
+ const out = await prepareRunJsCode(src, { preprocessTemplates: false });
127
+ expect(out).not.toContain(`__runjs_ensure_libs`);
128
+ });
129
+
130
+ it('is idempotent for already-prepared code', async () => {
131
+ const src = `return ctx.libs['lodash'];`;
132
+ const out1 = await prepareRunJsCode(src, { preprocessTemplates: false });
133
+ const out2 = await prepareRunJsCode(out1, { preprocessTemplates: false });
134
+ expect(out2).toBe(out1);
135
+ expect(out2.match(/__runjs_ensure_libs/g)?.length ?? 0).toBe(1);
136
+ });
137
+
138
+ it('does not break JSX attribute string values when preprocessing templates', async () => {
139
+ const src = `ctx.render(<Input title="{{ctx.user.name}}" />);`;
140
+ const out = await prepareRunJsCode(src, { preprocessTemplates: true });
141
+ expect(out).toMatch(/ctx\.React\.createElement/);
142
+ expect(out).toMatch(/title:\s*\(/);
143
+ expect(out).toContain(`.split("{{ctx.user.name}}").join(`);
144
+ });
145
+
146
+ it('caches prepared code by source and preprocessTemplates option', async () => {
147
+ const spy = vi.spyOn(jsxTransform, 'compileRunJs');
148
+ const src = `/* cache-test */\nconst a = 1;\nreturn a;`;
149
+
150
+ await prepareRunJsCode(src, { preprocessTemplates: false });
151
+ await prepareRunJsCode(src, { preprocessTemplates: false });
152
+ await prepareRunJsCode(src, { preprocessTemplates: true });
153
+ await prepareRunJsCode(src, { preprocessTemplates: true });
154
+
155
+ expect(spy).toHaveBeenCalledTimes(2);
156
+ spy.mockRestore();
157
+ });
158
+ });
159
+ });
@@ -0,0 +1,44 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { describe, expect, it } from 'vitest';
11
+ import { extractUsedVariablePathsFromRunJS, isRunJSValue, normalizeRunJSValue } from '../runjsValue';
12
+
13
+ describe('runjsValue utils', () => {
14
+ it('isRunJSValue: strict shape detection', () => {
15
+ expect(isRunJSValue({ code: 'return 1' })).toBe(true);
16
+ expect(isRunJSValue({ code: 'return 1', version: 'v1' })).toBe(true);
17
+
18
+ expect(isRunJSValue(null)).toBe(false);
19
+ expect(isRunJSValue('return 1')).toBe(false);
20
+ expect(isRunJSValue({})).toBe(false);
21
+ expect(isRunJSValue({ version: 'v1' })).toBe(false);
22
+ expect(isRunJSValue({ code: 1 })).toBe(false);
23
+ expect(isRunJSValue({ code: 'return 1', foo: 1 })).toBe(false);
24
+ expect(isRunJSValue([])).toBe(false);
25
+ });
26
+
27
+ it('normalizeRunJSValue: defaults version to v1', () => {
28
+ expect(normalizeRunJSValue({ code: 'return 1' })).toEqual({ code: 'return 1', version: 'v1' });
29
+ expect(normalizeRunJSValue({ code: 'return 1', version: 'v2' })).toEqual({ code: 'return 1', version: 'v2' });
30
+ });
31
+
32
+ it('extractUsedVariablePathsFromRunJS: extracts ctx usage (dot + bracket root)', () => {
33
+ const code = `
34
+ // comment: ctx.ignore.me
35
+ const x = "ctx.ignore.too";
36
+ return ctx.formValues.a + ctx.record.id + ctx.someVar + ctx['user'].name;
37
+ `;
38
+ const out = extractUsedVariablePathsFromRunJS(code);
39
+ expect(out.formValues).toContain('a');
40
+ expect(out.record).toContain('id');
41
+ expect(out.someVar).toContain('');
42
+ expect(out.user).toContain('name');
43
+ });
44
+ });
@@ -7,8 +7,19 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { describe, expect, it } from 'vitest';
11
- import { createSafeDocument, createSafeWindow, createSafeNavigator } from '../safeGlobals';
10
+ import { beforeEach, describe, expect, it } from 'vitest';
11
+ import {
12
+ __resetRunJSSafeGlobalsRegistryForTests,
13
+ createSafeDocument,
14
+ createSafeNavigator,
15
+ createSafeWindow,
16
+ registerRunJSSafeDocumentGlobals,
17
+ registerRunJSSafeWindowGlobals,
18
+ } from '../safeGlobals';
19
+
20
+ beforeEach(() => {
21
+ __resetRunJSSafeGlobalsRegistryForTests();
22
+ });
12
23
 
13
24
  describe('safeGlobals', () => {
14
25
  it('createSafeWindow exposes only allowed globals and extras', () => {
@@ -17,10 +28,36 @@ describe('safeGlobals', () => {
17
28
  expect(win.console).toBeDefined();
18
29
  expect(win.foo).toBe(123);
19
30
  expect(new win.FormData()).toBeInstanceOf(window.FormData);
31
+ if (typeof window.Blob !== 'undefined') {
32
+ expect(typeof win.Blob).toBe('function');
33
+ expect(new win.Blob(['x'])).toBeInstanceOf(window.Blob);
34
+ }
35
+ if (typeof window.URL !== 'undefined') {
36
+ expect(win.URL).toBe(window.URL);
37
+ expect(typeof win.URL.createObjectURL).toBe('function');
38
+ }
20
39
  // access to location proxy is allowed, but sensitive props throw
21
40
  expect(() => win.location.href).toThrow(/not allowed/);
22
41
  });
23
42
 
43
+ it('createSafeWindow allows writing new props and reading them back', () => {
44
+ const win: any = createSafeWindow();
45
+ win.someLib = { ok: true };
46
+ expect(win.someLib).toEqual({ ok: true });
47
+ expect('someLib' in win).toBe(true);
48
+ });
49
+
50
+ it('createSafeWindow can access dynamically registered globals from real window', () => {
51
+ const key = '__nb_safe_window_global__';
52
+ (window as any)[key] = { v: 1 };
53
+ registerRunJSSafeWindowGlobals([key]);
54
+
55
+ const win: any = createSafeWindow();
56
+ expect(win[key]).toEqual({ v: 1 });
57
+
58
+ delete (window as any)[key];
59
+ });
60
+
24
61
  it('createSafeDocument exposes whitelisted methods and extras', () => {
25
62
  const doc: any = createSafeDocument({ bar: true });
26
63
  expect(typeof doc.createElement).toBe('function');
@@ -28,6 +65,24 @@ describe('safeGlobals', () => {
28
65
  expect(() => doc.cookie).toThrow(/not allowed/);
29
66
  });
30
67
 
68
+ it('createSafeDocument allows writing new props and reading them back', () => {
69
+ const doc: any = createSafeDocument();
70
+ doc.someLib = { ok: true };
71
+ expect(doc.someLib).toEqual({ ok: true });
72
+ expect('someLib' in doc).toBe(true);
73
+ });
74
+
75
+ it('createSafeDocument can access dynamically registered globals from real document', () => {
76
+ const key = '__nb_safe_document_global__';
77
+ (document as any)[key] = 123;
78
+ registerRunJSSafeDocumentGlobals([key]);
79
+
80
+ const doc: any = createSafeDocument();
81
+ expect(doc[key]).toBe(123);
82
+
83
+ delete (document as any)[key];
84
+ });
85
+
31
86
  it('createSafeNavigator exposes limited props and guards others', () => {
32
87
  const nav: any = createSafeNavigator();
33
88
  // clipboard object should always exist