@nocobase/flow-engine 2.1.0-alpha.1 → 2.1.0-alpha.10

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 (283) hide show
  1. package/LICENSE +201 -661
  2. package/README.md +79 -10
  3. package/lib/BlockScopedFlowEngine.js +0 -1
  4. package/lib/FlowDefinition.d.ts +2 -0
  5. package/lib/JSRunner.d.ts +15 -0
  6. package/lib/JSRunner.js +82 -7
  7. package/lib/ViewScopedFlowEngine.js +8 -1
  8. package/lib/acl/Acl.js +13 -3
  9. package/lib/components/FlowContextSelector.js +155 -10
  10. package/lib/components/MobilePopup.js +6 -5
  11. package/lib/components/dnd/gridDragPlanner.d.ts +1 -0
  12. package/lib/components/dnd/gridDragPlanner.js +59 -3
  13. package/lib/components/settings/wrappers/component/SwitchWithTitle.js +2 -1
  14. package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +76 -15
  15. package/lib/components/settings/wrappers/contextual/FlowsContextMenu.js +24 -4
  16. package/lib/components/settings/wrappers/contextual/StepSettingsDialog.js +21 -3
  17. package/lib/components/subModel/AddSubModelButton.js +16 -1
  18. package/lib/components/subModel/utils.js +2 -2
  19. package/lib/components/variables/VariableInput.js +9 -4
  20. package/lib/components/variables/VariableTag.js +46 -39
  21. package/lib/components/variables/utils.d.ts +7 -0
  22. package/lib/components/variables/utils.js +42 -2
  23. package/lib/data-source/index.d.ts +7 -27
  24. package/lib/data-source/index.js +84 -51
  25. package/lib/executor/FlowExecutor.d.ts +2 -1
  26. package/lib/executor/FlowExecutor.js +190 -26
  27. package/lib/flowContext.d.ts +230 -7
  28. package/lib/flowContext.js +2270 -148
  29. package/lib/flowEngine.d.ts +160 -1
  30. package/lib/flowEngine.js +383 -26
  31. package/lib/flowI18n.js +6 -4
  32. package/lib/flowSettings.d.ts +14 -6
  33. package/lib/flowSettings.js +51 -17
  34. package/lib/index.d.ts +7 -1
  35. package/lib/index.js +21 -0
  36. package/lib/lazy-helper.d.ts +14 -0
  37. package/lib/lazy-helper.js +71 -0
  38. package/lib/locale/en-US.json +9 -2
  39. package/lib/locale/index.d.ts +14 -0
  40. package/lib/locale/zh-CN.json +8 -1
  41. package/lib/models/CollectionFieldModel.d.ts +1 -0
  42. package/lib/models/CollectionFieldModel.js +3 -2
  43. package/lib/models/flowModel.d.ts +7 -0
  44. package/lib/models/flowModel.js +83 -8
  45. package/lib/provider.js +7 -6
  46. package/lib/resources/baseRecordResource.d.ts +5 -0
  47. package/lib/resources/baseRecordResource.js +24 -0
  48. package/lib/resources/multiRecordResource.d.ts +1 -0
  49. package/lib/resources/multiRecordResource.js +11 -4
  50. package/lib/resources/singleRecordResource.js +2 -0
  51. package/lib/resources/sqlResource.d.ts +4 -3
  52. package/lib/resources/sqlResource.js +8 -3
  53. package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +12 -2
  54. package/lib/runjs-context/contexts/JSBlockRunJSContext.js +2 -2
  55. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.d.ts +16 -0
  56. package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +125 -0
  57. package/lib/runjs-context/contexts/JSItemRunJSContext.js +12 -2
  58. package/lib/runjs-context/contexts/base.js +706 -41
  59. package/lib/runjs-context/contributions.d.ts +33 -0
  60. package/lib/runjs-context/contributions.js +88 -0
  61. package/lib/runjs-context/helpers.js +12 -1
  62. package/lib/runjs-context/registry.d.ts +1 -1
  63. package/lib/runjs-context/setup.js +22 -9
  64. package/lib/runjs-context/snippets/global/api-request.snippet.js +3 -3
  65. package/lib/runjs-context/snippets/global/import-esm.snippet.js +2 -3
  66. package/lib/runjs-context/snippets/global/query-selector.snippet.js +8 -3
  67. package/lib/runjs-context/snippets/global/require-amd.snippet.js +1 -1
  68. package/lib/runjs-context/snippets/index.d.ts +11 -1
  69. package/lib/runjs-context/snippets/index.js +61 -40
  70. package/lib/runjs-context/snippets/scene/block/add-event-listener.snippet.js +10 -7
  71. package/lib/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.js +3 -3
  72. package/lib/runjs-context/snippets/scene/block/chartjs-bar.snippet.js +2 -2
  73. package/lib/runjs-context/snippets/scene/block/echarts-init.snippet.js +2 -2
  74. package/lib/runjs-context/snippets/scene/block/render-iframe.snippet.js +2 -2
  75. package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +1 -1
  76. package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +1 -1
  77. package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +1 -1
  78. package/lib/runjs-context/snippets/scene/block/resource-example.snippet.js +5 -5
  79. package/lib/runjs-context/snippets/scene/block/three-users-orbit.snippet.js +6 -6
  80. package/lib/runjs-context/snippets/scene/block/vue-component.snippet.js +3 -4
  81. package/lib/runjs-context/snippets/scene/detail/color-by-value.snippet.js +1 -1
  82. package/lib/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.js +20 -3
  83. package/lib/runjs-context/snippets/scene/detail/format-number.snippet.js +1 -1
  84. package/lib/runjs-context/snippets/scene/detail/innerHTML-value.snippet.js +1 -1
  85. package/lib/runjs-context/snippets/scene/detail/percentage-bar.snippet.js +3 -3
  86. package/lib/runjs-context/snippets/scene/detail/relative-time.snippet.js +3 -3
  87. package/lib/runjs-context/snippets/scene/detail/status-tag.snippet.js +2 -2
  88. package/lib/runjs-context/snippets/scene/form/cascade-select.snippet.js +1 -1
  89. package/lib/runjs-context/snippets/scene/form/render-basic.snippet.js +2 -2
  90. package/lib/runjs-context/snippets/scene/table/cell-open-dialog.snippet.js +6 -3
  91. package/lib/runjs-context/snippets/scene/table/concat-fields.snippet.js +3 -1
  92. package/lib/runjsLibs.d.ts +28 -0
  93. package/lib/runjsLibs.js +532 -0
  94. package/lib/scheduler/ModelOperationScheduler.d.ts +7 -1
  95. package/lib/scheduler/ModelOperationScheduler.js +28 -23
  96. package/lib/types.d.ts +63 -1
  97. package/lib/utils/associationObjectVariable.d.ts +2 -2
  98. package/lib/utils/createCollectionContextMeta.js +1 -0
  99. package/lib/utils/createEphemeralContext.js +2 -2
  100. package/lib/utils/dateVariable.d.ts +16 -0
  101. package/lib/utils/dateVariable.js +380 -0
  102. package/lib/utils/exceptions.d.ts +7 -0
  103. package/lib/utils/exceptions.js +10 -0
  104. package/lib/utils/index.d.ts +8 -3
  105. package/lib/utils/index.js +49 -0
  106. package/lib/utils/params-resolvers.js +16 -9
  107. package/lib/utils/parsePathnameToViewParams.js +1 -1
  108. package/lib/utils/resolveModuleUrl.d.ts +58 -0
  109. package/lib/utils/resolveModuleUrl.js +65 -0
  110. package/lib/utils/resolveRunJSObjectValues.d.ts +16 -0
  111. package/lib/utils/resolveRunJSObjectValues.js +61 -0
  112. package/lib/utils/runjsModuleLoader.d.ts +58 -0
  113. package/lib/utils/runjsModuleLoader.js +422 -0
  114. package/lib/utils/runjsTemplateCompat.d.ts +35 -0
  115. package/lib/utils/runjsTemplateCompat.js +743 -0
  116. package/lib/utils/runjsValue.d.ts +29 -0
  117. package/lib/utils/runjsValue.js +275 -0
  118. package/lib/utils/safeGlobals.d.ts +18 -8
  119. package/lib/utils/safeGlobals.js +164 -17
  120. package/lib/utils/schema-utils.d.ts +17 -1
  121. package/lib/utils/schema-utils.js +80 -0
  122. package/lib/views/FlowView.d.ts +7 -1
  123. package/lib/views/createViewMeta.d.ts +0 -7
  124. package/lib/views/createViewMeta.js +19 -70
  125. package/lib/views/index.d.ts +1 -2
  126. package/lib/views/index.js +4 -3
  127. package/lib/views/runViewBeforeClose.d.ts +10 -0
  128. package/lib/views/runViewBeforeClose.js +45 -0
  129. package/lib/views/useDialog.d.ts +2 -1
  130. package/lib/views/useDialog.js +28 -6
  131. package/lib/views/useDrawer.d.ts +2 -1
  132. package/lib/views/useDrawer.js +27 -5
  133. package/lib/views/usePage.d.ts +6 -1
  134. package/lib/views/usePage.js +53 -9
  135. package/lib/views/usePopover.js +4 -1
  136. package/lib/views/viewEvents.d.ts +17 -0
  137. package/lib/views/viewEvents.js +90 -0
  138. package/package.json +5 -5
  139. package/src/BlockScopedFlowEngine.ts +2 -5
  140. package/src/JSRunner.ts +111 -5
  141. package/src/ViewScopedFlowEngine.ts +8 -0
  142. package/src/__tests__/JSRunner.test.ts +91 -1
  143. package/src/__tests__/createViewMeta.popup.test.ts +62 -1
  144. package/src/__tests__/flowContext.test.ts +693 -1
  145. package/src/__tests__/flowEngine.dataSourceDirty.test.ts +63 -0
  146. package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
  147. package/src/__tests__/flowModel.openView.navigation.test.ts +28 -0
  148. package/src/__tests__/flowRunJSContextDefine.test.ts +63 -0
  149. package/src/__tests__/flowRuntimeContext.test.ts +2 -1
  150. package/src/__tests__/flowSettings.open.test.tsx +123 -19
  151. package/src/__tests__/flowSettings.test.ts +94 -15
  152. package/src/__tests__/provider.test.tsx +0 -5
  153. package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
  154. package/src/__tests__/runjsContext.test.ts +23 -7
  155. package/src/__tests__/runjsContextImplementations.test.ts +34 -3
  156. package/src/__tests__/runjsContextRuntime.test.ts +3 -3
  157. package/src/__tests__/runjsContributions.test.ts +89 -0
  158. package/src/__tests__/runjsExternalLibs.test.ts +242 -0
  159. package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
  160. package/src/__tests__/runjsLocales.test.ts +4 -1
  161. package/src/__tests__/runjsPreprocessDefault.test.ts +72 -0
  162. package/src/__tests__/runjsRuntimeFeatures.test.ts +166 -0
  163. package/src/__tests__/runjsSnippets.test.ts +40 -3
  164. package/src/__tests__/viewScopedFlowEngine.test.ts +3 -3
  165. package/src/acl/Acl.tsx +3 -3
  166. package/src/components/FlowContextSelector.tsx +208 -12
  167. package/src/components/MobilePopup.tsx +4 -2
  168. package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +3 -3
  169. package/src/components/__tests__/gridDragPlanner.test.ts +229 -1
  170. package/src/components/dnd/gridDragPlanner.ts +68 -2
  171. package/src/components/settings/wrappers/component/SwitchWithTitle.tsx +2 -1
  172. package/src/components/settings/wrappers/component/__tests__/InlineControls.test.tsx +74 -0
  173. package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +109 -16
  174. package/src/components/settings/wrappers/contextual/FlowsContextMenu.tsx +41 -7
  175. package/src/components/settings/wrappers/contextual/StepSettingsDialog.tsx +31 -4
  176. package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +157 -5
  177. package/src/components/subModel/AddSubModelButton.tsx +17 -1
  178. package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +142 -32
  179. package/src/components/subModel/utils.ts +1 -1
  180. package/src/components/variables/VariableInput.tsx +12 -4
  181. package/src/components/variables/VariableTag.tsx +54 -45
  182. package/src/components/variables/__tests__/FlowContextSelector.test.tsx +260 -3
  183. package/src/components/variables/__tests__/VariableTag.test.tsx +50 -0
  184. package/src/components/variables/__tests__/utils.test.ts +81 -3
  185. package/src/components/variables/utils.ts +67 -6
  186. package/src/data-source/index.ts +88 -110
  187. package/src/executor/FlowExecutor.ts +230 -28
  188. package/src/executor/__tests__/flowExecutor.test.ts +123 -0
  189. package/src/flowContext.ts +2989 -212
  190. package/src/flowEngine.ts +427 -22
  191. package/src/flowI18n.ts +7 -5
  192. package/src/flowSettings.ts +58 -18
  193. package/src/index.ts +14 -1
  194. package/src/lazy-helper.tsx +57 -0
  195. package/src/locale/en-US.json +9 -2
  196. package/src/locale/zh-CN.json +8 -1
  197. package/src/models/CollectionFieldModel.tsx +3 -1
  198. package/src/models/__tests__/dispatchEvent.when.test.ts +768 -0
  199. package/src/models/__tests__/flowModel.clone.test.ts +416 -0
  200. package/src/models/__tests__/flowModel.test.ts +20 -4
  201. package/src/models/flowModel.tsx +112 -7
  202. package/src/provider.tsx +9 -7
  203. package/src/resources/__tests__/multiRecordResource.test.ts +44 -0
  204. package/src/resources/__tests__/sqlResource.test.ts +60 -0
  205. package/src/resources/baseRecordResource.ts +31 -0
  206. package/src/resources/multiRecordResource.ts +11 -4
  207. package/src/resources/singleRecordResource.ts +3 -0
  208. package/src/resources/sqlResource.ts +11 -6
  209. package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +10 -0
  210. package/src/runjs-context/contexts/JSBlockRunJSContext.ts +6 -2
  211. package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +106 -0
  212. package/src/runjs-context/contexts/JSItemRunJSContext.ts +10 -0
  213. package/src/runjs-context/contexts/base.ts +715 -44
  214. package/src/runjs-context/contributions.ts +88 -0
  215. package/src/runjs-context/helpers.ts +11 -1
  216. package/src/runjs-context/registry.ts +1 -1
  217. package/src/runjs-context/setup.ts +24 -9
  218. package/src/runjs-context/snippets/global/api-request.snippet.ts +3 -3
  219. package/src/runjs-context/snippets/global/import-esm.snippet.ts +2 -3
  220. package/src/runjs-context/snippets/global/query-selector.snippet.ts +8 -3
  221. package/src/runjs-context/snippets/global/require-amd.snippet.ts +1 -1
  222. package/src/runjs-context/snippets/index.ts +75 -41
  223. package/src/runjs-context/snippets/scene/block/add-event-listener.snippet.ts +11 -13
  224. package/src/runjs-context/snippets/scene/block/api-fetch-render-list.snippet.ts +3 -3
  225. package/src/runjs-context/snippets/scene/block/chartjs-bar.snippet.ts +2 -2
  226. package/src/runjs-context/snippets/scene/block/echarts-init.snippet.ts +2 -2
  227. package/src/runjs-context/snippets/scene/block/render-iframe.snippet.ts +2 -2
  228. package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +1 -1
  229. package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +1 -1
  230. package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +1 -1
  231. package/src/runjs-context/snippets/scene/block/resource-example.snippet.ts +6 -11
  232. package/src/runjs-context/snippets/scene/block/three-users-orbit.snippet.ts +6 -6
  233. package/src/runjs-context/snippets/scene/block/vue-component.snippet.ts +3 -4
  234. package/src/runjs-context/snippets/scene/detail/color-by-value.snippet.ts +1 -1
  235. package/src/runjs-context/snippets/scene/detail/copy-to-clipboard.snippet.ts +20 -3
  236. package/src/runjs-context/snippets/scene/detail/format-number.snippet.ts +1 -1
  237. package/src/runjs-context/snippets/scene/detail/innerHTML-value.snippet.ts +1 -1
  238. package/src/runjs-context/snippets/scene/detail/percentage-bar.snippet.ts +3 -3
  239. package/src/runjs-context/snippets/scene/detail/relative-time.snippet.ts +3 -3
  240. package/src/runjs-context/snippets/scene/detail/status-tag.snippet.ts +2 -2
  241. package/src/runjs-context/snippets/scene/form/cascade-select.snippet.ts +1 -1
  242. package/src/runjs-context/snippets/scene/form/render-basic.snippet.ts +3 -8
  243. package/src/runjs-context/snippets/scene/table/cell-open-dialog.snippet.ts +6 -3
  244. package/src/runjs-context/snippets/scene/table/concat-fields.snippet.ts +3 -1
  245. package/src/runjsLibs.ts +622 -0
  246. package/src/scheduler/ModelOperationScheduler.ts +41 -24
  247. package/src/types.ts +86 -1
  248. package/src/utils/__tests__/dateVariable.test.ts +101 -0
  249. package/src/utils/__tests__/params-resolvers.test.ts +40 -0
  250. package/src/utils/__tests__/parsePathnameToViewParams.test.ts +7 -0
  251. package/src/utils/__tests__/runjsRequireAsyncAutoWhitelist.test.ts +38 -0
  252. package/src/utils/__tests__/runjsTemplateCompat.test.ts +159 -0
  253. package/src/utils/__tests__/runjsValue.test.ts +44 -0
  254. package/src/utils/__tests__/safeGlobals.test.ts +57 -2
  255. package/src/utils/__tests__/utils.test.ts +157 -0
  256. package/src/utils/associationObjectVariable.ts +2 -2
  257. package/src/utils/createCollectionContextMeta.ts +1 -0
  258. package/src/utils/createEphemeralContext.ts +5 -4
  259. package/src/utils/dateVariable.ts +397 -0
  260. package/src/utils/exceptions.ts +11 -0
  261. package/src/utils/index.ts +38 -3
  262. package/src/utils/params-resolvers.ts +23 -9
  263. package/src/utils/parsePathnameToViewParams.ts +2 -2
  264. package/src/utils/resolveModuleUrl.ts +91 -0
  265. package/src/utils/resolveRunJSObjectValues.ts +46 -0
  266. package/src/utils/runjsModuleLoader.ts +553 -0
  267. package/src/utils/runjsTemplateCompat.ts +828 -0
  268. package/src/utils/runjsValue.ts +287 -0
  269. package/src/utils/safeGlobals.ts +188 -17
  270. package/src/utils/schema-utils.ts +109 -1
  271. package/src/views/FlowView.tsx +11 -1
  272. package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
  273. package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
  274. package/src/views/__tests__/useDialog.closeDestroy.test.tsx +44 -16
  275. package/src/views/__tests__/viewEvents.resolveOpenerEngine.test.ts +28 -0
  276. package/src/views/createViewMeta.ts +22 -75
  277. package/src/views/index.tsx +1 -2
  278. package/src/views/runViewBeforeClose.ts +19 -0
  279. package/src/views/useDialog.tsx +34 -5
  280. package/src/views/useDrawer.tsx +33 -4
  281. package/src/views/usePage.tsx +63 -8
  282. package/src/views/usePopover.tsx +4 -1
  283. package/src/views/viewEvents.ts +55 -0
