@nocobase/flow-engine 2.0.0-alpha.25 → 2.0.0-alpha.26

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 (29) hide show
  1. package/lib/flowContext.js +45 -0
  2. package/lib/index.d.ts +1 -0
  3. package/lib/index.js +3 -0
  4. package/lib/models/flowModel.js +3 -3
  5. package/lib/runjs-context/contexts/base.js +14 -0
  6. package/lib/runjs-context/snippets/index.js +1 -0
  7. package/lib/runjs-context/snippets/scene/block/render-button-handler.snippet.js +6 -4
  8. package/lib/runjs-context/snippets/scene/block/render-info-card.snippet.js +15 -16
  9. package/lib/runjs-context/snippets/scene/block/render-react-jsx.snippet.d.ts +11 -0
  10. package/lib/runjs-context/snippets/scene/block/render-react-jsx.snippet.js +58 -0
  11. package/lib/runjs-context/snippets/scene/block/render-react.snippet.js +7 -7
  12. package/lib/runjs-context/snippets/scene/block/render-statistics.snippet.js +23 -28
  13. package/lib/runjs-context/snippets/scene/block/render-timeline.snippet.js +19 -20
  14. package/lib/utils/jsxTransform.d.ts +15 -0
  15. package/lib/utils/jsxTransform.js +68 -0
  16. package/package.json +5 -4
  17. package/src/flowContext.ts +63 -0
  18. package/src/index.ts +1 -0
  19. package/src/models/flowModel.tsx +2 -2
  20. package/src/runjs-context/contexts/base.ts +16 -0
  21. package/src/runjs-context/snippets/index.ts +1 -0
  22. package/src/runjs-context/snippets/scene/block/render-button-handler.snippet.ts +6 -4
  23. package/src/runjs-context/snippets/scene/block/render-info-card.snippet.ts +15 -16
  24. package/src/runjs-context/snippets/scene/block/render-react-jsx.snippet.ts +39 -0
  25. package/src/runjs-context/snippets/scene/block/render-react.snippet.ts +7 -7
  26. package/src/runjs-context/snippets/scene/block/render-statistics.snippet.ts +23 -28
  27. package/src/runjs-context/snippets/scene/block/render-timeline.snippet.ts +19 -20
  28. package/src/utils/__tests__/jsxTransform.test.ts +38 -0
  29. package/src/utils/jsxTransform.ts +39 -0
@@ -61,6 +61,7 @@ var import_lodash = __toESM(require("lodash"));
61
61
  var import_qs = __toESM(require("qs"));
62
62
  var import_react = __toESM(require("react"));
63
63
  var ReactDOMClient = __toESM(require("react-dom/client"));
64
+ var import_ElementProxy = require("./ElementProxy");
64
65
  var import_Acl = require("./acl/Acl");
65
66
  var import_ContextPathProxy = require("./ContextPathProxy");
66
67
  var import_data_source = require("./data-source");
@@ -1280,6 +1281,50 @@ const _FlowRunJSContext = class _FlowRunJSContext extends FlowContext {
1280
1281
  }, "createRoot")
1281
1282
  };
1282
1283
  this.defineProperty("ReactDOM", { value: ReactDOMShim });
1284
+ this.defineMethod(
1285
+ "render",
1286
+ function(vnode, container) {
1287
+ const el = container || this.element;
1288
+ if (!el) throw new Error("ctx.render: container not provided and ctx.element is not available");
1289
+ const containerEl = (el == null ? void 0 : el.__el) || el;
1290
+ const globalRef = globalThis;
1291
+ globalRef.__nbRunjsRoots = globalRef.__nbRunjsRoots || /* @__PURE__ */ new WeakMap();
1292
+ const rootMap = globalRef.__nbRunjsRoots;
1293
+ if (typeof vnode === "string") {
1294
+ const existingRoot = rootMap.get(containerEl);
1295
+ if (existingRoot && typeof existingRoot.unmount === "function") {
1296
+ try {
1297
+ existingRoot.unmount();
1298
+ } finally {
1299
+ rootMap.delete(containerEl);
1300
+ }
1301
+ }
1302
+ const proxy = new import_ElementProxy.ElementProxy(containerEl);
1303
+ proxy.innerHTML = String(vnode ?? "");
1304
+ return null;
1305
+ }
1306
+ if (vnode && vnode.nodeType && (vnode.nodeType === 1 || vnode.nodeType === 3 || vnode.nodeType === 11)) {
1307
+ const existingRoot = rootMap.get(containerEl);
1308
+ if (existingRoot && typeof existingRoot.unmount === "function") {
1309
+ try {
1310
+ existingRoot.unmount();
1311
+ } finally {
1312
+ rootMap.delete(containerEl);
1313
+ }
1314
+ }
1315
+ while (containerEl.firstChild) containerEl.removeChild(containerEl.firstChild);
1316
+ containerEl.appendChild(vnode);
1317
+ return null;
1318
+ }
1319
+ let root = rootMap.get(containerEl);
1320
+ if (!root) {
1321
+ root = this.ReactDOM.createRoot(containerEl);
1322
+ rootMap.set(containerEl, root);
1323
+ }
1324
+ root.render(vnode);
1325
+ return root;
1326
+ }
1327
+ );
1283
1328
  }
