@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.
- package/lib/flowContext.js +2 -9
- package/lib/flowSettings.js +12 -10
- package/lib/index.d.ts +2 -0
- package/lib/index.js +3 -0
- package/lib/runjs-context/contexts/base.js +10 -4
- package/lib/runjsLibs.d.ts +15 -0
- package/lib/runjsLibs.js +223 -0
- package/lib/utils/runjsTemplateCompat.js +353 -2
- package/lib/views/index.d.ts +1 -1
- package/lib/views/index.js +4 -0
- package/lib/views/usePage.d.ts +4 -0
- package/lib/views/usePage.js +33 -4
- package/package.json +4 -4
- package/src/__tests__/flowSettings.open.test.tsx +69 -15
- package/src/__tests__/runjsLibsLazyLoading.test.ts +44 -0
- package/src/flowContext.ts +3 -12
- package/src/flowSettings.ts +12 -11
- package/src/index.ts +2 -0
- package/src/runjs-context/contexts/base.ts +9 -2
- package/src/runjsLibs.ts +218 -0
- package/src/utils/__tests__/runjsTemplateCompat.test.ts +33 -0
- package/src/utils/runjsTemplateCompat.ts +395 -2
- package/src/views/__tests__/FlowView.usePage.test.tsx +54 -1
- package/src/views/index.tsx +1 -1
- package/src/views/usePage.tsx +38 -4
|
@@ -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
|
+
});
|
package/src/flowContext.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
package/src/flowSettings.ts
CHANGED
|
@@ -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>(
|
|
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
|
|
703
|
-
|
|
704
|
-
|
|
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.
|
|
709
|
-
|
|
710
|
-
|
|
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:
|
|
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
|
},
|
package/src/runjsLibs.ts
ADDED
|
@@ -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 });
|