@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
@@ -0,0 +1,397 @@
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 dayjs from 'dayjs';
11
+
12
+ const CTX_DATE_REGEX = /^\{\{\s*ctx\.date(?:\.(.+?))?\s*\}\}$/;
13
+
14
+ const PRESET_KEYS = new Set([
15
+ 'today',
16
+ 'now',
17
+ 'yesterday',
18
+ 'tomorrow',
19
+ 'thisWeek',
20
+ 'lastWeek',
21
+ 'nextWeek',
22
+ 'thisMonth',
23
+ 'lastMonth',
24
+ 'nextMonth',
25
+ 'thisQuarter',
26
+ 'lastQuarter',
27
+ 'nextQuarter',
28
+ 'thisYear',
29
+ 'lastYear',
30
+ 'nextYear',
31
+ ]);
32
+
33
+ const RELATIVE_DIRECTIONS = new Set(['next', 'past']);
34
+ const RELATIVE_UNITS = new Set(['day', 'week', 'month', 'year']);
35
+
36
+ function parseCtxDateSegments(value: string): string[] | null {
37
+ if (typeof value !== 'string') return null;
38
+ const trimmed = value.trim();
39
+ const match = trimmed.match(CTX_DATE_REGEX);
40
+ if (!match) return null;
41
+ const rawPath = String(match[1] || '');
42
+ if (!rawPath) return [];
43
+ return rawPath
44
+ .split('.')
45
+ .map((seg) => seg.trim())
46
+ .filter(Boolean);
47
+ }
48
+
49
+ export function isCtxDatePathPrefix(pathSegments: string[]): boolean {
50
+ const segments = withDatePrefix((pathSegments || []).map((seg) => String(seg)));
51
+ if (segments[0] !== 'date') return false;
52
+ if (segments.length === 1) return true;
53
+
54
+ if (segments[1] === 'preset') {
55
+ if (segments.length === 2) return true;
56
+ return segments.length === 3 && PRESET_KEYS.has(segments[2]);
57
+ }
58
+
59
+ if (segments[1] === 'relative') {
60
+ if (segments.length === 2) return true;
61
+ if (segments.length === 3) return RELATIVE_DIRECTIONS.has(segments[2]);
62
+ if (segments.length === 4) {
63
+ return RELATIVE_DIRECTIONS.has(segments[2]) && RELATIVE_UNITS.has(segments[3]);
64
+ }
65
+ if (segments.length === 5) {
66
+ return (
67
+ RELATIVE_DIRECTIONS.has(segments[2]) &&
68
+ RELATIVE_UNITS.has(segments[3]) &&
69
+ typeof parseNumberToken(segments[4]) === 'number'
70
+ );
71
+ }
72
+ return false;
73
+ }
74
+
75
+ if (segments[1] === 'exact') {
76
+ if (segments.length === 2) return true;
77
+
78
+ if (segments[2] === 'single') {
79
+ if (segments.length === 3) return true;
80
+ if (segments.length === 4) return segments[3] === 'date';
81
+ if (segments.length === 5) return segments[3] === 'date' && /^v.+/.test(segments[4]);
82
+ return false;
83
+ }
84
+
85
+ if (segments[2] === 'range') {
86
+ if (segments.length === 3) return true;
87
+ if (segments.length === 4) return segments[3] === 'date';
88
+ if (segments.length === 5) return segments[3] === 'date' && /^v.+/.test(segments[4]);
89
+ if (segments.length === 6) return segments[3] === 'date' && /^v.+/.test(segments[4]) && /^v.+/.test(segments[5]);
90
+ return false;
91
+ }
92
+
93
+ return false;
94
+ }
95
+
96
+ return false;
97
+ }
98
+
99
+ function withDatePrefix(pathSegments: string[]): string[] {
100
+ if (pathSegments[0] === 'date') {
101
+ return pathSegments;
102
+ }
103
+ return ['date', ...pathSegments];
104
+ }
105
+
106
+ function toCtxDateExpression(pathSegments: string[]): string {
107
+ const segs = withDatePrefix(pathSegments);
108
+ return `{{ ctx.${segs.join('.')} }}`;
109
+ }
110
+
111
+ function utf8ToBase64(input: string): string {
112
+ const globalBuffer = (globalThis as any)?.Buffer;
113
+ if (globalBuffer && typeof globalBuffer.from === 'function') {
114
+ return globalBuffer.from(input, 'utf8').toString('base64');
115
+ }
116
+
117
+ if (typeof btoa === 'function') {
118
+ const encoded = encodeURIComponent(input).replace(/%([0-9A-F]{2})/g, (_m, p1) =>
119
+ String.fromCharCode(parseInt(p1, 16)),
120
+ );
121
+ return btoa(encoded);
122
+ }
123
+
124
+ throw new Error('No base64 encoder available');
125
+ }
126
+
127
+ function base64ToUtf8(input: string): string {
128
+ const globalBuffer = (globalThis as any)?.Buffer;
129
+ if (globalBuffer && typeof globalBuffer.from === 'function') {
130
+ return globalBuffer.from(input, 'base64').toString('utf8');
131
+ }
132
+
133
+ if (typeof atob === 'function') {
134
+ const binary = atob(input);
135
+ const encoded = Array.from(binary)
136
+ .map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, '0')}`)
137
+ .join('');
138
+ return decodeURIComponent(encoded);
139
+ }
140
+
141
+ throw new Error('No base64 decoder available');
142
+ }
143
+
144
+ function normalizeToString(value: any): string | undefined {
145
+ if (value == null) return undefined;
146
+ if (typeof value === 'string') {
147
+ const trimmed = value.trim();
148
+ return trimmed.length ? trimmed : undefined;
149
+ }
150
+ if (dayjs.isDayjs(value)) {
151
+ return value.toISOString();
152
+ }
153
+ if (value instanceof Date) {
154
+ return dayjs(value).toISOString();
155
+ }
156
+ if (typeof value === 'number' || typeof value === 'boolean') {
157
+ return String(value);
158
+ }
159
+ return undefined;
160
+ }
161
+
162
+ function parseNumberToken(token: string): number | undefined {
163
+ const match = String(token || '').match(/^n(\d+)$/);
164
+ if (!match) return undefined;
165
+ const parsed = Number(match[1]);
166
+ if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
167
+ return parsed;
168
+ }
169
+
170
+ function startOfIsoWeek(date: dayjs.Dayjs): dayjs.Dayjs {
171
+ const day = date.day();
172
+ const offset = day === 0 ? -6 : 1 - day;
173
+ return date.add(offset, 'day').startOf('day');
174
+ }
175
+
176
+ function startOfQuarter(date: dayjs.Dayjs): dayjs.Dayjs {
177
+ const month = date.month();
178
+ const quarterStartMonth = Math.floor(month / 3) * 3;
179
+ return date.month(quarterStartMonth).startOf('month');
180
+ }
181
+
182
+ export function encodeBase64Url(input: string): string {
183
+ const base64 = utf8ToBase64(String(input || ''));
184
+ return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
185
+ }
186
+
187
+ export function decodeBase64Url(input: string): string | undefined {
188
+ try {
189
+ const raw = String(input || '').replace(/=+$/g, '');
190
+ if (!raw) return undefined;
191
+ if (!/^[A-Za-z0-9_-]+$/.test(raw)) return undefined;
192
+
193
+ const normalized = raw.replace(/-/g, '+').replace(/_/g, '/');
194
+ const padLength = normalized.length % 4 === 0 ? 0 : 4 - (normalized.length % 4);
195
+ const padded = `${normalized}${'='.repeat(padLength)}`;
196
+ const decoded = base64ToUtf8(padded);
197
+
198
+ if (encodeBase64Url(decoded) !== raw) return undefined;
199
+ return decoded;
200
+ } catch (_error) {
201
+ return undefined;
202
+ }
203
+ }
204
+
205
+ export function isCtxDateExpression(value: unknown): value is string {
206
+ if (typeof value !== 'string') return false;
207
+ return CTX_DATE_REGEX.test(value.trim());
208
+ }
209
+
210
+ export function isCompleteCtxDatePath(pathSegments: string[]): boolean {
211
+ if (!isCtxDatePathPrefix(pathSegments)) return false;
212
+ const segments = withDatePrefix((pathSegments || []).map((seg) => String(seg)));
213
+ if (segments[0] !== 'date') return false;
214
+
215
+ if (segments[1] === 'preset') {
216
+ return segments.length === 3 && PRESET_KEYS.has(segments[2]);
217
+ }
218
+
219
+ if (segments[1] === 'relative') {
220
+ if (segments.length !== 5) return false;
221
+ return (
222
+ RELATIVE_DIRECTIONS.has(segments[2]) &&
223
+ RELATIVE_UNITS.has(segments[3]) &&
224
+ typeof parseNumberToken(segments[4]) === 'number'
225
+ );
226
+ }
227
+
228
+ if (segments[1] === 'exact' && segments[2] === 'single' && segments[3] === 'date') {
229
+ return segments.length === 5 && /^v.+/.test(segments[4]);
230
+ }
231
+
232
+ if (segments[1] === 'exact' && segments[2] === 'range' && segments[3] === 'date') {
233
+ return segments.length === 6 && /^v.+/.test(segments[4]) && /^v.+/.test(segments[5]);
234
+ }
235
+
236
+ return false;
237
+ }
238
+
239
+ export function parseCtxDateExpression(value: unknown): any {
240
+ if (!isCtxDateExpression(value)) return undefined;
241
+ const segments = withDatePrefix(parseCtxDateSegments(value as string) || []);
242
+
243
+ if (segments[1] === 'preset' && segments.length === 3 && PRESET_KEYS.has(segments[2])) {
244
+ return { type: segments[2] };
245
+ }
246
+
247
+ if (
248
+ segments[1] === 'relative' &&
249
+ segments.length === 5 &&
250
+ RELATIVE_DIRECTIONS.has(segments[2]) &&
251
+ RELATIVE_UNITS.has(segments[3])
252
+ ) {
253
+ const amount = parseNumberToken(segments[4]);
254
+ if (typeof amount === 'number') {
255
+ return { type: segments[2], unit: segments[3], number: amount };
256
+ }
257
+ return undefined;
258
+ }
259
+
260
+ if (segments[1] === 'exact' && segments[2] === 'single' && segments[3] === 'date' && segments.length === 5) {
261
+ const raw = String(segments[4] || '');
262
+ if (!raw.startsWith('v')) return undefined;
263
+ return decodeBase64Url(raw.slice(1));
264
+ }
265
+
266
+ if (segments[1] === 'exact' && segments[2] === 'range' && segments[3] === 'date' && segments.length === 6) {
267
+ const leftRaw = String(segments[4] || '');
268
+ const rightRaw = String(segments[5] || '');
269
+ if (!leftRaw.startsWith('v') || !rightRaw.startsWith('v')) return undefined;
270
+ const left = decodeBase64Url(leftRaw.slice(1));
271
+ const right = decodeBase64Url(rightRaw.slice(1));
272
+ if (typeof left === 'undefined' || typeof right === 'undefined') return undefined;
273
+ return [left, right];
274
+ }
275
+
276
+ return undefined;
277
+ }
278
+
279
+ export function serializeCtxDateValue(value: unknown): string | undefined {
280
+ if (isCtxDateExpression(value)) {
281
+ return String(value).trim();
282
+ }
283
+
284
+ if (value == null || value === '') {
285
+ return undefined;
286
+ }
287
+
288
+ if (Array.isArray(value)) {
289
+ const start = normalizeToString(value[0]);
290
+ const end = normalizeToString(value[1]);
291
+ if (start && end) {
292
+ return toCtxDateExpression([
293
+ 'date',
294
+ 'exact',
295
+ 'range',
296
+ 'date',
297
+ `v${encodeBase64Url(start)}`,
298
+ `v${encodeBase64Url(end)}`,
299
+ ]);
300
+ }
301
+ if (start) {
302
+ return toCtxDateExpression(['date', 'exact', 'single', 'date', `v${encodeBase64Url(start)}`]);
303
+ }
304
+ return undefined;
305
+ }
306
+
307
+ if (typeof value === 'object' && value) {
308
+ const typed = value as { type?: unknown; unit?: unknown; number?: unknown };
309
+ const type = typeof typed.type === 'string' ? typed.type : '';
310
+
311
+ if (type === 'past' || type === 'next') {
312
+ const unit = typeof typed.unit === 'string' && RELATIVE_UNITS.has(typed.unit) ? typed.unit : 'day';
313
+ const rawNumber = Number(typed.number);
314
+ const number = Number.isFinite(rawNumber) && rawNumber > 0 ? Math.floor(rawNumber) : 1;
315
+ return toCtxDateExpression(['date', 'relative', type, unit, `n${number}`]);
316
+ }
317
+
318
+ if (PRESET_KEYS.has(type)) {
319
+ return toCtxDateExpression(['date', 'preset', type]);
320
+ }
321
+ }
322
+
323
+ const single = normalizeToString(value);
324
+ if (single) {
325
+ return toCtxDateExpression(['date', 'exact', 'single', 'date', `v${encodeBase64Url(single)}`]);
326
+ }
327
+ return undefined;
328
+ }
329
+
330
+ export function resolveCtxDatePath(pathSegments: string[]): any {
331
+ const segments = withDatePrefix((pathSegments || []).map((seg) => String(seg)));
332
+ if (segments[0] !== 'date') return undefined;
333
+
334
+ if (segments[1] === 'preset' && segments.length === 3) {
335
+ const key = segments[2];
336
+ if (key === 'now') {
337
+ return dayjs().toISOString();
338
+ }
339
+
340
+ const now = dayjs();
341
+
342
+ if (key === 'today') return now.format('YYYY-MM-DD');
343
+ if (key === 'yesterday') return now.add(-1, 'day').format('YYYY-MM-DD');
344
+ if (key === 'tomorrow') return now.add(1, 'day').format('YYYY-MM-DD');
345
+
346
+ if (key === 'thisWeek') return startOfIsoWeek(now).format('YYYY-MM-DD');
347
+ if (key === 'lastWeek') return startOfIsoWeek(now.add(-1, 'week')).format('YYYY-MM-DD');
348
+ if (key === 'nextWeek') return startOfIsoWeek(now.add(1, 'week')).format('YYYY-MM-DD');
349
+
350
+ if (key === 'thisMonth') return now.startOf('month').format('YYYY-MM-DD');
351
+ if (key === 'lastMonth') return now.add(-1, 'month').startOf('month').format('YYYY-MM-DD');
352
+ if (key === 'nextMonth') return now.add(1, 'month').startOf('month').format('YYYY-MM-DD');
353
+
354
+ if (key === 'thisQuarter') return startOfQuarter(now).format('YYYY-MM-DD');
355
+ if (key === 'lastQuarter') return startOfQuarter(now.add(-3, 'month')).format('YYYY-MM-DD');
356
+ if (key === 'nextQuarter') return startOfQuarter(now.add(3, 'month')).format('YYYY-MM-DD');
357
+
358
+ if (key === 'thisYear') return now.startOf('year').format('YYYY-MM-DD');
359
+ if (key === 'lastYear') return now.add(-1, 'year').startOf('year').format('YYYY-MM-DD');
360
+ if (key === 'nextYear') return now.add(1, 'year').startOf('year').format('YYYY-MM-DD');
361
+
362
+ return undefined;
363
+ }
364
+
365
+ if (
366
+ segments[1] === 'relative' &&
367
+ segments.length === 5 &&
368
+ RELATIVE_DIRECTIONS.has(segments[2]) &&
369
+ RELATIVE_UNITS.has(segments[3])
370
+ ) {
371
+ const amount = parseNumberToken(segments[4]);
372
+ if (typeof amount !== 'number') return undefined;
373
+ const direction = segments[2] === 'past' ? -1 : 1;
374
+ const unit = segments[3] as dayjs.ManipulateType;
375
+ return dayjs()
376
+ .add(direction * amount, unit)
377
+ .format('YYYY-MM-DD');
378
+ }
379
+
380
+ if (segments[1] === 'exact' && segments[2] === 'single' && segments[3] === 'date' && segments.length === 5) {
381
+ const token = String(segments[4] || '');
382
+ if (!token.startsWith('v')) return undefined;
383
+ return decodeBase64Url(token.slice(1));
384
+ }
385
+
386
+ if (segments[1] === 'exact' && segments[2] === 'range' && segments[3] === 'date' && segments.length === 6) {
387
+ const leftToken = String(segments[4] || '');
388
+ const rightToken = String(segments[5] || '');
389
+ if (!leftToken.startsWith('v') || !rightToken.startsWith('v')) return undefined;
390
+ const left = decodeBase64Url(leftToken.slice(1));
391
+ const right = decodeBase64Url(rightToken.slice(1));
392
+ if (typeof left === 'undefined' || typeof right === 'undefined') return undefined;
393
+ return [left, right];
394
+ }
395
+
396
+ return undefined;
397
+ }
@@ -34,3 +34,14 @@ export class FlowExitAllException extends Error {
34
34
  this.modelUid = modelUid;
35
35
  }
36
36
  }
37
+
38
+ /**
39
+ * 取消当前保存但保持设置弹窗打开
40
+ * 用于“保存前确认”场景,用户取消时不应关闭弹窗也不应提示错误
41
+ */
42
+ export class FlowCancelSaveException extends Error {
43
+ constructor(message = 'Flow settings save cancelled.') {
44
+ super(message);
45
+ this.name = 'FlowCancelSaveException';
46
+ }
47
+ }
@@ -22,7 +22,7 @@ export {
22
22
  export { escapeT, getT, tExpr } from './translation';
23
23
 
24
24
  // 异常类
25
- export { FlowExitException } from './exceptions';
25
+ export { FlowCancelSaveException, FlowExitAllException, FlowExitException } from './exceptions';
26
26
 
27
27
  // 流程定义相关
28
28
  export { defineAction } from './flow-definitions';
@@ -34,7 +34,14 @@ export { isInheritedFrom } from './inheritance';
34
34
  export { resolveCreateModelOptions, resolveDefaultParams, resolveExpressions } from './params-resolvers';
35
35
 
36
36
  // Schema 工具
37
- export { compileUiSchema, resolveStepUiSchema, resolveUiMode, shouldHideStepInSettings } from './schema-utils';
37
+ export {
38
+ compileUiSchema,
39
+ resolveStepUiSchema,
40
+ resolveStepDisabledInSettings,
41
+ resolveUiMode,
42
+ shouldHideEventInSettings,
43
+ shouldHideStepInSettings,
44
+ } from './schema-utils';
38
45
 
39
46
  // Runtime Context Steps 设置
40
47
  export { setupRuntimeContextSteps } from './setupRuntimeContextSteps';
@@ -59,9 +66,34 @@ export { extractPropertyPath, formatPathToVariable, isVariableExpression } from
59
66
 
60
67
  export { clearAutoFlowError, getAutoFlowError, setAutoFlowError, type AutoFlowError } from './autoFlowError';
61
68
  export { parsePathnameToViewParams, type ViewParam } from './parsePathnameToViewParams';
69
+ export {
70
+ decodeBase64Url,
71
+ encodeBase64Url,
72
+ isCompleteCtxDatePath,
73
+ isCtxDatePathPrefix,
74
+ isCtxDateExpression,
75
+ parseCtxDateExpression,
76
+ resolveCtxDatePath,
77
+ serializeCtxDateValue,
78
+ } from './dateVariable';
62
79
 
63
80
  // 安全全局对象(window/document)
64
- export { createSafeDocument, createSafeWindow, createSafeNavigator } from './safeGlobals';
81
+ export {
82
+ createSafeDocument,
83
+ createSafeWindow,
84
+ createSafeNavigator,
85
+ createSafeRunJSGlobals,
86
+ runjsWithSafeGlobals,
87
+ } from './safeGlobals';
88
+
89
+ // RunJS value helpers
90
+ export { isRunJSValue, normalizeRunJSValue, extractUsedVariablePathsFromRunJS, type RunJSValue } from './runjsValue';
91
+
92
+ // RunJS helpers
93
+ export { resolveRunJSObjectValues } from './resolveRunJSObjectValues';
94
+
95
+ // RunJS 代码兼容预处理({{ }})与 JSX 编译
96
+ export { prepareRunJsCode, preprocessRunJsTemplates } from './runjsTemplateCompat';
65
97
 
66
98
  // Ephemeral context helper(用于临时注入属性/方法,避免污染父级 ctx)
67
99
  export { createEphemeralContext } from './createEphemeralContext';
@@ -69,3 +101,6 @@ export { createEphemeralContext } from './createEphemeralContext';
69
101
  // Filter helpers
70
102
  export { pruneFilter } from './pruneFilter';
71
103
  export { isBeforeRenderFlow } from './flows';
104
+
105
+ // Module URL resolver
106
+ export { resolveModuleUrl, isCssFile } from './resolveModuleUrl';
@@ -409,8 +409,13 @@ export async function preprocessExpression(expression: string, ctx: FlowContext)
409
409
  async function compileExpression<TModel extends FlowModel = FlowModel>(expression: string, ctx: FlowContext) {
410
410
  // 仅点号路径匹配:ctx.a.b.c(不支持括号/函数/索引),用于数组聚合取值
411
411
  const matchDotOnly = (expr: string): string | null => {
412
- const m = expr.trim().match(/^ctx\.([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*)$/);
413
- return m ? m[1] : null;
412
+ // 顶层变量名仍使用 JS 标识符规则(与 ctx.defineProperty 保持一致);
413
+ // 子路径允许包含 '-'(例如 formValues.oho-test.o2m-users)。
414
+ const m = expr
415
+ .trim()
416
+ .match(/^ctx\.([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\.([a-zA-Z_$][a-zA-Z0-9_$-]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$-]*)*))?$/);
417
+ if (!m) return null;
418
+ return m[2] ? `${m[1]}.${m[2]}` : m[1];
414
419
  };
415
420
 
416
421
  // 基于 getValuesByPath 的聚合取值:支持数组扁平化,仅支持 '.' 访问
@@ -423,6 +428,20 @@ async function compileExpression<TModel extends FlowModel = FlowModel>(expressio
423
428
  return getValuesByPath(base as object, segs.join('.'));
424
429
  };
425
430
 
431
+ const resolveInnerExpression = async (innerExpr: string): Promise<any> => {
432
+ const dotPath = matchDotOnly(innerExpr);
433
+ if (dotPath) {
434
+ const resolved = await resolveDotOnlyPath(dotPath);
435
+ // 当 dotPath 含 '-' 时可能与减号运算符存在歧义,例如:ctx.aa.bb-ctx.cc。
436
+ // 若按 path 解析未取到值,则回退到 JS 表达式解析,尽量保持兼容。
437
+ if (resolved === undefined && dotPath.includes('-')) {
438
+ return await processExpression(innerExpr, ctx);
439
+ }
440
+ return resolved;
441
+ }
442
+ return await processExpression(innerExpr, ctx);
443
+ };
444
+
426
445
  /**
427
446
  * 单个表达式模式匹配
428
447
  *
@@ -443,11 +462,7 @@ async function compileExpression<TModel extends FlowModel = FlowModel>(expressio
443
462
  const singleMatch = expression.match(/^\s*\{\{\s*([^{}]+?)\s*\}\}\s*$/);
444
463
  if (singleMatch) {
445
464
  const inner = singleMatch[1];
446
- const dotPath = matchDotOnly(inner);
447
- if (dotPath) {
448
- return await resolveDotOnlyPath(dotPath);
449
- }
450
- return await processExpression(inner, ctx);
465
+ return await resolveInnerExpression(inner);
451
466
  }
452
467
 
453
468
  /**
@@ -470,8 +485,7 @@ async function compileExpression<TModel extends FlowModel = FlowModel>(expressio
470
485
  let result = expression;
471
486
 
472
487
  for (const [fullMatch, innerExpr] of matches) {
473
- const dotPath = matchDotOnly(innerExpr);
474
- const value = dotPath ? await resolveDotOnlyPath(dotPath) : await processExpression(innerExpr, ctx);
488
+ const value = await resolveInnerExpression(innerExpr);
475
489
  if (value !== undefined) {
476
490
  const replacement = typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value);
477
491
  result = result.replace(fullMatch, replacement);
@@ -116,8 +116,8 @@ export const parsePathnameToViewParams = (pathname: string): ViewParam[] => {
116
116
  // 解析失败,按字符串保留
117
117
  parsed = decoded;
118
118
  }
119
- } else if (decoded && decoded.includes('=') && decoded.includes('&')) {
120
- // 形如 a=b&c=d 的整体段
119
+ } else if (decoded && /^[^=&]+=[^=&]*(?:&[^=&]+=[^=&]*)*$/.test(decoded)) {
120
+ // 形如 a=b 或 a=b&c=d 的整体段
121
121
  parsed = parseKeyValuePairs(decoded);
122
122
  }
123
123
  currentView.filterByTk = parsed;
@@ -0,0 +1,91 @@
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
+ /**
11
+ * 解析模块 URL,将相对路径转换为完整的 CDN URL
12
+ *
13
+ * @param url - 模块地址(支持相对路径或完整 URL)
14
+ * @param options - 可选配置
15
+ * @param options.addSuffix - 是否添加 ESM_CDN_SUFFIX 后缀(如 `+esm`),默认为 `true`
16
+ * @returns 解析后的完整 URL
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * // 相对路径会被拼接上 CDN 前缀和后缀(默认添加)
21
+ * // 如果使用 esm.sh(默认),不需要后缀
22
+ * resolveModuleUrl('vue@3.4.0')
23
+ * // => 'https://esm.sh/vue@3.4.0'
24
+ *
25
+ * // 如果使用 jsdelivr,需要配置 ESM_CDN_SUFFIX='/+esm'
26
+ * // resolveModuleUrl('vue@3.4.0') => 'https://cdn.jsdelivr.net/npm/vue@3.4.0/+esm'
27
+ *
28
+ * // 不添加后缀(适用于 UMD 库或 CSS 文件)
29
+ * resolveModuleUrl('vue@3.4.0', { addSuffix: false })
30
+ * // => 'https://esm.sh/vue@3.4.0' (即使配置了 suffix 也不会添加)
31
+ *
32
+ * // 原始 URL(适用于 UMD 库)
33
+ * resolveModuleUrl('lodash@4.17.21/lodash.js', { raw: true })
34
+ * // => 'https://esm.sh/lodash@4.17.21/lodash.js?raw' (即使配置了 suffix 也不会添加)
35
+ *
36
+ * // 完整 URL 保持不变
37
+ * resolveModuleUrl('https://cdn.jsdelivr.net/npm/vue@3.4.0')
38
+ * // => 'https://cdn.jsdelivr.net/npm/vue@3.4.0'
39
+ * ```
40
+ */
41
+ export function resolveModuleUrl(url: string, options?: { addSuffix?: boolean; raw?: boolean }): string {
42
+ if (!url || typeof url !== 'string') {
43
+ throw new Error('invalid url');
44
+ }
45
+
46
+ const u = url.trim();
47
+
48
+ // 如果是完整 URL(http:// 或 https://),直接返回
49
+ if (u.startsWith('http://') || u.startsWith('https://')) {
50
+ return u;
51
+ }
52
+
53
+ // 相对路径:拼接 CDN 前缀和后缀
54
+ const ESM_CDN_BASE_URL = (window as any)['__esm_cdn_base_url__'] || 'https://esm.sh';
55
+ const ESM_CDN_SUFFIX = options?.addSuffix ? (window as any)['__esm_cdn_suffix__'] || '' : '';
56
+
57
+ // 移除 base URL 末尾的斜杠,移除相对路径开头的斜杠
58
+ const base = ESM_CDN_BASE_URL.replace(/\/$/, '');
59
+ const path = u.replace(/^\//, '');
60
+
61
+ if (options?.raw) {
62
+ const sep = path.includes('?') ? '&' : '?';
63
+ return `${base}/${path}${sep}raw`;
64
+ }
65
+
66
+ return `${base}/${path}${ESM_CDN_SUFFIX}`;
67
+ }
68
+
69
+ /**
70
+ * 判断 URL 是否为 CSS 文件
71
+ *
72
+ * @param url - 文件 URL(支持带 query 和 hash,如 `example.css?v=123`)
73
+ * @returns 如果是 CSS 文件返回 `true`,否则返回 `false`
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * isCssFile('style.css') // => true
78
+ * isCssFile('style.css?v=123') // => true
79
+ * isCssFile('style.css#section') // => true
80
+ * isCssFile('script.js') // => false
81
+ * ```
82
+ */
83
+ export function isCssFile(url: string): boolean {
84
+ if (!url || typeof url !== 'string') {
85
+ return false;
86
+ }
87
+
88
+ // 去掉 query 和 hash 后判断文件扩展名
89
+ const pathPart = url.split('?')[0].split('#')[0];
90
+ return pathPart.endsWith('.css');
91
+ }
@@ -0,0 +1,46 @@
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 { isRunJSValue, normalizeRunJSValue } from './runjsValue';
11
+ import { runjsWithSafeGlobals } from './safeGlobals';
12
+
13
+ /**
14
+ * Resolve an object's values, executing any RunJSValue entries via ctx.runjs.
15
+ *
16
+ * - Skips `undefined` values
17
+ * - Skips empty RunJS code (treated as not configured)
18
+ * - Throws when a RunJS execution fails
19
+ */
20
+ export async function resolveRunJSObjectValues(ctx: unknown, raw: unknown): Promise<Record<string, any>> {
21
+ const out: Record<string, any> = {};
22
+
23
+ if (!raw || typeof raw !== 'object') return out;
24
+ if (Array.isArray(raw)) return out;
25
+
26
+ for (const [key, value] of Object.entries(raw as Record<string, any>)) {
27
+ if (typeof value === 'undefined') continue;
28
+
29
+ if (isRunJSValue(value)) {
30
+ const { code, version } = normalizeRunJSValue(value);
31
+ if (!code.trim()) continue;
32
+ const ret = await runjsWithSafeGlobals(ctx, code, { version });
33
+ if (!ret?.success) {
34
+ throw new Error(`RunJS execution failed for "${key}"`);
35
+ }
36
+ if (typeof ret.value !== 'undefined') {
37
+ out[key] = ret.value;
38
+ }
39
+ continue;
40
+ }
41
+
42
+ out[key] = value;
43
+ }
44
+
45
+ return out;
46
+ }