1284
1329
  static define(meta, options) {
1285
1330
  const locale = options == null ? void 0 : options.locale;
package/lib/index.d.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
  export * from './types';
10
10
  export * from './utils';
11
+ export { compileRunJs } from './utils/jsxTransform';
11
12
  export * from './resources';
12
13
  export * from './flowEngine';
13
14
  export * from './hooks';
package/lib/index.js CHANGED
@@ -28,6 +28,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
28
28
  var src_exports = {};
29
29
  __export(src_exports, {
30
30
  RunJSContextRegistry: () => import_registry.RunJSContextRegistry,
31
+ compileRunJs: () => import_jsxTransform.compileRunJs,
31
32
  createBlockScopedEngine: () => import_BlockScopedFlowEngine.createBlockScopedEngine,
32
33
  createJSRunnerWithVersion: () => import_helpers.createJSRunnerWithVersion,
33
34
  createViewScopedEngine: () => import_ViewScopedFlowEngine.createViewScopedEngine,
@@ -43,6 +44,7 @@ __export(src_exports, {
43
44
  module.exports = __toCommonJS(src_exports);
44
45
  __reExport(src_exports, require("./types"), module.exports);
45
46
  __reExport(src_exports, require("./utils"), module.exports);
47
+ var import_jsxTransform = require("./utils/jsxTransform");
46
48
  __reExport(src_exports, require("./resources"), module.exports);
47
49
  __reExport(src_exports, require("./flowEngine"), module.exports);
48
50
  __reExport(src_exports, require("./hooks"), module.exports);
@@ -68,6 +70,7 @@ var import_BlockScopedFlowEngine = require("./BlockScopedFlowEngine");
68
70
  // Annotate the CommonJS export names for ESM import in node:
69
71
  0 && (module.exports = {
70
72
  RunJSContextRegistry,
73
+ compileRunJs,
71
74
  createBlockScopedEngine,
72
75
  createJSRunnerWithVersion,
73
76
  createViewScopedEngine,
@@ -1054,7 +1054,8 @@ const _FlowModel = class _FlowModel {
1054
1054
  uid: this.uid,
1055
1055
  ...import_lodash.default.omit(this._options, ["flowEngine"]),
1056
1056
  stepParams: this.stepParams,
1057
- sortIndex: this.sortIndex
1057
+ sortIndex: this.sortIndex,
1058
+ flowRegistry: {}
1058
1059
  };
1059
1060
  const subModels = this.subModels;
1060
1061
  for (const subModelKey in subModels) {
@@ -1069,8 +1070,7 @@ const _FlowModel = class _FlowModel {
1069
1070
  }
1070
1071
  }
1071
1072
  for (const [key, flow] of this.flowRegistry.getFlows()) {
1072
- data["flowRegistry"] = data["flowRegistry"] || {};
1073
- data["flowRegistry"][key] = flow.toData();
1073
+ data.flowRegistry[key] = flow.toData();
1074
1074
  }
1075
1075
  return data;
1076
1076
  }
@@ -69,6 +69,13 @@ function defineBaseContextMeta() {
69
69
  },
70
70
  methods: {
71
71
  t: 'Internationalization function for translating text. Parameters: (key: string, variables?: object) => string. Example: `ctx.t("Hello {name}", { name: "World" })`',
72
+ render: {
73
+ description: 'Render into container. Accepts ReactElement, DOM Node/Fragment, or HTML string. Parameters: (vnode: ReactElement | Node | DocumentFragment | string, container?: HTMLElement|ElementProxy) => Root|null. Example: `ctx.render(<div>Hello</div>)` or `ctx.render("<b>hi</b>")`',
74
+ detail: "ReactDOM Root",
75
+ completion: {
76
+ insertText: `ctx.render(<div />)`
77
+ }
78
+ },
72
79
  requireAsync: 'Asynchronously load external libraries from URL. Parameters: (url: string) => Promise<any>. Example: `const lodash = await ctx.requireAsync("https://cdn.jsdelivr.net/npm/lodash")`',
73
80
  importAsync: 'Dynamically import ESM module by URL. Parameters: (url: string) => Promise<Module>. Example: `const mod = await ctx.importAsync("https://cdn.jsdelivr.net/npm/lit-html@2/+esm")`',
74
81
  resolveJsonTemplate: "Resolve JSON templates containing variable expressions with {{ }} syntax. Parameters: (template: any, context?: object) => any",
@@ -140,6 +147,13 @@ function defineBaseContextMeta() {
140
147
  },
141
148
  methods: {
142
149
  t: '\u56FD\u9645\u5316\u51FD\u6570\uFF0C\u7528\u4E8E\u7FFB\u8BD1\u6587\u6848\u3002\u53C2\u6570\uFF1A(key: string, variables?: object) => string\u3002\u793A\u4F8B\uFF1A`ctx.t("\u4F60\u597D {name}", { name: "\u4E16\u754C" })`',
150
+ render: {
151
+ description: '\u6E32\u67D3\u5230\u5BB9\u5668\u3002vnode \u652F\u6301 ReactElement\u3001DOM \u8282\u70B9/\u7247\u6BB5\u3001\u6216 HTML \u5B57\u7B26\u4E32\u3002\u53C2\u6570\uFF1A(vnode: ReactElement | Node | DocumentFragment | string, container?: HTMLElement|ElementProxy) => Root|null\u3002\u793A\u4F8B\uFF1A`ctx.render(<div />)` \u6216 `ctx.render("<b>hi</b>")`',
152
+ detail: "ReactDOM Root",
153
+ completion: {
154
+ insertText: `ctx.render(<div />)`
155
+ }
156
+ },
143
157
  requireAsync: '\u6309 URL \u5F02\u6B65\u52A0\u8F7D\u5916\u90E8\u5E93\u3002\u53C2\u6570\uFF1A(url: string) => Promise<any>\u3002\u793A\u4F8B\uFF1A`const lodash = await ctx.requireAsync("https://cdn.jsdelivr.net/npm/lodash")`',
144
158
  importAsync: '\u6309 URL \u52A8\u6001\u5BFC\u5165 ESM \u6A21\u5757\uFF08\u5F00\u53D1/\u751F\u4EA7\u5747\u53EF\u7528\uFF09\u3002\u53C2\u6570\uFF1A(url: string) => Promise<Module>\u3002\u793A\u4F8B\uFF1A`const mod = await ctx.importAsync("https://cdn.jsdelivr.net/npm/lit-html@2/+esm")`',
145
159
  resolveJsonTemplate: "\u89E3\u6790\u542B {{ }} \u53D8\u91CF\u8868\u8FBE\u5F0F\u7684 JSON \u6A21\u677F\u3002\u53C2\u6570\uFF1A(template: any, context?: object) => any",
@@ -60,6 +60,7 @@ const snippets = {
60
60
  "scene/block/echarts-init": /* @__PURE__ */ __name(() => import("./scene/block/echarts-init.snippet"), "scene/block/echarts-init"),
61
61
  // scene/block
62
62
  "scene/block/render-react": /* @__PURE__ */ __name(() => import("./scene/block/render-react.snippet"), "scene/block/render-react"),
63
+ "scene/block/render-react-jsx": /* @__PURE__ */ __name(() => import("./scene/block/render-react-jsx.snippet"), "scene/block/render-react-jsx"),
63
64
  "scene/block/render-button-handler": /* @__PURE__ */ __name(() => import("./scene/block/render-button-handler.snippet"), "scene/block/render-button-handler"),
64
65
  "scene/block/add-event-listener": /* @__PURE__ */ __name(() => import("./scene/block/add-event-listener.snippet"), "scene/block/add-event-listener"),
65
66
  "scene/block/chartjs-bar": /* @__PURE__ */ __name(() => import("./scene/block/chartjs-bar.snippet"), "scene/block/chartjs-bar"),
@@ -42,11 +42,13 @@ const snippet = {
42
42
  }
43
43
  },
44
44
  content: `
45
- const { React, ReactDOM, antd } = ctx;
46
- const { Button } = antd;
45
+ const { Button } = ctx.antd;
47
46
 
48
- const node = React.createElement(Button, { type: 'primary', onClick: () => ctx.message.success(ctx.t('Clicked!')) }, ctx.t('Button'));
49
- ReactDOM.createRoot(ctx.element).render(node);
47
+ ctx.render(
48
+ <Button type="primary" onClick={() => ctx.message.success(ctx.t('Clicked!'))}>
49
+ {ctx.t('Button')}
50
+ </Button>
51
+ );
50
52
  `
51
53
  };
52
54
  var render_button_handler_snippet_default = snippet;
@@ -42,29 +42,28 @@ const snippet = {
42
42
  }
43
43
  },
44
44
  content: `
45
- const { Card, Descriptions, Tag, Typography } = ctx.antd;
46
- const { createElement: h } = ctx.React;
45
+ const { Card, Descriptions, Tag } = ctx.antd;
47
46
 
48
47
  if (!ctx.record) {
49
- ctx.element.innerHTML = '<div style="padding:16px;color:#999;">' + ctx.t('No record data') + '</div>';
48
+ ctx.render('<div style="padding:16px;color:#999;">' + ctx.t('No record data') + '</div>');
50
49
  return;
51
50
  }
52
51
 
53
52
  const record = ctx.record;
54
53
 
55
- ctx.ReactDOM.createRoot(ctx.element).render(
56
- h(Card, { title: ctx.t('Record Details'), bordered: true, style: { margin: 0 } },
57
- h(Descriptions, { column: 2, size: 'small' },
58
- h(Descriptions.Item, { label: ctx.t('ID') }, record.id || '-'),
59
- h(Descriptions.Item, { label: ctx.t('Status') },
60
- h(Tag, { color: record.status === 'active' ? 'green' : 'default' }, record.status || '-')
61
- ),
62
- h(Descriptions.Item, { label: ctx.t('Title') }, record.title || '-'),
63
- h(Descriptions.Item, { label: ctx.t('Created At') },
64
- record.createdAt ? new Date(record.createdAt).toLocaleString() : '-'
65
- )
66
- )
67
- )
54
+ ctx.render(
55
+ <Card title={ctx.t('Record Details')} bordered style={{ margin: 0 }}>
56
+ <Descriptions column={2} size="small">
57
+ <Descriptions.Item label={ctx.t('ID')}>{record.id || '-'}</Descriptions.Item>
58
+ <Descriptions.Item label={ctx.t('Status')}>
59
+ <Tag color={record.status === 'active' ? 'green' : 'default'}>{record.status || '-'}</Tag>
60
+ </Descriptions.Item>
61
+ <Descriptions.Item label={ctx.t('Title')}>{record.title || '-'}</Descriptions.Item>
62
+ <Descriptions.Item label={ctx.t('Created At')}>
63
+ {record.createdAt ? new Date(record.createdAt).toLocaleString() : '-'}
64
+ </Descriptions.Item>
65
+ </Descriptions>
66
+ </Card>
68
67
  );
69
68
 
70
69
  `
@@ -0,0 +1,11 @@
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 type { SnippetModule } from '../../types';
10
+ declare const snippet: SnippetModule;
11
+ export default snippet;
@@ -0,0 +1,58 @@
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 __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+ var __copyProps = (to, from, except, desc) => {
19
+ if (from && typeof from === "object" || typeof from === "function") {
20
+ for (let key of __getOwnPropNames(from))
21
+ if (!__hasOwnProp.call(to, key) && key !== except)
22
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
23
+ }
24
+ return to;
25
+ };
26
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
+ var render_react_jsx_snippet_exports = {};
28
+ __export(render_react_jsx_snippet_exports, {
29
+ default: () => render_react_jsx_snippet_default
30
+ });
31
+ module.exports = __toCommonJS(render_react_jsx_snippet_exports);
32
+ var import_JSBlockRunJSContext = require("../../../contexts/JSBlockRunJSContext");
33
+ const snippet = {
34
+ contexts: [import_JSBlockRunJSContext.JSBlockRunJSContext],
35
+ prefix: "sn-react-jsx",
36
+ label: "Render React (JSX)",
37
+ description: "Render a simple React component using JSX syntax",
38
+ locales: {
39
+ "zh-CN": {
40
+ label: "\u6E32\u67D3 React\uFF08JSX\uFF09",
41
+ description: "\u4F7F\u7528 JSX \u8BED\u6CD5\u6E32\u67D3\u4E00\u4E2A\u7B80\u5355\u7684 React \u7EC4\u4EF6"
42
+ }
43
+ },
44
+ content: `
45
+ // Render a React component with JSX
46
+ const { React } = ctx;
47
+
48
+ const App = () => (
49
+ <div style={{ padding: 12 }}>
50
+ <h3 style={{ margin: 0, color: '#1890ff' }}>Hello JSX</h3>
51
+ <div style={{ color: '#555' }}>This block is rendered by JSX.</div>
52
+ </div>
53
+ );
54
+
55
+ ctx.render(<App />);
56
+ `
57
+ };
58
+ var render_react_jsx_snippet_default = snippet;
@@ -43,15 +43,15 @@ const snippet = {
43
43
  },
44
44
  content: `
45
45
  // Render a React element into ctx.element via ReactDOM
46
- const { React, ReactDOM, antd } = ctx;
47
- const { Button } = antd;
46
+ const { Button } = ctx.antd;
48
47
 
49
- const node = React.createElement(
50
- 'div',
51
- { style: { padding: 12 } },
52
- React.createElement(Button, { type: 'primary', onClick: () => ctx.message.success(ctx.t('Clicked!')) }, ctx.t('Click')),
48
+ ctx.render(
49
+ <div style={{ padding: 12 }}>
50
+ <Button type="primary" onClick={() => ctx.message.success(ctx.t('Clicked!'))}>
51
+ {ctx.t('Click')}
52
+ </Button>
53
+ </div>
53
54
  );
54
- ReactDOM.createRoot(ctx.element).render(node);
55
55
  `
56
56
  };
57
57
  var render_react_snippet_default = snippet;
@@ -43,7 +43,6 @@ const snippet = {
43
43
  },
44
44
  content: `
45
45
  const { Card, Statistic, Row, Col } = ctx.antd;
46
- const { createElement: h } = ctx.React;
47
46
 
48
47
  const res = await ctx.api.request({
49
48
  url: 'users:list',
@@ -67,33 +66,29 @@ const distinctRoles = new Set(
67
66
  .filter(Boolean),
68
67
  ).size;
69
68
 
70
- ctx.ReactDOM.createRoot(ctx.element).render(
71
- h(Row, { gutter: 16 },
72
- h(Col, { span: 6 },
73
- h(Card, {},
74
- h(Statistic, { title: ctx.t('Total users'), value: total, valueStyle: { color: '#3f8600' } })
75
- )
76
- ),
77
- h(Col, { span: 6 },
78
- h(Card, {},
79
- h(Statistic, { title: ctx.t('Administrators'), value: adminCount, valueStyle: { color: '#1890ff' } })
80
- )
81
- ),
82
- h(Col, { span: 6 },
83
- h(Card, {},
84
- h(Statistic, { title: ctx.t('Users with email'), value: withEmail, valueStyle: { color: '#faad14' } })
85
- )
86
- ),
87
- h(Col, { span: 6 },
88
- h(Card, {},
89
- h(Statistic, {
90
- title: ctx.t('Distinct roles'),
91
- value: distinctRoles,
92
- valueStyle: { color: '#cf1322' },
93
- })
94
- )
95
- )
96
- )
69
+ ctx.render(
70
+ <Row gutter={16}>
71
+ <Col span={6}>
72
+ <Card>
73
+ <Statistic title={ctx.t('Total users')} value={total} valueStyle={{ color: '#3f8600' }} />
74
+ </Card>
75
+ </Col>
76
+ <Col span={6}>
77
+ <Card>
78
+ <Statistic title={ctx.t('Administrators')} value={adminCount} valueStyle={{ color: '#1890ff' }} />
79
+ </Card>
80
+ </Col>
81
+ <Col span={6}>
82
+ <Card>
83
+ <Statistic title={ctx.t('Users with email')} value={withEmail} valueStyle={{ color: '#faad14' }} />
84
+ </Card>
85
+ </Col>
86
+ <Col span={6}>
87
+ <Card>
88
+ <Statistic title={ctx.t('Distinct roles')} value={distinctRoles} valueStyle={{ color: '#cf1322' }} />
89
+ </Card>
90
+ </Col>
91
+ </Row>
97
92
  );
98
93
  `
99
94
  };
@@ -43,7 +43,6 @@ const snippet = {
43
43
  },
44
44
  content: `
45
45
  const { Timeline, Card } = ctx.antd;
46
- const { createElement: h } = ctx.React;
47
46
 
48
47
  const res = await ctx.api.request({
49
48
  url: 'users:list',
@@ -57,28 +56,28 @@ const res = await ctx.api.request({
57
56
  const records = res?.data?.data || [];
58
57
 
59
58
  if (!records.length) {
60
- ctx.element.innerHTML = '<div style="padding:16px;color:#999;">' + ctx.t('No data') + '</div>';
59
+ ctx.render('<div style="padding:16px;color:#999;">' + ctx.t('No data') + '</div>');
61
60
  return;
62
61
  }
63
62
 
64
- ctx.ReactDOM.createRoot(ctx.element).render(
65
- h(Card, { title: ctx.t('Activity Timeline'), bordered: true },
66
- h(Timeline, { mode: 'left' },
67
- ...records.map(record =>
68
- h(Timeline.Item, {
69
- key: record.id,
70
- label: record.createdAt ? new Date(record.createdAt).toLocaleString() : '',
71
- },
72
- h('div', {},
73
- h('strong', {}, record.nickname || record.username || ctx.t('Unnamed user')),
74
- record.email
75
- ? h('div', { style: { color: '#999', fontSize: '12px', marginTop: '4px' } }, record.email)
76
- : null,
77
- )
78
- )
79
- )
80
- )
81
- )
63
+ ctx.render(
64
+ <Card title={ctx.t('Activity Timeline')} bordered>
65
+ <Timeline mode="left">
66
+ {records.map((record) => (
67
+ <Timeline.Item
68
+ key={record.id}
69
+ label={record.createdAt ? new Date(record.createdAt).toLocaleString() : ''}
70
+ >
71
+ <div>
72
+ <strong>{record.nickname || record.username || ctx.t('Unnamed user')}</strong>
73
+ {record.email ? (
74
+ <div style={{ color: '#999', fontSize: '12px', marginTop: '4px' }}>{record.email}</div>
75
+ ) : null}
76
+ </div>
77
+ </Timeline.Item>
78
+ ))}
79
+ </Timeline>
80
+ </Card>
82
81
  );
83
82
  `
84
83
  };
@@ -0,0 +1,15 @@
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
+ * Lightweight JSX -> JS compiler for RunJS user code.
11
+ * - Uses sucrase via dynamic import (lazy; avoids static cycles and cost when not needed)
12
+ * - Maps JSX to ctx.React.createElement / ctx.React.Fragment so no global React is required
13
+ * - If sucrase is unavailable or transform throws, returns original code as graceful fallback
14
+ */
15
+ export declare function compileRunJs(code: string): Promise<string>;
@@ -0,0 +1,68 @@
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 __create = Object.create;
11
+ var __defProp = Object.defineProperty;
12
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
13
+ var __getOwnPropNames = Object.getOwnPropertyNames;
14
+ var __getProtoOf = Object.getPrototypeOf;
15
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
16
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
17
+ var __export = (target, all) => {
18
+ for (var name in all)
19
+ __defProp(target, name, { get: all[name], enumerable: true });
20
+ };
21
+ var __copyProps = (to, from, except, desc) => {
22
+ if (from && typeof from === "object" || typeof from === "function") {
23
+ for (let key of __getOwnPropNames(from))
24
+ if (!__hasOwnProp.call(to, key) && key !== except)
25
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
26
+ }
27
+ return to;
28
+ };
29
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
30
+ // If the importer is in node compatibility mode or this is not an ESM
31
+ // file that has been converted to a CommonJS file using a Babel-
32
+ // compatible transform (i.e. "__esModule" has not been set), then set
33
+ // "default" to the CommonJS "module.exports" for node compatibility.
34
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
35
+ mod
36
+ ));
37
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
38
+ var jsxTransform_exports = {};
39
+ __export(jsxTransform_exports, {
40
+ compileRunJs: () => compileRunJs
41
+ });
42
+ module.exports = __toCommonJS(jsxTransform_exports);
43
+ async function compileRunJs(code) {
44
+ var _a;
45
+ const maybeJSX = /<[A-Za-z]|<\//.test(code);
46
+ if (!maybeJSX) return code;
47
+ try {
48
+ const mod = await import("sucrase");
49
+ const transform = (mod == null ? void 0 : mod.transform) || ((_a = mod == null ? void 0 : mod.default) == null ? void 0 : _a.transform);
50
+ if (typeof transform !== "function") return code;
51
+ const res = transform(code, {
52
+ transforms: ["jsx"],
53
+ jsxPragma: "ctx.React.createElement",
54
+ jsxFragmentPragma: "ctx.React.Fragment",
55
+ // Keep ES syntax as-is; JSRunner runs in modern engines
56
+ disableESTransforms: true,
57
+ production: true
58
+ });
59
+ return (res && (res.code || (res == null ? void 0 : res.output))) ?? code;
60
+ } catch (_e) {
61
+ return code;
62
+ }
63
+ }
64
+ __name(compileRunJs, "compileRunJs");
65
+ // Annotate the CommonJS export names for ESM import in node:
66
+ 0 && (module.exports = {
67
+ compileRunJs
68
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/flow-engine",
3
- "version": "2.0.0-alpha.25",
3
+ "version": "2.0.0-alpha.26",
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.0.0-alpha.25",
12
- "@nocobase/shared": "2.0.0-alpha.25",
11
+ "@nocobase/sdk": "2.0.0-alpha.26",
12
+ "@nocobase/shared": "2.0.0-alpha.26",
13
13
  "ahooks": "^3.7.2",
14
14
  "dompurify": "^3.0.2",
15
15
  "lodash": "^4.x",
@@ -18,6 +18,7 @@
18
18
  "slate": "^0.103.0",
19
19
  "slate-history": "^0.109.0",
20
20
  "slate-react": "^0.110.3",
21
+ "sucrase": "^3.35.0",
21
22
  "uid": "^2.0.2"
22
23
  },
23
24
  "peerDependencies": {
@@ -34,5 +35,5 @@
34
35
  ],
35
36
  "author": "NocoBase Team",
36
37
  "license": "AGPL-3.0",
37
- "gitHead": "8ce1972206272bd1723db193abd0425c532c5687"
38
+ "gitHead": "3f9e6825dfefabec9640dc46783955174275df04"
38
39
  }
@@ -20,6 +20,7 @@ import pino from 'pino';
20
20
  import qs from 'qs';
21
21
  import React, { createRef } from 'react';
22
22
  import * as ReactDOMClient from 'react-dom/client';
23
+ import { ElementProxy } from './ElementProxy';
23
24
  import type { Location } from 'react-router-dom';
24
25
  import { ACL } from './acl/Acl';
25
26
  import { ContextPathProxy } from './ContextPathProxy';
@@ -1698,6 +1699,68 @@ export class FlowRunJSContext extends FlowContext {
1698
1699
  },
1699
1700
  };