@@ -37,6 +37,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
37
37
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
38
38
  var usePage_exports = {};
39
39
  __export(usePage_exports, {
40
+ EMBED_REPLACING_DATA_KEY: () => EMBED_REPLACING_DATA_KEY,
41
+ GLOBAL_EMBED_CONTAINER_ID: () => GLOBAL_EMBED_CONTAINER_ID,
40
42
  usePage: () => usePage
41
43
  });
42
44
  module.exports = __toCommonJS(usePage_exports);
@@ -48,22 +50,27 @@ var import_FlowContextProvider = require("../FlowContextProvider");
48
50
  var import_createViewMeta = require("./createViewMeta");
49
51
  var import_PageComponent = require("./PageComponent");
50
52
  var import_usePatchElement = __toESM(require("./usePatchElement"));
53
+ var import_viewEvents = require("./viewEvents");
51
54
  var import_provider = require("../provider");
52
55
  var import_ViewScopedFlowEngine = require("../ViewScopedFlowEngine");
53
56
  var import_variablesParams = require("../utils/variablesParams");
57
+ var import_runViewBeforeClose = require("./runViewBeforeClose");
54
58
  let uuid = 0;
59
+ const GLOBAL_EMBED_CONTAINER_ID = "nocobase-embed-container";
60
+ const EMBED_REPLACING_DATA_KEY = "nocobaseEmbedReplacing";
55
61
  const PageElementsHolder = import_react.default.memo(
56
62
  import_react.default.forwardRef((props, ref) => {
57
63
  const [elements, patchElement] = (0, import_usePatchElement.default)();
58
64
  import_react.default.useImperativeHandle(ref, () => ({ patchElement }), [patchElement]);
59
- console.log("[NocoBase] Rendering PageElementsHolder with elements count:", elements.length);
60
65
  return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, elements);
61
66
  })
