@nocobase/flow-engine 2.0.0-beta.9 → 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
@@ -0,0 +1,828 @@
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 { compileRunJs } from './jsxTransform';
11
+
12
+ type PrepareRunJsCodeOptions = {
13
+ preprocessTemplates?: boolean;
14
+ };
15
+
16
+ const RESOLVE_JSON_TEMPLATE_CALL = 'ctx.resolveJsonTemplate';
17
+ const CTX_TEMPLATE_MARKER_RE = /\{\{\s*ctx(?:\.|\[|\?\.)/;
18
+ const CTX_LIBS_MARKER_RE = /\bctx(?:\?\.|\.)libs\b/;
19
+ const STRINGIFY_HELPER_BASE_NAME = '__runjs_templateValueToString';
20
+ const BARE_PLACEHOLDER_VAR_RE = /\b__runjs_ctx_tpl_\d+\b/;
21
+ const STRINGIFY_HELPER_RE = /\b__runjs_templateValueToString(?:_\d+)?\b/;
22
+ const PREPROCESSED_MARKER_RE = /\b__runjs_ctx_tpl_\d+\b|\b__runjs_templateValueToString(?:_\d+)?\b/;
23
+ const ENSURE_LIBS_MARKER_RE = /\b__runjs_ensure_libs\b/;
24
+ const PREPARE_RUNJS_CODE_CACHE_LIMIT = 256;
25
+
26
+ const PREPARE_RUNJS_CODE_CACHE = {
27
+ withTemplates: new Map<string, Promise<string>>(),
28
+ withoutTemplates: new Map<string, Promise<string>>(),
29
+ };
30
+
31
+ function lruGet<V>(map: Map<string, V>, key: string): V | undefined {
32
+ const v = map.get(key);
33
+ if (typeof v === 'undefined') return undefined;
34
+ map.delete(key);
35
+ map.set(key, v);
36
+ return v;
37
+ }
38
+
39
+ function lruSet<V>(map: Map<string, V>, key: string, value: V, limit: number): void {
40
+ if (map.has(key)) map.delete(key);
41
+ map.set(key, value);
42
+ if (map.size <= limit) return;
43
+ const oldestKey = map.keys().next().value;
44
+ if (typeof oldestKey !== 'undefined') map.delete(oldestKey);
45
+ }
46
+
47
+ function isIdentChar(ch: string | undefined): boolean {
48
+ return !!ch && /[A-Za-z0-9_$]/.test(ch);
49
+ }
50
+
51
+ function isCtxTemplatePlaceholder(placeholder: string): boolean {
52
+ if (!placeholder.startsWith('{{') || !placeholder.endsWith('}}')) return false;
53
+ const inner = placeholder.slice(2, -2).trim();
54
+ return inner.startsWith('ctx.') || inner.startsWith('ctx[') || inner.startsWith('ctx?.');
55
+ }
56
+
57
+ function readsResolveJsonTemplateCall(code: string, index: number): boolean {
58
+ if (!code.startsWith(RESOLVE_JSON_TEMPLATE_CALL, index)) return false;
59
+ const before = index > 0 ? code[index - 1] : '';
60
+ const after = code[index + RESOLVE_JSON_TEMPLATE_CALL.length] || '';
61
+ if (isIdentChar(before) || isIdentChar(after)) return false;
62
+ return true;
63
+ }
64
+
65
+ function readLineComment(code: string, start: number): number {
66
+ let i = start + 2;
67
+ while (i < code.length) {
68
+ const ch = code[i];
69
+ i += 1;
70
+ if (ch === '\n') break;
71
+ }
72
+ return i;
73
+ }
74
+
75
+ function readBlockComment(code: string, start: number): number {
76
+ let i = start + 2;
77
+ while (i < code.length) {
78
+ const ch = code[i];
79
+ const next = code[i + 1];
80
+ if (ch === '*' && next === '/') {
81
+ return i + 2;
82
+ }
83
+ i += 1;
84
+ }
85
+ return i;
86
+ }
87
+
88
+ function readQuotedString(code: string, start: number, quote: "'" | '"'): number {
89
+ let i = start + 1;
90
+ while (i < code.length) {
91
+ const ch = code[i];
92
+ if (ch === '\\') {
93
+ i += 2;
94
+ continue;
95
+ }
96
+ i += 1;
97
+ if (ch === quote) break;
98
+ }
99
+ return i;
100
+ }
101
+
102
+ function readTemplateLiteral(code: string, start: number): number {
103
+ let i = start + 1;
104
+ while (i < code.length) {
105
+ const ch = code[i];
106
+ if (ch === '\\') {
107
+ i += 2;
108
+ continue;
109
+ }
110
+ if (ch === '`') {
111
+ return i + 1;
112
+ }
113
+ if (ch === '$' && code[i + 1] === '{') {
114
+ i += 2;
115
+ let depth = 1;
116
+ while (i < code.length && depth > 0) {
117
+ const c = code[i];
118
+ const n = code[i + 1];
119
+ if (c === "'" || c === '"') {
120
+ i = readQuotedString(code, i, c);
121
+ continue;
122
+ }
123
+ if (c === '`') {
124
+ i = readTemplateLiteral(code, i);
125
+ continue;
126
+ }
127
+ if (c === '/' && n === '/') {
128
+ i = readLineComment(code, i);
129
+ continue;
130
+ }
131
+ if (c === '/' && n === '*') {
132
+ i = readBlockComment(code, i);
133
+ continue;
134
+ }
135
+ if (c === '{') depth += 1;
136
+ else if (c === '}') depth -= 1;
137
+ i += 1;
138
+ }
139
+ continue;
140
+ }
141
+ i += 1;
142
+ }
143
+ return i;
144
+ }
145
+
146
+ function extractCtxPlaceholders(source: string): string[] {
147
+ if (!CTX_TEMPLATE_MARKER_RE.test(source)) return [];
148
+ const out: string[] = [];
149
+ let idx = 0;
150
+ while (idx < source.length) {
151
+ const start = source.indexOf('{{', idx);
152
+ if (start === -1) break;
153
+ const end = source.indexOf('}}', start + 2);
154
+ if (end === -1) break;
155
+ const placeholder = source.slice(start, end + 2);
156
+ if (isCtxTemplatePlaceholder(placeholder)) out.push(placeholder);
157
+ idx = end + 2;
158
+ }
159
+ return out;
160
+ }
161
+
162
+ function skipWhitespaceForward(code: string, start: number): number {
163
+ let i = start;
164
+ while (i < code.length && /\s/.test(code[i])) i += 1;
165
+ return i;
166
+ }
167
+
168
+ function skipWhitespaceBackward(code: string, start: number): number {
169
+ let i = start;
170
+ while (i >= 0 && /\s/.test(code[i])) i -= 1;
171
+ return i;
172
+ }
173
+
174
+ function skipSpaceAndCommentsForward(code: string, start: number): number {
175
+ let i = start;
176
+ for (;;) {
177
+ i = skipWhitespaceForward(code, i);
178
+ const ch = code[i];
179
+ const next = code[i + 1];
180
+ if (ch === '/' && next === '/') {
181
+ i = readLineComment(code, i);
182
+ continue;
183
+ }
184
+ if (ch === '/' && next === '*') {
185
+ i = readBlockComment(code, i);
186
+ continue;
187
+ }
188
+ return i;
189
+ }
190
+ }
191
+
192
+ function isIdentStartChar(ch: string | undefined): boolean {
193
+ return !!ch && /[A-Za-z_$]/.test(ch);
194
+ }
195
+
196
+ function readIdentifier(code: string, start: number): { name: string; end: number } | null {
197
+ const first = code[start];
198
+ if (!isIdentStartChar(first)) return null;
199
+ let i = start + 1;
200
+ while (i < code.length && isIdentChar(code[i])) i += 1;
201
+ return { name: code.slice(start, i), end: i };
202
+ }
203
+
204
+ function readSimpleStringLiteralValue(
205
+ code: string,
206
+ start: number,
207
+ quote: "'" | '"',
208
+ ): { value: string; end: number } | null {
209
+ if (code[start] !== quote) return null;
210
+ let i = start + 1;
211
+ let value = '';
212
+ while (i < code.length) {
213
+ const ch = code[i];
214
+ if (ch === '\\') {
215
+ const n = code[i + 1];
216
+ if (typeof n === 'undefined') break;
217
+ value += n;
218
+ i += 2;
219
+ continue;
220
+ }
221
+ if (ch === quote) {
222
+ return { value, end: i + 1 };
223
+ }
224
+ value += ch;
225
+ i += 1;
226
+ }
227
+ return null;
228
+ }
229
+
230
+ function readsCtxLibsBase(code: string, index: number): number | null {
231
+ if (!code.startsWith('ctx', index)) return null;
232
+ const before = index > 0 ? code[index - 1] : '';
233
+ const after = code[index + 3] || '';
234
+ if (isIdentChar(before) || isIdentChar(after)) return null;
235
+
236
+ const tail = code.slice(index + 3);
237
+ if (tail.startsWith('.libs')) return index + 3 + 5; // ".libs"
238
+ if (tail.startsWith('?.libs')) return index + 3 + 6; // "?.libs"
239
+ return null;
240
+ }
241
+
242
+ function tryReadCtxLibAccess(code: string, index: number): { end: number; key?: string } | null {
243
+ const baseEnd = readsCtxLibsBase(code, index);
244
+ if (baseEnd === null) return null;
245
+ let i = skipSpaceAndCommentsForward(code, baseEnd);
246
+
247
+ const ch = code[i];
248
+ const next = code[i + 1];
249
+ const next2 = code[i + 2];
250
+
251
+ // Optional chaining computed: ctx.libs?.['x']
252
+ if (ch === '?' && next === '.' && next2 === '[') {
253
+ i = skipSpaceAndCommentsForward(code, i + 3);
254
+ const q = code[i];
255
+ if (q === "'" || q === '"') {
256
+ const parsed = readSimpleStringLiteralValue(code, i, q);
257
+ if (!parsed) return { end: i + 1 };
258
+ let j = skipSpaceAndCommentsForward(code, parsed.end);
259
+ if (code[j] === ']') j += 1;
260
+ return { end: j, key: parsed.value };
261
+ }
262
+ return { end: i };
263
+ }
264
+
265
+ // Computed: ctx.libs['x']
266
+ if (ch === '[') {
267
+ i = skipSpaceAndCommentsForward(code, i + 1);
268
+ const q = code[i];
269
+ if (q === "'" || q === '"') {
270
+ const parsed = readSimpleStringLiteralValue(code, i, q);
271
+ if (!parsed) return { end: i + 1 };
272
+ let j = skipSpaceAndCommentsForward(code, parsed.end);
273
+ if (code[j] === ']') j += 1;
274
+ return { end: j, key: parsed.value };
275
+ }
276
+ return { end: i };
277
+ }
278
+
279
+ // Optional chaining dot: ctx.libs?.x
280
+ if (ch === '?' && next === '.') {
281
+ i = skipSpaceAndCommentsForward(code, i + 2);
282
+ const ident = readIdentifier(code, i);
283
+ if (!ident) return { end: i };
284
+ return { end: ident.end, key: ident.name };
285
+ }
286
+
287
+ // Dot: ctx.libs.x
288
+ if (ch === '.') {
289
+ i = skipSpaceAndCommentsForward(code, i + 1);
290
+ const ident = readIdentifier(code, i);
291
+ if (!ident) return { end: i };
292
+ return { end: ident.end, key: ident.name };
293
+ }
294
+
295
+ return { end: baseEnd };
296
+ }
297
+
298
+ function parseDestructuredKeysFromObjectPattern(pattern: string): string[] {
299
+ const out: string[] = [];
300
+ const src = String(pattern ?? '');
301
+ let itemStart = 0;
302
+ let braceDepth = 0;
303
+ let bracketDepth = 0;
304
+ let parenDepth = 0;
305
+
306
+ const pushItem = (raw: string) => {
307
+ const s = String(raw ?? '').trim();
308
+ if (!s) return;
309
+ if (s.startsWith('...')) return;
310
+
311
+ // Find first top-level ":" or "=" to isolate key part.
312
+ let keyPart = s;
313
+ let i = 0;
314
+ let b = 0;
315
+ let br = 0;
316
+ let p = 0;
317
+ while (i < s.length) {
318
+ const ch = s[i];
319
+ const next = s[i + 1];
320
+
321
+ if (ch === '/' && next === '/') {
322
+ // comment until end
323
+ break;
324
+ }
325
+ if (ch === '/' && next === '*') {
326
+ const end = s.indexOf('*/', i + 2);
327
+ i = end === -1 ? s.length : end + 2;
328
+ continue;
329
+ }
330
+ if (ch === "'" || ch === '"') {
331
+ // skip strings inside default expressions
332
+ let j = i + 1;
333
+ while (j < s.length) {
334
+ const c = s[j];
335
+ if (c === '\\') {
336
+ j += 2;
337
+ continue;
338
+ }
339
+ j += 1;
340
+ if (c === ch) break;
341
+ }
342
+ i = j;
343
+ continue;
344
+ }
345
+ if (ch === '`') {
346
+ // skip template literal (including ${} roughly by skipping to next backtick)
347
+ let j = i + 1;
348
+ while (j < s.length) {
349
+ const c = s[j];
350
+ if (c === '\\') {
351
+ j += 2;
352
+ continue;
353
+ }
354
+ j += 1;
355
+ if (c === '`') break;
356
+ }
357
+ i = j;
358
+ continue;
359
+ }
360
+
361
+ if (ch === '{') b += 1;
362
+ else if (ch === '}') b -= 1;
363
+ else if (ch === '[') br += 1;
364
+ else if (ch === ']') br -= 1;
365
+ else if (ch === '(') p += 1;
366
+ else if (ch === ')') p -= 1;
367
+
368
+ if (b === 0 && br === 0 && p === 0) {
369
+ if (ch === ':') {
370
+ keyPart = s.slice(0, i).trim();
371
+ break;
372
+ }
373
+ if (ch === '=') {
374
+ keyPart = s.slice(0, i).trim();
375
+ break;
376
+ }
377
+ }
378
+
379
+ i += 1;
380
+ }
381
+
382
+ if (!keyPart) return;
383
+ if (keyPart.startsWith("'") || keyPart.startsWith('"')) {
384
+ const q = keyPart[0] as "'" | '"';
385
+ const parsed = readSimpleStringLiteralValue(keyPart, 0, q);
386
+ if (parsed) out.push(parsed.value);
387
+ return;
388
+ }
389
+
390
+ const m = keyPart.match(/^[A-Za-z_$][A-Za-z0-9_$]*/);
391
+ if (m) out.push(m[0]);
392
+ };
393
+
394
+ let i = 0;
395
+ while (i < src.length) {
396
+ const ch = src[i];
397
+ const next = src[i + 1];
398
+ if (ch === '/' && next === '/') {
399
+ break;
400
+ }
401
+ if (ch === '/' && next === '*') {
402
+ const end = src.indexOf('*/', i + 2);
403
+ i = end === -1 ? src.length : end + 2;
404
+ continue;
405
+ }
406
+ if (ch === "'" || ch === '"') {
407
+ i = readQuotedString(src, i, ch);
408
+ continue;
409
+ }
410
+ if (ch === '`') {
411
+ i = readTemplateLiteral(src, i);
412
+ continue;
413
+ }
414
+
415
+ if (ch === '{') braceDepth += 1;
416
+ else if (ch === '}') braceDepth -= 1;
417
+ else if (ch === '[') bracketDepth += 1;
418
+ else if (ch === ']') bracketDepth -= 1;
419
+ else if (ch === '(') parenDepth += 1;
420
+ else if (ch === ')') parenDepth -= 1;
421
+
422
+ if (braceDepth === 0 && bracketDepth === 0 && parenDepth === 0 && ch === ',') {
423
+ pushItem(src.slice(itemStart, i));
424
+ itemStart = i + 1;
425
+ }
426
+
427
+ i += 1;
428
+ }
429
+ pushItem(src.slice(itemStart));
430
+
431
+ return out;
432
+ }
433
+
434
+ function extractUsedCtxLibKeys(code: string): string[] {
435
+ if (!CTX_LIBS_MARKER_RE.test(code)) return [];
436
+ const out = new Set<string>();
437
+
438
+ const scanTemplateExpression = (start: number): number => {
439
+ let i = start;
440
+ let braceDepth = 1;
441
+ while (i < code.length && braceDepth > 0) {
442
+ const ch = code[i];
443
+ const next = code[i + 1];
444
+ if (ch === '/' && next === '/') {
445
+ i = readLineComment(code, i);
446
+ continue;
447
+ }
448
+ if (ch === '/' && next === '*') {
449
+ i = readBlockComment(code, i);
450
+ continue;
451
+ }
452
+ if (ch === "'" || ch === '"') {
453
+ i = readQuotedString(code, i, ch);
454
+ continue;
455
+ }
456
+ if (ch === '`') {
457
+ i = scanTemplateLiteral(i + 1);
458
+ continue;
459
+ }
460
+
461
+ const access = tryReadCtxLibAccess(code, i);
462
+ if (access) {
463
+ if (access.key) out.add(access.key);
464
+ i = access.end;
465
+ continue;
466
+ }
467
+
468
+ if (ch === '{') braceDepth += 1;
469
+ else if (ch === '}') braceDepth -= 1;
470
+
471
+ i += 1;
472
+ }
473
+ return i;
474
+ };
475
+
476
+ const scanTemplateLiteral = (start: number): number => {
477
+ let i = start;
478
+ while (i < code.length) {
479
+ const ch = code[i];
480
+ const next = code[i + 1];
481
+ if (ch === '\\') {
482
+ i += 2;
483
+ continue;
484
+ }
485
+ if (ch === '`') return i + 1;
486
+ if (ch === '$' && next === '{') {
487
+ i = scanTemplateExpression(i + 2);
488
+ continue;
489
+ }
490
+ i += 1;
491
+ }
492
+ return i;
493
+ };
494
+
495
+ let i = 0;
496
+ while (i < code.length) {
497
+ const ch = code[i];
498
+ const next = code[i + 1];
499
+ if (ch === '/' && next === '/') {
500
+ i = readLineComment(code, i);
501
+ continue;
502
+ }
503
+ if (ch === '/' && next === '*') {
504
+ i = readBlockComment(code, i);
505
+ continue;
506
+ }
507
+ if (ch === "'" || ch === '"') {
508
+ i = readQuotedString(code, i, ch);
509
+ continue;
510
+ }
511
+ if (ch === '`') {
512
+ i = scanTemplateLiteral(i + 1);
513
+ continue;
514
+ }
515
+
516
+ const access = tryReadCtxLibAccess(code, i);
517
+ if (access) {
518
+ if (access.key) out.add(access.key);
519
+ i = access.end;
520
+ continue;
521
+ }
522
+
523
+ // Destructuring: { a, b } = ctx.libs
524
+ if (ch === '=' && next !== '=' && next !== '>' && next !== '<') {
525
+ const right = skipSpaceAndCommentsForward(code, i + 1);
526
+ const rhsBaseEnd = readsCtxLibsBase(code, right);
527
+ if (rhsBaseEnd !== null) {
528
+ const leftEnd = skipWhitespaceBackward(code, i - 1);
529
+ if (code[leftEnd] === '}') {
530
+ let depth = 0;
531
+ let j = leftEnd;
532
+ while (j >= 0) {
533
+ const c = code[j];
534
+ if (c === '}') depth += 1;
535
+ else if (c === '{') {
536
+ depth -= 1;
537
+ if (depth === 0) break;
538
+ }
539
+ j -= 1;
540
+ }
541
+ if (j >= 0 && code[j] === '{') {
542
+ const inner = code.slice(j + 1, leftEnd);
543
+ for (const k of parseDestructuredKeysFromObjectPattern(inner)) out.add(k);
544
+ }
545
+ }
546
+ }
547
+ }
548
+
549
+ i += 1;
550
+ }
551
+
552
+ return Array.from(out);
553
+ }
554
+
555
+ function injectEnsureLibsPreamble(code: string): string {
556
+ if (!CTX_LIBS_MARKER_RE.test(code)) return code;
557
+ if (ENSURE_LIBS_MARKER_RE.test(code)) return code;
558
+ const keys = extractUsedCtxLibKeys(code);
559
+ if (!keys.length) return code;
560
+ return `/* __runjs_ensure_libs */\nawait ctx.__ensureLibs(${JSON.stringify(keys)});\n${code}`;
561
+ }
562
+
563
+ function isObjectLikeKeyPosition(code: string, tokenStart: number, tokenEnd: number): boolean {
564
+ const next = skipWhitespaceForward(code, tokenEnd);
565
+ if (code[next] !== ':') return false;
566
+ const prev = skipWhitespaceBackward(code, tokenStart - 1);
567
+ const prevCh = prev >= 0 ? code[prev] : '';
568
+ return prevCh === '{' || prevCh === ',';
569
+ }
570
+
571
+ function wrapStringTokenWithReplacements(
572
+ token: string,
573
+ placeholders: string[],
574
+ placeholderVar: (p: string) => string,
575
+ stringifyHelperName: string,
576
+ ): string {
577
+ const uniq = Array.from(new Set(placeholders));
578
+ if (!uniq.length) return token;
579
+
580
+ // 通过运行时(同步)字符串化 helper,避免在嵌套函数里注入 `await`。
581
+ // 保持旧语义:当值为 undefined 时,不替换模板标记本身。
582
+ let expr = token;
583
+ for (const p of uniq) {
584
+ const v = placeholderVar(p);
585
+ expr = `(${expr}).split(${JSON.stringify(p)}).join(${stringifyHelperName}(${v}, ${JSON.stringify(p)}))`;
586
+ }
587
+ return expr;
588
+ }
589
+
590
+ /**
591
+ * 预处理 RunJS 源码,兼容旧版 `{{ ... }}` 占位符。
592
+ *
593
+ * 注意:这不是“变量解析”(这里不会计算任何值)。
594
+ * 它会把代码重写为合法的 JS,并在执行期间调用 `ctx.resolveJsonTemplate(...)` 来解析 {{ ... }}。
595
+ *
596
+ * 设计说明:
597
+ * - 为避免出现 “await is only valid in async functions” 之类的语法错误,会把所有模板解析提升到
598
+ * RunJS 程序的顶层,再用变量替换各处的出现位置。
599
+ * - 对于字符串/模板字面量,不在原位置直接包一层 `await ...`。而是保留原字面量表达式,并通过
600
+ * 一个小 helper 用 `.split().join()` 做替换,以匹配 `resolveJsonTemplate` 对 object/undefined
601
+ * 的字符串替换行为。
602
+ */
603
+ export function preprocessRunJsTemplates(
604
+ code: string,
605
+ options: {
606
+ processBarePlaceholders?: boolean;
607
+ processStringLiterals?: boolean;
608
+ } = {},
609
+ ): string {
610
+ if (!CTX_TEMPLATE_MARKER_RE.test(code)) return code;
611
+ const processBarePlaceholders = options.processBarePlaceholders !== false;
612
+ const processStringLiterals = options.processStringLiterals !== false;
613
+ // 避免重复预处理(例如调用方不小心把已处理过的代码再次传回 ctx.runjs)。
614
+ // 这里使用启发式判断;这些内部符号名刻意设计得不太可能与用户代码冲突。
615
+ if (processBarePlaceholders && processStringLiterals && PREPROCESSED_MARKER_RE.test(code)) return code;
616
+ if (processBarePlaceholders && !processStringLiterals && BARE_PLACEHOLDER_VAR_RE.test(code)) return code;
617
+ if (!processBarePlaceholders && processStringLiterals && STRINGIFY_HELPER_RE.test(code)) return code;
618
+
619
+ const placeholderVars = new Map<string, string>();
620
+ const preambleLines: string[] = [];
621
+ let needsStringifyHelper = false;
622
+
623
+ const usedNames = new Set<string>();
624
+ const pickUniqueName = (base: string): string => {
625
+ if (!code.includes(base) && !usedNames.has(base)) {
626
+ usedNames.add(base);
627
+ return base;
628
+ }
629
+ let i = 1;
630
+ while (code.includes(`${base}_${i}`) || usedNames.has(`${base}_${i}`)) i += 1;
631
+ const name = `${base}_${i}`;
632
+ usedNames.add(name);
633
+ return name;
634
+ };
635
+ const stringifyHelperName = pickUniqueName(STRINGIFY_HELPER_BASE_NAME);
636
+
637
+ let placeholderCounter = 0;
638
+ const nextPlaceholderVarName = (): string => {
639
+ for (;;) {
640
+ const name = `__runjs_ctx_tpl_${placeholderCounter}`;
641
+ placeholderCounter += 1;
642
+ if (code.includes(name) || usedNames.has(name)) continue;
643
+ usedNames.add(name);
644
+ return name;
645
+ }
646
+ };
647
+
648
+ const getPlaceholderVar = (placeholder: string): string => {
649
+ const existing = placeholderVars.get(placeholder);
650
+ if (existing) return existing;
651
+ const name = nextPlaceholderVarName();
652
+ placeholderVars.set(placeholder, name);
653
+ preambleLines.push(`const ${name} = await ctx.resolveJsonTemplate(${JSON.stringify(placeholder)});`);
654
+ return name;
655
+ };
656
+
657
+ let out = '';
658
+ let i = 0;
659
+
660
+ // 显式的 `ctx.resolveJsonTemplate(...)` 调用内部不做处理。
661
+ let resolveCallPending = false;
662
+ let resolveParenDepth = 0;
663
+
664
+ while (i < code.length) {
665
+ const ch = code[i];
666
+ const next = code[i + 1];
667
+
668
+ // 注释(始终保留;内部不做任何转换)
669
+ if (ch === '/' && next === '/') {
670
+ const end = readLineComment(code, i);
671
+ out += code.slice(i, end);
672
+ i = end;
673
+ continue;
674
+ }
675
+ if (ch === '/' && next === '*') {
676
+ const end = readBlockComment(code, i);
677
+ out += code.slice(i, end);
678
+ i = end;
679
+ continue;
680
+ }
681
+
682
+ // 字符串/模板字面量(可选转换,但在 resolveJsonTemplate 调用内部永不转换)
683
+ if (ch === "'" || ch === '"') {
684
+ const end = readQuotedString(code, i, ch);
685
+ const token = code.slice(i, end);
686
+ if (processStringLiterals && resolveParenDepth === 0) {
687
+ const placeholders = extractCtxPlaceholders(token);
688
+ if (placeholders.length) {
689
+ needsStringifyHelper = true;
690
+ const expr = wrapStringTokenWithReplacements(token, placeholders, getPlaceholderVar, stringifyHelperName);
691
+ out += isObjectLikeKeyPosition(code, i, end) ? `[${expr}]` : expr;
692
+ } else {
693
+ out += token;
694
+ }
695
+ } else out += token;
696
+ i = end;
697
+ continue;
698
+ }
699
+ if (ch === '`') {
700
+ const end = readTemplateLiteral(code, i);
701
+ const token = code.slice(i, end);
702
+ if (processStringLiterals && resolveParenDepth === 0) {
703
+ const placeholders = extractCtxPlaceholders(token);
704
+ if (placeholders.length) {
705
+ needsStringifyHelper = true;
706
+ const expr = wrapStringTokenWithReplacements(token, placeholders, getPlaceholderVar, stringifyHelperName);
707
+ out += isObjectLikeKeyPosition(code, i, end) ? `[${expr}]` : expr;
708
+ } else {
709
+ out += token;
710
+ }
711
+ } else out += token;
712
+ i = end;
713
+ continue;
714
+ }
715
+
716
+ // 跟踪 `ctx.resolveJsonTemplate(` 的调用区域
717
+ if (resolveParenDepth === 0 && !resolveCallPending && readsResolveJsonTemplateCall(code, i)) {
718
+ out += RESOLVE_JSON_TEMPLATE_CALL;
719
+ i += RESOLVE_JSON_TEMPLATE_CALL.length;
720
+ resolveCallPending = true;
721
+ continue;
722
+ }
723
+ if (resolveCallPending) {
724
+ if (/\s/.test(ch)) {
725
+ out += ch;
726
+ i += 1;
727
+ continue;
728
+ }
729
+ if (ch === '(') {
730
+ out += ch;
731
+ i += 1;
732
+ resolveCallPending = false;
733
+ resolveParenDepth = 1;
734
+ continue;
735
+ }
736
+ // 不是调用;重置状态并按正常逻辑重新处理当前字符
737
+ resolveCallPending = false;
738
+ continue;
739
+ }
740
+
741
+ // 在 resolveJsonTemplate(...) 内部,仅跟踪括号深度并原样输出
742
+ if (resolveParenDepth > 0) {
743
+ if (ch === '(') resolveParenDepth += 1;
744
+ else if (ch === ')') resolveParenDepth -= 1;
745
+ out += ch;
746
+ i += 1;
747
+ continue;
748
+ }
749
+
750
+ if (processBarePlaceholders) {
751
+ // 裸的 {{ ... }} 占位符 -> 运行时 await resolveJsonTemplate("{{...}}")
752
+ if (ch === '{' && next === '{') {
753
+ const end = code.indexOf('}}', i + 2);
754
+ if (end !== -1) {
755
+ const placeholder = code.slice(i, end + 2);
756
+ if (isCtxTemplatePlaceholder(placeholder)) {
757
+ out += getPlaceholderVar(placeholder);
758
+ i = end + 2;
759
+ continue;
760
+ }
761
+ }
762
+ }
763
+ }
764
+
765
+ out += ch;
766
+ i += 1;
767
+ }
768
+
769
+ if (!preambleLines.length) return out;
770
+
771
+ const preamble: string[] = [];
772
+ if (needsStringifyHelper) {
773
+ preamble.push(
774
+ `const ${stringifyHelperName} = (value, placeholder) => {`,
775
+ ` if (typeof value === 'undefined') return placeholder;`,
776
+ ` if (typeof value === 'object' && value !== null) {`,
777
+ ` try {`,
778
+ ` return JSON.stringify(value);`,
779
+ ` } catch (e) {`,
780
+ ` return String(value);`,
781
+ ` }`,
782
+ ` }`,
783
+ ` return String(value);`,
784
+ `};`,
785
+ );
786
+ }
787
+ preamble.push(...preambleLines);
788
+
789
+ return `${preamble.join('\n')}\n${out}`;
790
+ }
791
+
792
+ /**
793
+ * 为执行准备用户的 RunJS 源码。
794
+ * - 可选:运行时模板兼容重写
795
+ * - JSX 转换(sucrase),确保 RunJS 可以安全使用 JSX
796
+ */
797
+ export async function prepareRunJsCode(code: string, options: PrepareRunJsCodeOptions = {}): Promise<string> {
798
+ const src = typeof code === 'string' ? code : String(code ?? '');
799
+ const preprocessTemplates = !!options.preprocessTemplates;
800
+ const cache = preprocessTemplates
801
+ ? PREPARE_RUNJS_CODE_CACHE.withTemplates
802
+ : PREPARE_RUNJS_CODE_CACHE.withoutTemplates;
803
+ const cached = lruGet(cache, src);
804
+ if (cached) return await cached;
805
+
806
+ const task = (async () => {
807
+ if (!preprocessTemplates) return injectEnsureLibsPreamble(await compileRunJs(src));
808
+
809
+ // 阶段 1:仅重写“裸”的 {{ ... }} 占位符,保持代码可解析(尤其是在 JSX 转换前)。
810
+ const preBare = preprocessRunJsTemplates(src, { processStringLiterals: false });
811
+ // 阶段 2:JSX -> JS。
812
+ const jsxCompiled = await compileRunJs(preBare);
813
+ // 阶段 3:对纯 JS 输出重写字符串/模板字面量(避免破坏 JSX 属性语法)。
814
+ const out = preprocessRunJsTemplates(jsxCompiled, { processBarePlaceholders: false });
815
+ // 阶段 4:为 ctx.libs 注入按需加载 preamble(保持用户同步访问语义)。
816
+ return injectEnsureLibsPreamble(out);
817
+ })();
818
+
819
+ lruSet(cache, src, task, PREPARE_RUNJS_CODE_CACHE_LIMIT);
820
+
821
+ try {
822
+ return await task;
823
+ } catch (e) {
824
+ // Avoid poisoning cache if unexpected errors happen
825
+ cache.delete(src);
826
+ throw e;
827
+ }
828
+ }