1700
1701
  this.defineProperty('ReactDOM', { value: ReactDOMShim });
1702
+
1703
+ // Convenience: ctx.render(<App />[, container])
1704
+ // - container defaults to ctx.element if available
1705
+ // - internally uses engine.reactView.createRoot to inherit app context
1706
+ // - caches root per container via global WeakMap
1707
+ this.defineMethod(
1708
+ 'render',
1709
+ function (
1710
+ this: any,
1711
+ vnode: React.ReactElement | Node | DocumentFragment | string,
1712
+ container?: Element | DocumentFragment,
1713
+ ) {
1714
+ const el = (container as any) || (this.element as any);
1715
+ if (!el) throw new Error('ctx.render: container not provided and ctx.element is not available');
1716
+ const containerEl: any = (el as any)?.__el || el; // unwrap ElementProxy
1717
+ const globalRef: any = globalThis as any;
1718
+ globalRef.__nbRunjsRoots = globalRef.__nbRunjsRoots || new WeakMap<any, any>();
1719
+ const rootMap: WeakMap<any, any> = globalRef.__nbRunjsRoots;
1720
+
1721
+ // If vnode is string (HTML), unmount react root and set sanitized HTML
1722
+ if (typeof vnode === 'string') {
1723
+ const existingRoot = rootMap.get(containerEl);
1724
+ if (existingRoot && typeof existingRoot.unmount === 'function') {
1725
+ try {
1726
+ existingRoot.unmount();
1727
+ } finally {
1728
+ rootMap.delete(containerEl);
1729
+ }
1730
+ }
1731
+ const proxy: any = new ElementProxy(containerEl);
1732
+ proxy.innerHTML = String(vnode ?? '');
1733
+ return null;
1734
+ }
1735
+
1736
+ // If vnode is a DOM Node or DocumentFragment, unmount and replace content
1737
+ if (
1738
+ vnode &&
1739
+ (vnode as any).nodeType &&
1740
+ ((vnode as any).nodeType === 1 || (vnode as any).nodeType === 3 || (vnode as any).nodeType === 11)
1741
+ ) {
1742
+ const existingRoot = rootMap.get(containerEl);
1743
+ if (existingRoot && typeof existingRoot.unmount === 'function') {
1744
+ try {
1745
+ existingRoot.unmount();
1746
+ } finally {
1747
+ rootMap.delete(containerEl);
1748
+ }
1749
+ }
1750
+ while (containerEl.firstChild) containerEl.removeChild(containerEl.firstChild);
1751
+ containerEl.appendChild(vnode as any);
1752
+ return null;
1753
+ }
1754
+
1755
+ let root = rootMap.get(containerEl);
1756
+ if (!root) {
1757
+ root = this.ReactDOM.createRoot(containerEl);
1758
+ rootMap.set(containerEl, root);
1759
+ }
1760
+ root.render(vnode as any);
1761
+ return root;
1762
+ },
1763
+ );
1701
1764
  }
