@nocobase/flow-engine 2.0.0-beta.13 → 2.0.0-beta.14

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.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { describe, it, expect } from 'vitest';
11
+ import { FlowEngine } from '../flowEngine';
12
+ import { registerRunJSLib } from '../runjsLibs';
13
+
14
+ describe('RunJS ctx.libs lazy loading', () => {
15
+ it('preloads member access via prepareRunJsCode injection', async () => {
16
+ registerRunJSLib('testLib', async () => ({ foo: 123 }));
17
+
18
+ const engine = new FlowEngine();
19
+ const ctx: any = engine.context;
20
+ const r = await ctx.runjs(`return ctx.libs.testLib.foo;`);
21
+ expect(r.success).toBe(true);
22
+ expect(r.value).toBe(123);
23
+ });
24
+
25
+ it('preloads bracket access via prepareRunJsCode injection', async () => {
26
+ registerRunJSLib('testLib', async () => ({ foo: 456 }));
27
+
28
+ const engine = new FlowEngine();
29
+ const ctx: any = engine.context;
30
+ const r = await ctx.runjs(`return ctx.libs['testLib'].foo;`);
31
+ expect(r.success).toBe(true);
32
+ expect(r.value).toBe(456);
33
+ });
34
+
35
+ it('preloads object destructuring via prepareRunJsCode injection', async () => {
36
+ registerRunJSLib('testLib', async () => ({ foo: 789 }));
37
+
38
+ const engine = new FlowEngine();
39
+ const ctx: any = engine.context;
40
+ const r = await ctx.runjs(`const { testLib } = ctx.libs; return testLib.foo;`);
41
+ expect(r.success).toBe(true);
42
+ expect(r.value).toBe(789);
43
+ });
44
+ });
@@ -13,7 +13,6 @@ import { APIClient } from '@nocobase/sdk';
13
13
  import type { Router } from '@remix-run/router';
14
14
  import { MessageInstance } from 'antd/es/message/interface';
15
15
  import * as antd from 'antd';
16
- import * as antdIcons from '@ant-design/icons';
17
16
  import type { HookAPI } from 'antd/es/modal/useModal';
18
17
  import { NotificationInstance } from 'antd/es/notification/interface';
19
18
  import _ from 'lodash';
@@ -51,6 +50,7 @@ import { FlowView, FlowViewer } from './views/FlowView';
51
50
  import { RunJSContextRegistry, getModelClassName } from './runjs-context/registry';
52
51
  import { createEphemeralContext } from './utils/createEphemeralContext';
53
52
  import dayjs from 'dayjs';
53
+ import { setupRunJSLibs } from './runjsLibs';
54
54
 
55
55
  // Helper: detect a RecordRef-like object
56
56
  function isRecordRefLike(val: any): boolean {
@@ -1829,6 +1829,7 @@ function __runjsDeepMerge(base: any, patch: any) {
1829
1829
  }
1830
1830
  return out;
1831
1831
  }