62
67
  );
63
68
  function usePage() {
64
69
  const holderRef = import_react.default.useRef(null);
70
+ const globalEmbedActiveRef = import_react.default.useRef(null);
65
71
  const open = /* @__PURE__ */ __name((config, flowContext) => {
66
72
  var _a, _b, _c;
73
+ const parentEngine = flowContext == null ? void 0 : flowContext.engine;
67
74
  uuid += 1;
68
75
  const pageRef = import_react.default.createRef();
69
76
  let closeFunc;
@@ -93,9 +100,27 @@ function usePage() {
93
100
  }, [props]);
94
101
  return null;
95
102
  }, "HeaderComponent");
96
- const { target, content, preventClose, inheritContext = true, inputArgs, ...restConfig } = config;
103
+ const {
104
+ target,
105
+ content,
106
+ preventClose,
107
+ inheritContext = true,
108
+ inputArgs: viewInputArgs = {},
109
+ ...restConfig
110
+ } = config;
111
+ const isGlobalEmbedContainer = target instanceof HTMLElement && target.id === GLOBAL_EMBED_CONTAINER_ID;
112
+ if (isGlobalEmbedContainer && globalEmbedActiveRef.current) {
113
+ try {
114
+ target.dataset[EMBED_REPLACING_DATA_KEY] = "1";
115
+ globalEmbedActiveRef.current.destroy();
116
+ } finally {
117
+ delete target.dataset[EMBED_REPLACING_DATA_KEY];
118
+ globalEmbedActiveRef.current = null;
119
+ }
120
+ }
97
121
  const ctx = new import_flowContext.FlowContext();