1702
1765
  static define(meta: RunJSDocMeta, options?: { locale?: string }) {
1703
1766
  const locale = options?.locale;
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ export * from './types';
12
12
 
13
13
  // 工具函数
14
14
  export * from './utils';
15
+ export { compileRunJs } from './utils/jsxTransform';
15
16
 
16
17
  // 资源类
17
18
  export * from './resources';
@@ -1331,6 +1331,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1331
1331
  ..._.omit(this._options, ['flowEngine']),
1332
1332
  stepParams: this.stepParams,
1333
1333
  sortIndex: this.sortIndex,
1334
+ flowRegistry: {},
1334
1335
  };
1335
1336
  const subModels = this.subModels as {
1336
1337
  [key: string]: FlowModel | FlowModel[];
@@ -1347,8 +1348,7 @@ export class FlowModel<Structure extends DefaultStructure = DefaultStructure> {
1347
1348
  }
1348
1349
  }
1349
1350
  for (const [key, flow] of this.flowRegistry.getFlows()) {
1350
- data['flowRegistry'] = data['flowRegistry'] || {};
1351
- data['flowRegistry'][key] = flow.toData();
1351
+ data.flowRegistry[key] = flow.toData();
1352
1352
  }
1353
1353
  return data;
1354
1354
  }