1832
+
1832
1833
  export class FlowRunJSContext extends FlowContext {
1833
1834
  constructor(delegate: FlowContext) {
1834
1835
  super();
@@ -1849,17 +1850,7 @@ export class FlowRunJSContext extends FlowContext {
1849
1850
  };
1850
1851
  this.defineProperty('ReactDOM', { value: ReactDOMShim });
1851
1852
 
1852
- // 为第三方/通用库提供统一命名空间:ctx.libs
1853
- // - 新增库应优先挂载到 ctx.libs.xxx
1854
- // - 同时保留顶层别名(如 ctx.React / ctx.antd),以兼容历史代码
1855
- const libs = Object.freeze({
1856
- React,
1857
- ReactDOM: ReactDOMShim,
1858
- antd,
1859
- dayjs,
1860
- antdIcons,
1861
- });
1862
- this.defineProperty('libs', { value: libs });
1853
+ setupRunJSLibs(this);
1863
1854
 
1864
1855
  // Convenience: ctx.render(<App />[, container])
1865
1856
  // - container defaults to ctx.element if available
@@ -32,6 +32,7 @@ import {
32
32
  shouldHideStepInSettings,
33
33
  } from './utils';
34
34
  import { FlowStepContext } from './hooks/useFlowStep';
35
+ import { GLOBAL_EMBED_CONTAINER_ID, EMBED_REPLACING_DATA_KEY } from './views';
35
36
 
36
37
  const Panel = Collapse.Panel;
37
38
 
@@ -682,14 +683,10 @@ export class FlowSettings {
682
683
  typeof resolvedUiMode === 'object' && resolvedUiMode ? resolvedUiMode.props || {} : {};
683
684
 
684
685
  if (modeType === 'embed') {
685
- const target = document.querySelector<HTMLDivElement>('#nocobase-embed-container');
686
+ const target = document.querySelector<HTMLDivElement>(`#${GLOBAL_EMBED_CONTAINER_ID}`);
686
687
  const onOpen = modeProps.onOpen;
687
688
  const onClose = modeProps.onClose;
688
689
 
689
- if (target) {
690
- target.innerHTML = ''; // 清空容器内原有内容
691
- }
692
-
693
690
  modeProps = {
694
691
  target,
695
692
  styles: {
@@ -699,15 +696,19 @@ export class FlowSettings {
699
696
  },
700
697
  ...modeProps,
701
698
  onOpen() {
702
- target.style.width = modeProps.width || '33.3%';
703
- target.style.maxWidth = modeProps.maxWidth || '800px';
704
- target.style.minWidth = modeProps.minWidth || '0px';
699
+ if (target) {
700
+ target.style.width = modeProps.width || '33.3%';
701
+ target.style.maxWidth = modeProps.maxWidth || '800px';
702
+ target.style.minWidth = modeProps.minWidth || '0px';
703
+ }
705
704
  onOpen?.();
706
705
  },
707
706
  onClose() {
708
- target.style.width = 'auto';
709
- target.style.maxWidth = 'none';
710
- target.style.minWidth = 'auto';
707
+ if (target && target.dataset[EMBED_REPLACING_DATA_KEY] !== '1') {
708
+ target.style.width = 'auto';
709
+ target.style.maxWidth = 'none';
710
+ target.style.minWidth = 'auto';
711
+ }
711
712
  onClose?.();
712
713
  },
713
714
  };
package/src/index.ts CHANGED
@@ -13,6 +13,8 @@ export * from './types';
13
13
  // 工具函数
14
14
  export * from './utils';
15
15
  export { compileRunJs } from './utils/jsxTransform';
16
+ export { registerRunJSLib } from './runjsLibs';
17
+ export type { RunJSLibCache, RunJSLibLoader } from './runjsLibs';
16
18
 
17
19
  // 资源类
18
20
  export * from './resources';
@@ -50,7 +50,8 @@ export function defineBaseContextMeta() {
50
50
  'ReactDOM client API including createRoot for rendering React components. Also available via `ctx.libs.ReactDOM`.',
51
51
  antd: 'Ant Design component library. Recommended access path: `ctx.libs.antd`.',
52
52
  libs: {
53
- description: 'Namespace for third-party and shared libraries. Includes React, ReactDOM, Ant Design, and dayjs.',
53
+ description:
54
+ 'Namespace for third-party and shared libraries. Includes React, ReactDOM, Ant Design, dayjs, lodash, math.js, and formula.js.',
54
55
  detail: 'Libraries namespace',
55
56
  properties: {
56
57
  React: 'React namespace (same as ctx.React).',
@@ -58,6 +59,9 @@ export function defineBaseContextMeta() {
58
59
  antd: 'Ant Design component library (same as ctx.antd).',
59
60
  dayjs: 'dayjs date-time utility library.',
60
61
  antdIcons: 'Ant Design icons library. Example: `ctx.libs.antdIcons.PlusOutlined`.',
62
+ lodash: 'Lodash utility library. Example: `ctx.libs.lodash.get(obj, "a.b.c")`.',
63
+ math: 'Math.js library for mathematical operations. Example: `ctx.libs.math.evaluate("2 + 3 * 4")`.',
64
+ formula: 'Formula.js library for spreadsheet-like formulas. Example: `ctx.libs.formula.SUM([1, 2, 3])`.',
61
65
  },
62
66
  },
63
67
  },
@@ -147,7 +151,7 @@ export function defineBaseContextMeta() {
147
151
  antd: 'Ant Design 组件库(RunJS 环境中可用)。推荐使用 `ctx.libs.antd` 访问。',
148
152
  libs: {
149
153
  description:
150
- '第三方/通用库的统一命名空间,包含 React、ReactDOM、Ant Design、dayjs 等。后续新增库会优先挂在此处。',
154
+ '第三方/通用库的统一命名空间,包含 React、ReactDOM、Ant Design、dayjs、lodash、math.js、formula.js 等。后续新增库会优先挂在此处。',
151
155
  detail: '通用库命名空间',
152
156
  properties: {
153
157
  React: 'React 命名空间(等价于 ctx.React)。',
@@ -155,6 +159,9 @@ export function defineBaseContextMeta() {
155
159
  antd: 'Ant Design 组件库(等价于 ctx.antd)。',
156
160
  dayjs: 'dayjs 日期时间工具库。',
157
161
  antdIcons: 'Ant Design 图标库。 例如:`ctx.libs.antdIcons.PlusOutlined`。',
162
+ lodash: 'Lodash 工具库。例如:`ctx.libs.lodash.get(obj, "a.b.c")`。',
163
+ math: 'Math.js 数学运算库。例如:`ctx.libs.math.evaluate("2 + 3 * 4")`。',
164
+ formula: 'Formula.js 电子表格公式库。例如:`ctx.libs.formula.SUM([1, 2, 3])`。',
158
165
  },
159
166
  },
160
167
  },
@@ -0,0 +1,218 @@
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 * as antdIcons from '@ant-design/icons';
11
+ import type { FlowContext } from './flowContext';
12
+
13
+ export type RunJSLibCache = 'global' | 'context';
14
+ export type RunJSLibLoader<T = any> = (ctx: FlowContext) => T | Promise<T>;
15
+
16
+ type RunJSLibRegistryEntry = {
17
+ loader: RunJSLibLoader<unknown>;
18
+ cache: RunJSLibCache;
19
+ };
20
+
21
+ const __runjsLibRegistry = new Map<string, RunJSLibRegistryEntry>();
22
+
23
+ const __runjsLibResolvedCache = new Map<string, unknown>();
24
+ const __runjsLibPendingCache = new Map<string, Promise<unknown>>();
25
+ const __runjsLibPendingByCtx = new WeakMap<FlowContext, Map<string, Promise<unknown>>>();
26
+
27
+ function __runjsGetPendingMapForCtx(ctx: FlowContext): Map<string, Promise<unknown>> {
28
+ let m = __runjsLibPendingByCtx.get(ctx);
29
+ if (!m) {
30
+ m = new Map<string, Promise<unknown>>();
31
+ __runjsLibPendingByCtx.set(ctx, m);
32
+ }
33
+ return m;
34
+ }
35
+
36
+ export function registerRunJSLib(name: string, loader: RunJSLibLoader, options?: { cache?: RunJSLibCache }): void {
37
+ if (typeof name !== 'string' || !name) return;
38
+ if (typeof loader !== 'function') return;
39
+ __runjsLibRegistry.set(name, { loader, cache: options?.cache || 'global' });
40
+ // allow re-register to take effect on next ensure
41
+ __runjsLibResolvedCache.delete(name);
42
+ __runjsLibPendingCache.delete(name);
43
+ }
44
+
45
+ function __runjsIsObject(val: unknown): val is Record<string, unknown> {
46
+ return !!val && typeof val === 'object';
47
+ }
48
+
49
+ function __runjsHasOwn(obj: unknown, key: string): obj is Record<string, unknown> {
50
+ return __runjsIsObject(obj) && Object.prototype.hasOwnProperty.call(obj, key);
51
+ }
52
+
53
+ function __runjsIsPromiseLike(val: unknown): val is PromiseLike<unknown> {
54
+ if (!__runjsIsObject(val)) return false;
55
+ const then = (val as { then?: unknown }).then;
56
+ return typeof then === 'function';
57
+ }
58
+
59
+ function __runjsGetCtxValue(ctx: FlowContext, key: string): unknown {
60
+ return (ctx as unknown as Record<string, unknown>)[key];
61
+ }
62
+
63
+ async function __runjsEnsureLib(ctx: FlowContext, name: string): Promise<unknown> {
64
+ const libs = (ctx as unknown as { libs?: unknown })?.libs;
65
+ if (__runjsHasOwn(libs, name)) {
66
+ const existing = libs[name];
67
+ if (typeof existing !== 'undefined') return existing;
68
+ }
69
+
70
+ const entry = __runjsLibRegistry.get(name);
71
+ if (!entry) return __runjsIsObject(libs) ? libs[name] : undefined;
72
+
73
+ const setLib = (v: unknown) => {
74
+ if (__runjsIsObject(libs)) libs[name] = v;
75
+ };
76
+
77
+ if (entry.cache === 'context') {
78
+ const pendingMap = __runjsGetPendingMapForCtx(ctx);
79
+ const existingPending = pendingMap.get(name);
80
+ if (existingPending) {
81
+ const v = await existingPending;
82
+ setLib(v);
83
+ return v;
84
+ }
85
+
86
+ const task = Promise.resolve()
87
+ .then(() => entry.loader(ctx))
88
+ .then(
89
+ (v) => {
90
+ pendingMap.delete(name);
91
+ return v;
92
+ },
93
+ (err) => {
94
+ pendingMap.delete(name);
95
+ throw err;
96
+ },
97
+ );
98
+ pendingMap.set(name, task);
99
+ const v = await task;
100
+ setLib(v);
101
+ return v;
102
+ }
103
+
104
+ if (__runjsLibResolvedCache.has(name)) {
105
+ const v = __runjsLibResolvedCache.get(name);
106
+ setLib(v);
107
+ return v;
108
+ }
109
+
110
+ const existingPending = __runjsLibPendingCache.get(name);
111
+ if (existingPending) {
112
+ const v = await existingPending;
113
+ setLib(v);
114
+ return v;
115
+ }
116
+
117
+ const task = Promise.resolve()
118
+ .then(() => entry.loader(ctx))
119
+ .then(
120
+ (v) => {
121
+ __runjsLibResolvedCache.set(name, v);
122
+ __runjsLibPendingCache.delete(name);
123
+ return v;
124
+ },
125
+ (err) => {
126
+ __runjsLibPendingCache.delete(name);
127
+ throw err;
128
+ },
129
+ );
130
+ __runjsLibPendingCache.set(name, task);
131
+ const v = await task;
132
+ setLib(v);
133
+ return v;
134
+ }
135
+
136
+ function setupRunJSLibAPIs(ctx: FlowContext): void {
137
+ if (!ctx || typeof ctx !== 'object') return;
138
+ if (typeof ctx.defineMethod !== 'function') return;
139
+
140
+ // Internal: ensure libs are loaded before first use.
141
+ // NOTE: name with `__` prefix to reduce accidental use; still callable from RunJS.
142
+ ctx.defineMethod('__ensureLib', async function (this: FlowContext, name: string) {
143
+ if (typeof name !== 'string' || !name) return undefined;
144
+ return await __runjsEnsureLib(this, name);
145
+ });
146
+ ctx.defineMethod('__ensureLibs', async function (this: FlowContext, names: unknown) {
147
+ if (!Array.isArray(names)) return;
148
+ for (const n of names) {
149
+ if (typeof n !== 'string' || !n) continue;
150
+ await __runjsEnsureLib(this, n);
151
+ }
152
+ });
153
+ }
154
+
155
+ const DEFAULT_RUNJS_LIBS: Array<{ name: string; cache: RunJSLibCache; loader: RunJSLibLoader }> = [
156
+ { name: 'React', cache: 'context', loader: (ctx) => __runjsGetCtxValue(ctx, 'React') },
157
+ { name: 'ReactDOM', cache: 'context', loader: (ctx) => __runjsGetCtxValue(ctx, 'ReactDOM') },
158
+ { name: 'antd', cache: 'context', loader: (ctx) => __runjsGetCtxValue(ctx, 'antd') },
159
+ { name: 'dayjs', cache: 'context', loader: (ctx) => __runjsGetCtxValue(ctx, 'dayjs') },
160
+ { name: 'antdIcons', cache: 'global', loader: () => antdIcons },
161
+ { name: 'lodash', cache: 'global', loader: () => import('lodash').then((m) => m.default || m) },
162
+ { name: 'formula', cache: 'global', loader: () => import('@formulajs/formulajs').then((m) => m.default || m) },
163
+ { name: 'math', cache: 'global', loader: () => import('mathjs').then((m) => m) },
164
+ ];
165
+
166
+ let __defaultRunJSLibsRegistered = false;
167
+
168
+ function ensureDefaultRunJSLibsRegistered(): void {
169
+ if (__defaultRunJSLibsRegistered) return;
170
+ __defaultRunJSLibsRegistered = true;
171
+ for (const { name, loader, cache } of DEFAULT_RUNJS_LIBS) {
172
+ if (__runjsLibRegistry.has(name)) continue;
173
+ registerRunJSLib(name, loader, { cache });
174
+ }
175
+ }
176
+
177
+ function resolveRegisteredLibSync(ctx: FlowContext, name: string): unknown {
178
+ const entry = __runjsLibRegistry.get(name);
179
+ if (!entry) return undefined;
180
+ if (entry.cache === 'global' && __runjsLibResolvedCache.has(name)) {
181
+ return __runjsLibResolvedCache.get(name);
182
+ }
183
+ const v = entry.loader(ctx);
184
+ if (__runjsIsPromiseLike(v)) return undefined;
185
+ if (entry.cache === 'global') __runjsLibResolvedCache.set(name, v);
186
+ return v;
187
+ }
188
+
189
+ export function setupRunJSLibs(ctx: FlowContext): void {
190
+ if (!ctx || typeof ctx !== 'object') return;
191
+ if (typeof ctx.defineProperty !== 'function') return;
192
+
193
+ ensureDefaultRunJSLibsRegistered();
194
+
195
+ // 为第三方/通用库提供统一命名空间:ctx.libs
196
+ // - 新增库应优先挂载到 ctx.libs.xxx(通过 registerRunJSLib)
197
+ // - 同时保留顶层别名(如 ctx.React / ctx.antd),以兼容历史代码
198
+ const libs: Record<string, unknown> = {};
199
+ for (const { name } of DEFAULT_RUNJS_LIBS) {
200
+ Object.defineProperty(libs, name, {
201
+ configurable: true,
202
+ enumerable: true,
203
+ get() {
204
+ const v = resolveRegisteredLibSync(ctx, name);
205
+ // Lazy materialize as writable data property on first access
206
+ Object.defineProperty(libs, name, {
207
+ configurable: true,
208
+ enumerable: true,
209
+ writable: true,
210
+ value: v,
211
+ });
212
+ return v;
213
+ },
214
+ });
215
+ }
216
+ ctx.defineProperty('libs', { value: libs });
217
+ setupRunJSLibAPIs(ctx);
218
+ }
@@ -102,6 +102,39 @@ ctx.render(<div className="x">{name}</div>);
102
102
  expect(out).toMatch(/ctx\.resolveJsonTemplate/);
103
103
  });
104
104
 
105
+ it('injects ctx.libs ensure preamble for member access', async () => {
106
+ const src = `return ctx.libs.lodash;`;
107
+ const out = await prepareRunJsCode(src, { preprocessTemplates: false });
108
+ expect(out).toContain(`/* __runjs_ensure_libs */`);
109
+ expect(out).toContain(`await ctx.__ensureLibs(["lodash"]);`);
110
+ });
111
+
112
+ it('injects ctx.libs ensure preamble for bracket access with string literal', async () => {
113
+ const src = `return ctx.libs['lodash'];`;
114
+ const out = await prepareRunJsCode(src, { preprocessTemplates: false });
115
+ expect(out).toContain(`await ctx.__ensureLibs(["lodash"]);`);
116
+ });
117
+
118
+ it('injects ctx.libs ensure preamble for object destructuring', async () => {
119
+ const src = `const { lodash } = ctx.libs;\nreturn lodash;`;
120
+ const out = await prepareRunJsCode(src, { preprocessTemplates: false });
121
+ expect(out).toContain(`await ctx.__ensureLibs(["lodash"]);`);
122
+ });
123
+
124
+ it('does not inject ctx.libs preamble when ctx.libs only appears in string/comment', async () => {
125
+ const src = `// ctx.libs.lodash\nconst s = "ctx.libs.lodash";\nreturn s;`;
126
+ const out = await prepareRunJsCode(src, { preprocessTemplates: false });
127
+ expect(out).not.toContain(`__runjs_ensure_libs`);
128
+ });
129
+
130
+ it('is idempotent for already-prepared code', async () => {
131
+ const src = `return ctx.libs['lodash'];`;
132
+ const out1 = await prepareRunJsCode(src, { preprocessTemplates: false });
133
+ const out2 = await prepareRunJsCode(out1, { preprocessTemplates: false });
134
+ expect(out2).toBe(out1);
135
+ expect(out2.match(/__runjs_ensure_libs/g)?.length ?? 0).toBe(1);
136
+ });
137
+
105
138
  it('does not break JSX attribute string values when preprocessing templates', async () => {
106
139
  const src = `ctx.render(<Input title="{{ctx.user.name}}" />);`;
107
140
  const out = await prepareRunJsCode(src, { preprocessTemplates: true });