98
122
  const scopedEngine = (0, import_ViewScopedFlowEngine.createViewScopedEngine)(flowContext.engine);
123
+ const openerEngine = (0, import_viewEvents.resolveOpenerEngine)(parentEngine, scopedEngine);
99
124
  ctx.defineProperty("engine", { value: scopedEngine });
100
125
  ctx.addDelegate(scopedEngine.context);
101
126
  if (inheritContext) {
@@ -105,30 +130,45 @@ function usePage() {
105
130
  }
106
131
  const currentPage = {
107
132
  type: "embed",
108
- inputArgs: config.inputArgs || {},
133
+ inputArgs: viewInputArgs,
109
134
  preventClose: !!config.preventClose,
135
+ beforeClose: void 0,
110
136
  destroy: /* @__PURE__ */ __name((result) => {
111
- var _a2, _b2;
137
+ var _a2, _b2, _c2, _d, _e;
112
138
  (_a2 = config.onClose) == null ? void 0 : _a2.call(config);
113
139
  resolvePromise == null ? void 0 : resolvePromise(result);
114
140
  (_b2 = pageRef.current) == null ? void 0 : _b2.destroy();
115
141
  closeFunc == null ? void 0 : closeFunc();
142
+ if (isGlobalEmbedContainer) {
143
+ globalEmbedActiveRef.current = null;
144
+ }
145
+ const isReplacing = isGlobalEmbedContainer && target instanceof HTMLElement && ((_c2 = target.dataset) == null ? void 0 : _c2[EMBED_REPLACING_DATA_KEY]) === "1";
146
+ if (!isReplacing) {
147
+ const openerEmitter = openerEngine == null ? void 0 : openerEngine.emitter;
148
+ (0, import_viewEvents.bumpViewActivatedVersion)(openerEmitter);
149
+ (_e = openerEmitter == null ? void 0 : openerEmitter.emit) == null ? void 0 : _e.call(openerEmitter, import_viewEvents.VIEW_ACTIVATED_EVENT, { type: "embed", viewUid: (_d = currentPage == null ? void 0 : currentPage.inputArgs) == null ? void 0 : _d.viewUid });
150
+ }
116
151
  scopedEngine.unlinkFromStack();
117
152
  }, "destroy"),
118
153
  update: /* @__PURE__ */ __name((newConfig) => {
119
154
  var _a2;
120
155
  return (_a2 = pageRef.current) == null ? void 0 : _a2.update(newConfig);
121
156
  }, "update"),
122
- close: /* @__PURE__ */ __name((result, force) => {
157
+ close: /* @__PURE__ */ __name(async (result, force) => {
123
158
  var _a2, _b2;
124
159
  if (preventClose && !force) {
125
- return;
160
+ return false;
161
+ }
162
+ const shouldClose = await (0, import_runViewBeforeClose.runViewBeforeClose)(currentPage, { result, force });
163
+ if (!shouldClose) {
164
+ return false;
126
165
  }
127
166
  if (config.triggerByRouter && ((_b2 = (_a2 = config.inputArgs) == null ? void 0 : _a2.navigation) == null ? void 0 : _b2.back)) {
128
167
  config.inputArgs.navigation.back();
129
- return;
168
+ return true;
130
169
  }
131
170
  currentPage.destroy(result);
171
+ return true;
132
172
  }, "close"),
133
173
  Header: HeaderComponent,
134
174
  Footer: FooterComponent,
@@ -147,7 +187,6 @@ function usePage() {
147
187
  };
148
188
  ctx.defineProperty("view", {
149
189
  get: /* @__PURE__ */ __name(() => currentPage, "get"),
150
- // meta: createViewMeta(ctx),
151
190
  // 仅当访问关联字段或前端无本地记录数据时,才交给服务端解析
152
191
  resolveOnServer: (0, import_variablesParams.createViewRecordResolveOnServer)(ctx, () => (0, import_variablesParams.getViewRecordFromParent)(flowContext, ctx))
153
192
  });
@@ -186,13 +225,16 @@ function usePage() {
186
225
  displayName: "PageWithContext"
187
226
  }
188
227
  );
189
- const key = (inputArgs == null ? void 0 : inputArgs.viewUid) || `page-${uuid}`;
228
+ const key = (viewInputArgs == null ? void 0 : viewInputArgs.viewUid) || `page-${uuid}`;
190
229
  const page = /* @__PURE__ */ import_react.default.createElement(import_provider.FlowEngineProvider, { key, engine: scopedEngine }, /* @__PURE__ */ import_react.default.createElement(import_FlowContextProvider.FlowViewContextProvider, { context: ctx }, /* @__PURE__ */ import_react.default.createElement(PageWithContext, null)));
191
230
  if (target && target instanceof HTMLElement) {
192
231
  closeFunc = (_b = holderRef.current) == null ? void 0 : _b.patchElement(import_react_dom.default.createPortal(page, target, key));
193
232
  } else {
194
233
  closeFunc = (_c = holderRef.current) == null ? void 0 : _c.patchElement(page);
195
234
  }
235
+ if (isGlobalEmbedContainer) {
236
+ globalEmbedActiveRef.current = { destroy: currentPage.destroy };
237
+ }
196
238
  return Object.assign(promise, currentPage);
197
239
  }, "open");
198
240
  const api = import_react.default.useMemo(() => ({ open }), []);
@@ -201,5 +243,7 @@ function usePage() {
201
243
  __name(usePage, "usePage");
202
244
  // Annotate the CommonJS export names for ESM import in node:
203
245
  0 && (module.exports = {
246
+ EMBED_REPLACING_DATA_KEY,
247
+ GLOBAL_EMBED_CONTAINER_ID,
204
248
  usePage
205
249
  });
@@ -68,8 +68,11 @@ const PopoverComponent = React.forwardRef(({ afterClose, content, placement, rec
68
68
  destroyTooltipOnHide: true,
69
69
  content: config.content,
70
70
  placement: config.placement,
71
- getPopupContainer: () => document.body,
71
+ getPopupContainer: () => document.querySelector("#nocobase-app-container") || document.body,
72
72
  onOpenChange: (nextOpen) => {
73
+ if (!nextOpen && config.preventClose) {
74
+ return;
75
+ }
73
76
  setVisible(nextOpen);
74
77
  if (!nextOpen) {
75
78
  afterClose == null ? void 0 : afterClose();
@@ -0,0 +1,17 @@
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
+ import { FlowEngine } from '../flowEngine';
10
+ export declare const VIEW_ACTIVATED_VERSION: unique symbol;
11
+ export declare const VIEW_ACTIVATED_EVENT: "view:activated";
12
+ export declare const DATA_SOURCE_DIRTY_EVENT: "dataSource:dirty";
13
+ export declare const ENGINE_SCOPE_KEY: "__NOCOBASE_ENGINE_SCOPE__";
14
+ export declare const VIEW_ENGINE_SCOPE: "view";
15
+ export declare function getEmitterViewActivatedVersion(emitter: any): number;
16
+ export declare function bumpViewActivatedVersion(emitter: any): number;
17
+ export declare function resolveOpenerEngine(parentEngine: FlowEngine, scopedEngine: FlowEngine): FlowEngine | undefined;
@@ -0,0 +1,90 @@
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
+ var __defProp = Object.defineProperty;
11
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
15
+ var __export = (target, all) => {
16
+ for (var name in all)
17
+ __defProp(target, name, { get: all[name], enumerable: true });
18
+ };
19
+ var __copyProps = (to, from, except, desc) => {
20
+ if (from && typeof from === "object" || typeof from === "function") {
21
+ for (let key of __getOwnPropNames(from))
22
+ if (!__hasOwnProp.call(to, key) && key !== except)
23
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
24
+ }
25
+ return to;
26
+ };
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+ var viewEvents_exports = {};
29
+ __export(viewEvents_exports, {
30
+ DATA_SOURCE_DIRTY_EVENT: () => DATA_SOURCE_DIRTY_EVENT,
31
+ ENGINE_SCOPE_KEY: () => ENGINE_SCOPE_KEY,
32
+ VIEW_ACTIVATED_EVENT: () => VIEW_ACTIVATED_EVENT,
33
+ VIEW_ACTIVATED_VERSION: () => VIEW_ACTIVATED_VERSION,
34
+ VIEW_ENGINE_SCOPE: () => VIEW_ENGINE_SCOPE,
35
+ bumpViewActivatedVersion: () => bumpViewActivatedVersion,
36
+ getEmitterViewActivatedVersion: () => getEmitterViewActivatedVersion,
37
+ resolveOpenerEngine: () => resolveOpenerEngine
38
+ });
39
+ module.exports = __toCommonJS(viewEvents_exports);
40
+ const VIEW_ACTIVATED_VERSION = Symbol.for("__NOCOBASE_VIEW_ACTIVATED_VERSION__");
41
+ const VIEW_ACTIVATED_EVENT = "view:activated";
42
+ const DATA_SOURCE_DIRTY_EVENT = "dataSource:dirty";
43
+ const ENGINE_SCOPE_KEY = "__NOCOBASE_ENGINE_SCOPE__";
44
+ const VIEW_ENGINE_SCOPE = "view";
45
+ function getEmitterViewActivatedVersion(emitter) {
46
+ const raw = Reflect.get(emitter, VIEW_ACTIVATED_VERSION);
47
+ const num = typeof raw === "number" ? raw : Number(raw);
48
+ return Number.isFinite(num) && num > 0 ? num : 0;
49
+ }
50
+ __name(getEmitterViewActivatedVersion, "getEmitterViewActivatedVersion");
51
+ function bumpViewActivatedVersion(emitter) {
52
+ const current = getEmitterViewActivatedVersion(emitter);
53
+ if (!Object.isExtensible(emitter)) return current;
54
+ const next = current + 1;
55
+ Reflect.set(emitter, VIEW_ACTIVATED_VERSION, next);
56
+ return next;
57
+ }
58
+ __name(bumpViewActivatedVersion, "bumpViewActivatedVersion");
59
+ function isViewEngine(engine) {
60
+ return Reflect.get(engine, ENGINE_SCOPE_KEY) === VIEW_ENGINE_SCOPE;
61
+ }
62
+ __name(isViewEngine, "isViewEngine");
63
+ function findNearestViewEngine(engine) {
64
+ let cur = engine;
65
+ let guard = 0;
66
+ while (cur && guard++ < 50) {
67
+ if (isViewEngine(cur)) return cur;
68
+ cur = cur.previousEngine;
69
+ }
70
+ }
71
+ __name(findNearestViewEngine, "findNearestViewEngine");
72
+ function resolveOpenerEngine(parentEngine, scopedEngine) {
73
+ if (!parentEngine) return void 0;
74
+ const parentViewEngine = findNearestViewEngine(parentEngine);
75
+ if (parentViewEngine) return parentViewEngine;
76
+ const previousEngine = scopedEngine == null ? void 0 : scopedEngine.previousEngine;
77
+ return findNearestViewEngine(previousEngine) || parentEngine;
78
+ }
79
+ __name(resolveOpenerEngine, "resolveOpenerEngine");
80
+ // Annotate the CommonJS export names for ESM import in node:
81
+ 0 && (module.exports = {
82
+ DATA_SOURCE_DIRTY_EVENT,
83
+ ENGINE_SCOPE_KEY,
84
+ VIEW_ACTIVATED_EVENT,
85
+ VIEW_ACTIVATED_VERSION,
86
+ VIEW_ENGINE_SCOPE,
87
+ bumpViewActivatedVersion,
88
+ getEmitterViewActivatedVersion,
89
+ resolveOpenerEngine
90
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/flow-engine",
3
- "version": "2.1.0-alpha.1",
3
+ "version": "2.1.0-alpha.10",
4
4
  "private": false,
5
5
  "description": "A standalone flow engine for NocoBase, managing workflows, models, and actions.",
6
6
  "main": "lib/index.js",
@@ -8,8 +8,8 @@
8
8
  "dependencies": {
9
9
  "@formily/antd-v5": "1.x",
10
10
  "@formily/reactive": "2.x",
11
- "@nocobase/sdk": "2.1.0-alpha.1",
12
- "@nocobase/shared": "2.1.0-alpha.1",
11
+ "@nocobase/sdk": "2.1.0-alpha.10",
12
+ "@nocobase/shared": "2.1.0-alpha.10",
13
13
  "ahooks": "^3.7.2",
14
14
  "dayjs": "^1.11.9",
15
15
  "dompurify": "^3.0.2",
@@ -35,6 +35,6 @@
35
35
  "workflow"
36
36
  ],
37
37
  "author": "NocoBase Team",
38
- "license": "AGPL-3.0",
39
- "gitHead": "d27baf21569643d6fa83f882233f4e90eb5b89f1"
38
+ "license": "Apache-2.0",
39
+ "gitHead": "ce790d46c0a5768ca9618c7d0d77ab8300de75c8"
40
40
  }
@@ -31,16 +31,13 @@ export function createBlockScopedEngine(parent: FlowEngine): FlowEngine {
31
31
  local.context.addDelegate(parent.context);
32
32
 
33
33
  // 覆盖 unlinkFromStack:BlockScoped 引擎被移除时,修复前后指针,避免“截断”后续视图/作用域
34
- const originalUnlink = local.unlinkFromStack.bind(local);
35
34
  local.unlinkFromStack = function () {
36
- // 修复指针:prev -> next,next -> prev,然后清理自身指针
37
- // 若不这么做,移除位于中间的 block 引擎会导致后续整段链丢失
38
35
  const prev = (local as any)._previousEngine as FlowEngine | undefined;
39
36
  const next = (local as any)._nextEngine as FlowEngine | undefined;
40
37
  if (prev) (prev as any)._nextEngine = next;
41
38
  if (next) (next as any)._previousEngine = prev;
42
- (local as any)._previousEngine = undefined as any;
43
- (local as any)._nextEngine = undefined as any;
39
+ (local as any)._previousEngine = undefined;
40
+ (local as any)._nextEngine = undefined;
44
41
  };
45
42
 
46
43
  // 默认全部代理到父引擎,只有少数字段(实例/缓存/执行器/上下文/链表指针)使用本地值
package/src/JSRunner.ts CHANGED
@@ -8,11 +8,80 @@
8
8
  */
9
9
 
10
10
  import 'ses';
11
+ import { FlowExitAllException, FlowExitException } from './utils/exceptions';
11
12
 
12
13
  export interface JSRunnerOptions {
13
14
  timeoutMs?: number;
14
15
  globals?: Record<string, any>;
15
16
  version?: string;
17
+ /**
18
+ * Enable RunJS template compatibility preprocessing for `{{ ... }}`.
19
+ * When enabled (or falling back to version default),
20
+ * the code will be rewritten to call `ctx.resolveJsonTemplate(...)` at runtime.
21
+ */
22
+ preprocessTemplates?: boolean;
23
+ }
24
+
25
+ /**
26
+ * Decide whether RunJS `{{ ... }}` compatibility preprocessing should run.
27
+ *
28
+ * Priority:
29
+ * 1. Explicit `preprocessTemplates` option always wins.
30
+ * 2. Otherwise, `version === 'v2'` disables preprocessing.
31
+ * 3. Fallback keeps v1-compatible behavior (enabled).
32
+ */
33
+ export function shouldPreprocessRunJSTemplates(
34
+ options?: Pick<JSRunnerOptions, 'preprocessTemplates' | 'version'>,
35
+ ): boolean {
36
+ if (typeof options?.preprocessTemplates === 'boolean') {
37
+ return options.preprocessTemplates;
38
+ }
39
+ return options?.version !== 'v2';
40
+ }
41
+
42
+ // Heuristic: detect likely bare `{{ctx.xxx}}` usage in executable positions (not quoted string literals).
43
+ const BARE_CTX_TEMPLATE_RE = /(^|[=(:,[\s)])(\{\{\s*(ctx(?:\.|\[|\?\.)[^}]*)\s*\}\})/m;
44
+
45
+ function extractDeprecatedCtxTemplateUsage(code: string): { placeholder: string; expression: string } | null {
46
+ const src = String(code || '');
47
+ const m = src.match(BARE_CTX_TEMPLATE_RE);
48
+ if (!m) return null;
49
+ const placeholder = String(m[2] || '').trim();
50
+ const expression = String(m[3] || '').trim();
51
+ if (!placeholder || !expression) return null;
52
+ return { placeholder, expression };
53
+ }
54
+
55
+ function shouldHintCtxTemplateSyntax(err: any, usage: { placeholder: string; expression: string } | null): boolean {
56
+ const isSyntaxError = err instanceof SyntaxError || String((err as any)?.name || '') === 'SyntaxError';
57
+ if (!isSyntaxError) return false;
58
+ if (!usage) return false;
59
+ const msg = String((err as any)?.message || err || '');
60
+ return /unexpected token/i.test(msg);
61
+ }
62
+
63
+ function toCtxTemplateSyntaxHintError(
64
+ err: any,
65
+ usage: {
66
+ placeholder: string;
67
+ expression: string;
68
+ },
69
+ ): Error {
70
+ const hint = `"${usage.placeholder}" has been deprecated and cannot be used as executable RunJS syntax. Use await ctx.getVar("${usage.expression}") instead, or keep "${usage.placeholder}" as a plain string.`;
71
+ const out = new SyntaxError(hint);
72
+ try {
73
+ (out as any).cause = err;
74
+ } catch (_) {
75
+ // ignore
76
+ }
77
+ try {
78
+ // Hint-only error: avoid leaking internal bundle line numbers from stack parsers in preview UI.
79
+ (out as any).__runjsHideLocation = true;
80
+ out.stack = `${out.name}: ${out.message}`;
81
+ } catch (_) {
82
+ // ignore
83
+ }
84
+ return out;
16
85
  }
17
86
 
18
87
  export class JSRunner {
@@ -28,13 +97,41 @@ export class JSRunner {
28
97
  return typeof fn === 'function' ? fn.bind(globalThis) : fn;
29
98
  };
30
99
 
100
+ const providedGlobals = options.globals || {};
101
+ const liftedGlobals: Record<string, any> = {};
102
+
103
+ // Auto-lift selected globals from safe window into top-level sandbox globals
104
+ // so user code can access them directly (e.g. `new Blob(...)`).
105
+ if (!Object.prototype.hasOwnProperty.call(providedGlobals, 'Blob')) {
106
+ try {
107
+ const blobCtor = (providedGlobals as any).window?.Blob;
108
+ if (typeof blobCtor !== 'undefined') {
109
+ liftedGlobals.Blob = blobCtor;
110
+ }
111
+ } catch {
112
+ // ignore when window proxy blocks property access
113
+ }
114
+ }
115
+
116
+ if (!Object.prototype.hasOwnProperty.call(providedGlobals, 'URL')) {
117
+ try {
118
+ const urlCtor = (providedGlobals as any).window?.URL;
119
+ if (typeof urlCtor !== 'undefined') {
120
+ liftedGlobals.URL = urlCtor;
121
+ }
122
+ } catch {
123
+ // ignore when window proxy blocks property access
124
+ }
125
+ }
126
+
31
127
  this.globals = {
32
128
  console,
33
129
  setTimeout: bindWindowFn('setTimeout'),
34
130
  clearTimeout: bindWindowFn('clearTimeout'),
35
131
  setInterval: bindWindowFn('setInterval'),
36
132
  clearInterval: bindWindowFn('clearInterval'),
37
- ...(options.globals || {}),
133
+ ...liftedGlobals,
134
+ ...providedGlobals,
38
135
  };
39
136
  this.timeoutMs = options.timeoutMs ?? 5000; // 默认 5 秒超时
40
137
  }
@@ -55,7 +152,8 @@ export class JSRunner {
55
152
  error?: any;
56
153
  timeout?: boolean;
57
154
  }> {
58
- if (location?.search.includes('skipRunJs=true')) {
155
+ const search = typeof location !== 'undefined' ? location.search : undefined;
156
+ if (typeof search === 'string' && search.includes('skipRunJs=true')) {
59
157
  return { success: true, value: null };
60
158
  }
61
159
  const wrapped = `(async () => {
@@ -76,11 +174,19 @@ export class JSRunner {
76
174
  const result = await Promise.race([task, timeoutPromise]);
77
175
  return { success: true, value: result };
78
176
  } catch (err) {
79
- console.error(err);
177
+ if (err instanceof FlowExitException) {
178
+ throw err;
179
+ }
180
+ if (err instanceof FlowExitAllException) {
181
+ throw err;
182
+ }
183
+ const usage = extractDeprecatedCtxTemplateUsage(code);
184
+ const outErr = shouldHintCtxTemplateSyntax(err, usage) && usage ? toCtxTemplateSyntaxHintError(err, usage) : err;
185
+ console.error(outErr);
80
186
  return {
81
187
  success: false,
82
- error: err,
83
- timeout: err.message === 'Execution timed out',
188
+ error: outErr,
189
+ timeout: (outErr as any)?.message === 'Execution timed out',
84
190
  };
85
191
  }
86
192
  }
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { FlowEngine } from './flowEngine';
11
+ import { ENGINE_SCOPE_KEY, VIEW_ENGINE_SCOPE } from './views/viewEvents';
11
12
 
12
13
  /**
13
14
  * ViewScopedFlowEngine(视图作用域引擎)
@@ -24,6 +25,8 @@ import { FlowEngine } from './flowEngine';
24
25
  */
25
26
  export function createViewScopedEngine(parent: FlowEngine): FlowEngine {
26
27
  const local = new FlowEngine();
28
+ // Mark for view-stack traversal (used by view activation events).
29
+ Object.defineProperty(local, ENGINE_SCOPE_KEY, { value: VIEW_ENGINE_SCOPE, configurable: true });
27
30
  if (parent.modelRepository) {
28
31
  local.setModelRepository(parent.modelRepository);
29
32
  }
@@ -43,6 +46,7 @@ export function createViewScopedEngine(parent: FlowEngine): FlowEngine {
43
46
  '_applyFlowCache',
44
47
  'executor',
45
48
  'context',
49
+ ENGINE_SCOPE_KEY,
46
50
  'previousEngine',
47
51
  'nextEngine',
48
52
  // 调度器与事件总线局部化
@@ -58,6 +62,10 @@ export function createViewScopedEngine(parent: FlowEngine): FlowEngine {
58
62
  '_nextEngine',
59
63
  // getModel 需要在本地执行以确保全局查找时正确遍历整个引擎栈
60
64
  'getModel',
65
+ // 视图销毁回调需要在本地存储,每个视图引擎有自己的销毁逻辑
66
+ '_destroyView',
67
+ 'setDestroyView',
68
+ 'destroyView',
61
69
  ]);
62
70
 
63
71
  const handler: ProxyHandler<FlowEngine> = {
@@ -8,7 +8,8 @@
8
8
  */
9
9
 
10
10
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
11
- import { JSRunner } from '../JSRunner';
11
+ import { JSRunner, shouldPreprocessRunJSTemplates } from '../JSRunner';
12
+ import { createSafeWindow } from '../utils';
12
13
 
13
14
  describe('JSRunner', () => {
14
15
  let originalSearch: string;
@@ -29,6 +30,18 @@ describe('JSRunner', () => {
29
30
  vi.restoreAllMocks();
30
31
  });
31
32
 
33
+ it('shouldPreprocessRunJSTemplates: explicit option has highest priority', () => {
34
+ expect(shouldPreprocessRunJSTemplates({ version: 'v2', preprocessTemplates: true })).toBe(true);
35
+ expect(shouldPreprocessRunJSTemplates({ version: 'v1', preprocessTemplates: false })).toBe(false);
36
+ });
37
+
38
+ it('shouldPreprocessRunJSTemplates: falls back to version policy', () => {
39
+ expect(shouldPreprocessRunJSTemplates({ version: 'v1' })).toBe(true);
40
+ expect(shouldPreprocessRunJSTemplates({ version: 'v2' })).toBe(false);
41
+ expect(shouldPreprocessRunJSTemplates({})).toBe(true);
42
+ expect(shouldPreprocessRunJSTemplates()).toBe(true);
43
+ });
44
+
32
45
  it('executes simple code and returns value', async () => {
33
46
  const runner = new JSRunner();
34
47
  const result = await runner.run('return 1 + 2 + 3');
@@ -48,6 +61,69 @@ describe('JSRunner', () => {
48
61
  expect(res2.value).toBe('baz');
49
62
  });
50
63
 
64
+ it('auto-lifts Blob from injected window to top-level globals', async () => {
65
+ if (typeof Blob === 'undefined') {
66
+ return;
67
+ }
68
+
69
+ const runner = new JSRunner({
70
+ globals: {
71
+ window: createSafeWindow(),
72
+ },
73
+ });
74
+
75
+ const result = await runner.run('return new Blob(["x"]).size');
76
+ expect(result.success).toBe(true);
77
+ expect(result.value).toBe(1);
78
+ });
79
+
80
+ it('keeps explicit globals.Blob higher priority than auto-lifted Blob', async () => {
81
+ const explicitBlob = function ExplicitBlob(this: any, chunks: any[]) {
82
+ this.size = Array.isArray(chunks) ? chunks.length : 0;
83
+ } as any;
84
+
85
+ const runner = new JSRunner({
86
+ globals: {
87
+ window: createSafeWindow(),
88
+ Blob: explicitBlob,
89
+ },
90
+ });
91
+
92
+ const result = await runner.run('const b = new Blob([1,2,3]); return b.size;');
93
+ expect(result.success).toBe(true);
94
+ expect(result.value).toBe(3);
95
+ });
96
+
97
+ it('auto-lifts URL from injected window to top-level globals', async () => {
98
+ const runner = new JSRunner({
99
+ globals: {
100
+ window: createSafeWindow(),
101
+ },
102
+ });
103
+
104
+ const result = await runner.run('return typeof URL.createObjectURL === "function"');
105
+ expect(result.success).toBe(true);
106
+ expect(result.value).toBe(true);
107
+ });
108
+
109
+ it('keeps explicit globals.URL higher priority than auto-lifted URL', async () => {
110
+ const explicitURL = {
111
+ createObjectURL: () => 'explicit://url',
112
+ revokeObjectURL: (_url: string) => undefined,
113
+ };
114
+
115
+ const runner = new JSRunner({
116
+ globals: {
117
+ window: createSafeWindow(),
118
+ URL: explicitURL,
119
+ },
120
+ });
121
+
122
+ const result = await runner.run('return URL.createObjectURL(new Blob(["x"]))');
123
+ expect(result.success).toBe(true);
124
+ expect(result.value).toBe('explicit://url');
125
+ });
126
+
51
127
  it('exposes console in sandbox by default', async () => {
52
128
  const runner = new JSRunner();
53
129
  const result = await runner.run('return typeof console !== "undefined"');
@@ -88,6 +164,20 @@ describe('JSRunner', () => {
88
164
  expect((result.error as Error).message).toBe('Execution timed out');
89
165
  });
90
166
 
167
+ it('returns friendly hint when bare {{ctx.xxx}} appears in syntax error', async () => {
168
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
169
+ const runner = new JSRunner();
170
+ const result = await runner.run('const z = {{ctx.user.id}}');
171
+ expect(result.success).toBe(false);
172
+ expect(result.error).toBeInstanceOf(SyntaxError);
173
+ const msg = String((result.error as any)?.message || '');
174
+ expect(msg).toContain('"{{ctx.user.id}}" has been deprecated');
175
+ expect(msg).toContain('await ctx.getVar("ctx.user.id")');
176
+ expect(msg).not.toContain('(at ');
177
+ expect((result.error as any)?.__runjsHideLocation).toBe(true);
178
+ expect(spy).toHaveBeenCalled();
179
+ });
180
+
91
181
  it('skips execution when URL contains skipRunJs=true', async () => {
92
182
  // 模拟预览模式下通过 URL 参数跳过代码执行
93
183
  if (typeof window !== 'undefined' && typeof window.history?.pushState === 'function') {