@@ -50,6 +50,14 @@ export function defineBaseContextMeta() {
50
50
  },
51
51
  methods: {
52
52
  t: 'Internationalization function for translating text. Parameters: (key: string, variables?: object) => string. Example: `ctx.t("Hello {name}", { name: "World" })`',
53
+ render: {
54
+ description:
55
+ 'Render into container. Accepts ReactElement, DOM Node/Fragment, or HTML string. Parameters: (vnode: ReactElement | Node | DocumentFragment | string, container?: HTMLElement|ElementProxy) => Root|null. Example: `ctx.render(<div>Hello</div>)` or `ctx.render("<b>hi</b>")`',
56
+ detail: 'ReactDOM Root',
57
+ completion: {
58
+ insertText: `ctx.render(<div />)`,
59
+ },
60
+ },
53
61
  requireAsync:
54
62
  'Asynchronously load external libraries from URL. Parameters: (url: string) => Promise<any>. Example: `const lodash = await ctx.requireAsync("https://cdn.jsdelivr.net/npm/lodash")`',
55
63
  importAsync:
@@ -127,6 +135,14 @@ export function defineBaseContextMeta() {
127
135
  },
128
136
  methods: {
129
137
  t: '国际化函数,用于翻译文案。参数:(key: string, variables?: object) => string。示例:`ctx.t("你好 {name}", { name: "世界" })`',
138
+ render: {
139
+ description:
140
+ '渲染到容器。vnode 支持 ReactElement、DOM 节点/片段、或 HTML 字符串。参数:(vnode: ReactElement | Node | DocumentFragment | string, container?: HTMLElement|ElementProxy) => Root|null。示例:`ctx.render(<div />)` 或 `ctx.render("<b>hi</b>")`',
141
+ detail: 'ReactDOM Root',
142
+ completion: {
143
+ insertText: `ctx.render(<div />)`,
144
+ },
145
+ },
130
146
  requireAsync:
131
147
  '按 URL 异步加载外部库。参数:(url: string) => Promise<any>。示例:`const lodash = await ctx.requireAsync("https://cdn.jsdelivr.net/npm/lodash")`',
132
148
  importAsync:
@@ -27,6 +27,7 @@ const snippets: Record<string, () => Promise<any>> = {
27
27
  'scene/block/echarts-init': () => import('./scene/block/echarts-init.snippet'),
28
28
  // scene/block
29
29
  'scene/block/render-react': () => import('./scene/block/render-react.snippet'),
30
+ 'scene/block/render-react-jsx': () => import('./scene/block/render-react-jsx.snippet'),
30
31
  'scene/block/render-button-handler': () => import('./scene/block/render-button-handler.snippet'),
31
32
  'scene/block/add-event-listener': () => import('./scene/block/add-event-listener.snippet'),
32
33
  'scene/block/chartjs-bar': () => import('./scene/block/chartjs-bar.snippet'),
@@ -22,11 +22,13 @@ const snippet: SnippetModule = {
22
22
  },
23
23
  },
24
24
  content: `
25
- const { React, ReactDOM, antd } = ctx;
26
- const { Button } = antd;
25
+ const { Button } = ctx.antd;
27
26
 
28
- const node = React.createElement(Button, { type: 'primary', onClick: () => ctx.message.success(ctx.t('Clicked!')) }, ctx.t('Button'));
29
- ReactDOM.createRoot(ctx.element).render(node);
27
+ ctx.render(
28
+ <Button type="primary" onClick={() => ctx.message.success(ctx.t('Clicked!'))}>
29
+ {ctx.t('Button')}
30
+ </Button>
31
+ );
30
32
  `,
31
33
  };
32
34
 
@@ -22,29 +22,28 @@ const snippet: SnippetModule = {
22
22
  },
23
23
  },
24
24
  content: `
25
- const { Card, Descriptions, Tag, Typography } = ctx.antd;
26
- const { createElement: h } = ctx.React;
25
+ const { Card, Descriptions, Tag } = ctx.antd;
27
26
 
28
27
  if (!ctx.record) {
29
- ctx.element.innerHTML = '<div style="padding:16px;color:#999;">' + ctx.t('No record data') + '</div>';
28
+ ctx.render('<div style="padding:16px;color:#999;">' + ctx.t('No record data') + '</div>');
30
29
  return;
31
30
  }
32
31
 
33
32
  const record = ctx.record;
34
33
 
35
- ctx.ReactDOM.createRoot(ctx.element).render(
36
- h(Card, { title: ctx.t('Record Details'), bordered: true, style: { margin: 0 } },
37
- h(Descriptions, { column: 2, size: 'small' },
38
- h(Descriptions.Item, { label: ctx.t('ID') }, record.id || '-'),
39
- h(Descriptions.Item, { label: ctx.t('Status') },
40
- h(Tag, { color: record.status === 'active' ? 'green' : 'default' }, record.status || '-')
41
- ),
42
- h(Descriptions.Item, { label: ctx.t('Title') }, record.title || '-'),
43
- h(Descriptions.Item, { label: ctx.t('Created At') },
44
- record.createdAt ? new Date(record.createdAt).toLocaleString() : '-'
45
- )
46
- )
47
- )
34
+ ctx.render(
35
+ <Card title={ctx.t('Record Details')} bordered style={{ margin: 0 }}>
36
+ <Descriptions column={2} size="small">
37
+ <Descriptions.Item label={ctx.t('ID')}>{record.id || '-'}</Descriptions.Item>
38
+ <Descriptions.Item label={ctx.t('Status')}>
39
+ <Tag color={record.status === 'active' ? 'green' : 'default'}>{record.status || '-'}</Tag>
40
+ </Descriptions.Item>
41
+ <Descriptions.Item label={ctx.t('Title')}>{record.title || '-'}</Descriptions.Item>
42
+ <Descriptions.Item label={ctx.t('Created At')}>
43
+ {record.createdAt ? new Date(record.createdAt).toLocaleString() : '-'}
44
+ </Descriptions.Item>
45
+ </Descriptions>
46
+ </Card>
48
47
  );
49
48
 
50
49
  `,
@@ -0,0 +1,39 @@
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 type { SnippetModule } from '../../types';
11
+ import { JSBlockRunJSContext } from '../../../contexts/JSBlockRunJSContext';
12
+
13
+ const snippet: SnippetModule = {
14
+ contexts: [JSBlockRunJSContext],
15
+ prefix: 'sn-react-jsx',
16
+ label: 'Render React (JSX)',
17
+ description: 'Render a simple React component using JSX syntax',
18
+ locales: {
19
+ 'zh-CN': {
20
+ label: '渲染 React(JSX)',
21
+ description: '使用 JSX 语法渲染一个简单的 React 组件',
22
+ },
23
+ },
24
+ content: `
25
+ // Render a React component with JSX
26
+ const { React } = ctx;
27
+
28
+ const App = () => (
29
+ <div style={{ padding: 12 }}>
30
+ <h3 style={{ margin: 0, color: '#1890ff' }}>Hello JSX</h3>
31
+ <div style={{ color: '#555' }}>This block is rendered by JSX.</div>
32
+ </div>
33
+ );
34
+
35
+ ctx.render(<App />);
36
+ `,
37
+ };
38
+
39
+ export default snippet;
@@ -23,15 +23,15 @@ const snippet: SnippetModule = {
23
23
  },
24
24
  content: `
25
25
  // Render a React element into ctx.element via ReactDOM
26
- const { React, ReactDOM, antd } = ctx;
27
- const { Button } = antd;
26
+ const { Button } = ctx.antd;
28
27
 
29
- const node = React.createElement(
30
- 'div',
31
- { style: { padding: 12 } },
32
- React.createElement(Button, { type: 'primary', onClick: () => ctx.message.success(ctx.t('Clicked!')) }, ctx.t('Click')),
28
+ ctx.render(
29
+ <div style={{ padding: 12 }}>
30
+ <Button type="primary" onClick={() => ctx.message.success(ctx.t('Clicked!'))}>
31
+ {ctx.t('Click')}
32
+ </Button>
33
+ </div>
33
34
  );
34
- ReactDOM.createRoot(ctx.element).render(node);
35
35
  `,
36
36
  };
37
37
 
@@ -23,7 +23,6 @@ const snippet: SnippetModule = {
23
23
  },
24
24
  content: `
25
25
  const { Card, Statistic, Row, Col } = ctx.antd;
26
- const { createElement: h } = ctx.React;
27
26
 
28
27
  const res = await ctx.api.request({
29
28
  url: 'users:list',
@@ -47,33 +46,29 @@ const distinctRoles = new Set(
47
46
  .filter(Boolean),
48
47
  ).size;
49
48
 
50
- ctx.ReactDOM.createRoot(ctx.element).render(
51
- h(Row, { gutter: 16 },
52
- h(Col, { span: 6 },
53
- h(Card, {},
54
- h(Statistic, { title: ctx.t('Total users'), value: total, valueStyle: { color: '#3f8600' } })
55
- )
56
- ),
57
- h(Col, { span: 6 },
58
- h(Card, {},
59
- h(Statistic, { title: ctx.t('Administrators'), value: adminCount, valueStyle: { color: '#1890ff' } })
60
- )
61
- ),
62
- h(Col, { span: 6 },
63
- h(Card, {},
64
- h(Statistic, { title: ctx.t('Users with email'), value: withEmail, valueStyle: { color: '#faad14' } })
65
- )
66
- ),
67
- h(Col, { span: 6 },
68
- h(Card, {},
69
- h(Statistic, {
70
- title: ctx.t('Distinct roles'),
71
- value: distinctRoles,
72
- valueStyle: { color: '#cf1322' },
73
- })
74
- )
75
- )
76
- )
49
+ ctx.render(
50
+ <Row gutter={16}>
51
+ <Col span={6}>
52
+ <Card>
53
+ <Statistic title={ctx.t('Total users')} value={total} valueStyle={{ color: '#3f8600' }} />
54
+ </Card>
55
+ </Col>
56
+ <Col span={6}>
57
+ <Card>
58
+ <Statistic title={ctx.t('Administrators')} value={adminCount} valueStyle={{ color: '#1890ff' }} />
59
+ </Card>
60
+ </Col>
61
+ <Col span={6}>
62
+ <Card>
63
+ <Statistic title={ctx.t('Users with email')} value={withEmail} valueStyle={{ color: '#faad14' }} />
64
+ </Card>
65
+ </Col>
66
+ <Col span={6}>
67
+ <Card>
68
+ <Statistic title={ctx.t('Distinct roles')} value={distinctRoles} valueStyle={{ color: '#cf1322' }} />
69
+ </Card>
70
+ </Col>
71
+ </Row>
77
72
  );
78
73
  `,
79
74
  };
@@ -23,7 +23,6 @@ const snippet: SnippetModule = {
23
23
  },
24
24
  content: `
25
25
  const { Timeline, Card } = ctx.antd;
26
- const { createElement: h } = ctx.React;
27
26
 
28
27
  const res = await ctx.api.request({
29
28
  url: 'users:list',
@@ -37,28 +36,28 @@ const res = await ctx.api.request({
37
36
  const records = res?.data?.data || [];
38
37
 
39
38
  if (!records.length) {
40
- ctx.element.innerHTML = '<div style="padding:16px;color:#999;">' + ctx.t('No data') + '</div>';
39
+ ctx.render('<div style="padding:16px;color:#999;">' + ctx.t('No data') + '</div>');
41
40
  return;
42
41
  }
43
42
 
44
- ctx.ReactDOM.createRoot(ctx.element).render(
45
- h(Card, { title: ctx.t('Activity Timeline'), bordered: true },
46
- h(Timeline, { mode: 'left' },
47
- ...records.map(record =>
48
- h(Timeline.Item, {
49
- key: record.id,
50
- label: record.createdAt ? new Date(record.createdAt).toLocaleString() : '',
51
- },
52
- h('div', {},
53
- h('strong', {}, record.nickname || record.username || ctx.t('Unnamed user')),
54
- record.email
55
- ? h('div', { style: { color: '#999', fontSize: '12px', marginTop: '4px' } }, record.email)
56
- : null,
57
- )
58
- )
59
- )
60
- )
61
- )
43
+ ctx.render(
44
+ <Card title={ctx.t('Activity Timeline')} bordered>
45
+ <Timeline mode="left">
46
+ {records.map((record) => (
47
+ <Timeline.Item
48
+ key={record.id}
49
+ label={record.createdAt ? new Date(record.createdAt).toLocaleString() : ''}
50
+ >
51
+ <div>
52
+ <strong>{record.nickname || record.username || ctx.t('Unnamed user')}</strong>
53
+ {record.email ? (
54
+ <div style={{ color: '#999', fontSize: '12px', marginTop: '4px' }}>{record.email}</div>
55
+ ) : null}
56
+ </div>
57
+ </Timeline.Item>
58
+ ))}
59
+ </Timeline>
60
+ </Card>
62
61
  );
63
62
  `,
64
63
  };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { describe, it, expect } from 'vitest';
11
+ import { compileRunJs } from '../../utils/jsxTransform';
12
+
13
+ describe('compileRunJs', () => {
14
+ it('returns original code when no JSX is present', async () => {
15
+ const src = `const a = 1; const b = a + 1;`;
16
+ const out = await compileRunJs(src);
17
+ expect(out).toBe(src);
18
+ });
19
+
20
+ it('transforms JSX when sucrase is available (skip if missing)', async () => {
21
+ const src = `ctx.render(<div className="x">hi</div>);`;
22
+
23
+ // Try to import sucrase to decide if this environment has it installed
24
+ const hasSucrase = await import('sucrase').then(() => true).catch(() => false);
25
+
26
+ const out = await compileRunJs(src);
27
+
28
+ if (!hasSucrase) {
29
+ // Environment without sucrase: current implementation falls back to original code
30
+ expect(out).toBe(src);
31
+ return;
32
+ }
33
+
34
+ // sucrase available: output should contain React.createElement mapping
35
+ expect(out).not.toBe(src);
36
+ expect(out).toMatch(/ctx\.React\.createElement/);
37
+ });
38
+ });
@@ -0,0 +1,39 @@
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
+ * Lightweight JSX -> JS compiler for RunJS user code.
12
+ * - Uses sucrase via dynamic import (lazy; avoids static cycles and cost when not needed)
13
+ * - Maps JSX to ctx.React.createElement / ctx.React.Fragment so no global React is required
14
+ * - If sucrase is unavailable or transform throws, returns original code as graceful fallback
15
+ */
16
+ export async function compileRunJs(code: string): Promise<string> {
17
+ // Fast path: cheap heuristic to skip import if no JSX likely present
18
+ // (not strict; still safe because we fall back to transform if dynamic import is cheap)
19
+ const maybeJSX = /<[A-Za-z]|<\//.test(code);
20
+ if (!maybeJSX) return code;
21
+
22
+ try {
23
+ const mod: any = await import('sucrase');
24
+ const transform = mod?.transform || mod?.default?.transform;
25
+ if (typeof transform !== 'function') return code;
26
+ const res = transform(code, {
27
+ transforms: ['jsx'],
28
+ jsxPragma: 'ctx.React.createElement',
29
+ jsxFragmentPragma: 'ctx.React.Fragment',
30
+ // Keep ES syntax as-is; JSRunner runs in modern engines
31
+ disableESTransforms: true,
32
+ production: true,
33
+ });
34
+ return (res && (res.code || res?.output)) ?? code;
35
+ } catch (_e) {
36
+ // Fallback: return original code if transform fails
37
+ return code;
38
+ }
39
+ }