Skip to content

Commit 80441d5

Browse files
authored
Merge pull request #4571 from easyops-cn/steve/v3-native-function
feat(): support native mode for storyboard functions
2 parents 3c932b7 + cc397a9 commit 80441d5

File tree

6 files changed

+138
-70
lines changed

6 files changed

+138
-70
lines changed

etc/runtime.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,7 @@ function updateTemplatePreviewSettings(appId: string, templateId: string, settin
576576
//
577577
// dist/types/Dialog.d.ts:10:5 - (ae-forgotten-export) The symbol "show_2" needs to be exported by the entry point index.d.ts
578578
// dist/types/Notification.d.ts:8:5 - (ae-forgotten-export) The symbol "show" needs to be exported by the entry point index.d.ts
579-
// dist/types/StoryboardFunctionRegistry.d.ts:47:5 - (ae-forgotten-export) The symbol "FunctionCoverageSettings" needs to be exported by the entry point index.d.ts
579+
// dist/types/StoryboardFunctionRegistry.d.ts:48:5 - (ae-forgotten-export) The symbol "FunctionCoverageSettings" needs to be exported by the entry point index.d.ts
580580
// dist/types/internal/Runtime.d.ts:34:9 - (ae-forgotten-export) The symbol "AppForCheck" needs to be exported by the entry point index.d.ts
581581

582582
// (No @packageDocumentation comment for this package)

packages/runtime/src/StoryboardFunctionRegistry.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,24 @@ describe("StoryboardFunctions", () => {
106106
}
107107
`,
108108
},
109+
{
110+
name: "nativeMode",
111+
source: `
112+
function nativeMode() {
113+
"native mode";
114+
return ["from", "normal", "mode"];
115+
}
116+
`,
117+
transformed: {
118+
globals: ["_", "document"],
119+
source: `
120+
function nativeMode() {
121+
"native mode";
122+
return document ?? _.split("from native mode", " ");
123+
}
124+
`,
125+
},
126+
},
109127
],
110128
{
111129
id: "my-app",
@@ -137,6 +155,7 @@ describe("StoryboardFunctions", () => {
137155
expect(fn.checkPermissions("my:action-b")).toBe(false);
138156
expect(fn.getUniqueId()).not.toBe("42");
139157
expect(fn.getUniqueId("test-")).not.toBe("test-42");
158+
expect(fn.nativeMode()).toEqual(["from", "native", "mode"]);
140159
});
141160

142161
it("should register no functions", () => {

packages/runtime/src/StoryboardFunctionRegistry.ts

Lines changed: 96 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import type { MicroApp, StoryboardFunction } from "@next-core/types";
1+
import type {
2+
MicroApp,
3+
StoryboardFunction,
4+
TransformedFunction,
5+
} from "@next-core/types";
26
import {
37
cook,
48
precookFunction,
@@ -8,6 +12,7 @@ import {
812
} from "@next-core/cook";
913
import { supply } from "@next-core/supply";
1014
import { collectMemberUsageInFunction } from "@next-core/utils/storyboard";
15+
import { hasOwnProperty } from "@next-core/utils/general";
1116
import type _ from "lodash";
1217
import { getGeneralGlobals } from "./internal/compute/getGeneralGlobals.js";
1318

@@ -17,7 +22,7 @@ export type ReadonlyStoryboardFunctions = Readonly<Record<string, Function>>;
1722
/** @internal */
1823
export type StoryboardFunctionPatch = Pick<
1924
StoryboardFunction,
20-
"source" | "typescript"
25+
"source" | "typescript" | "transformed"
2126
>;
2227

2328
/** @internal */
@@ -50,6 +55,7 @@ export interface RuntimeStoryboardFunction {
5055
cooked?: Function;
5156
deps: Set<string> | string[];
5257
hasPermissionsCheck: boolean;
58+
transformed?: TransformedFunction;
5359
}
5460

5561
/** @internal */
@@ -66,7 +72,7 @@ export interface FunctionCoverageSettings {
6672
}
6773

6874
/** @internal */
69-
export type PartialMicroApp = Pick<MicroApp, "id" | "isBuildPush">;
75+
export type PartialMicroApp = Pick<MicroApp, "id" | "isBuildPush" | "config">;
7076

7177
/** @internal */
7278
export function StoryboardFunctionRegistryFactory({
@@ -130,13 +136,29 @@ export function StoryboardFunctionRegistryFactory({
130136
registeredFunctions.set(fn.name, {
131137
source: fn.source,
132138
typescript: fn.typescript,
139+
transformed: fn.transformed,
133140
deps,
134141
hasPermissionsCheck,
135142
});
136143
}
137144
}
138145
}
139146

147+
function getGlobalVariables(globals: Set<string> | string[]) {
148+
return supply(
149+
globals,
150+
getGeneralGlobals(globals, {
151+
collectCoverage,
152+
widgetId,
153+
widgetVersion,
154+
app: currentApp,
155+
storyboardFunctions,
156+
isStoryboardFunction: true,
157+
}),
158+
!!collectCoverage
159+
);
160+
}
161+
140162
function getStoryboardFunction(name: string): Function | undefined {
141163
const fn = registeredFunctions.get(name);
142164
if (!fn) {
@@ -149,67 +171,77 @@ export function StoryboardFunctionRegistryFactory({
149171
if (collectCoverage) {
150172
collector = collectCoverage.createCollector(name);
151173
}
152-
const precooked = precookFunction(fn.source, {
153-
typescript: fn.typescript,
154-
hooks: collector && {
155-
beforeVisit: collector.beforeVisit,
156-
},
157-
cacheKey: fn,
158-
});
159-
const globalVariables = supply(
160-
precooked.attemptToVisitGlobals,
161-
getGeneralGlobals(precooked.attemptToVisitGlobals, {
162-
collectCoverage,
163-
widgetId,
164-
widgetVersion,
165-
app: currentApp,
166-
storyboardFunctions,
167-
isStoryboardFunction: true,
168-
}),
169-
!!collectCoverage
170-
);
171174

172-
fn.cooked = cook(precooked.function, fn.source, {
173-
rules: {
174-
noVar: true,
175-
},
176-
globalVariables: overrides
177-
? {
178-
...globalVariables,
179-
...(overrides?.LodashWithStaticFields &&
180-
precooked.attemptToVisitGlobals.has("_")
181-
? {
182-
_: {
183-
...(globalVariables._ as typeof _),
184-
...overrides.LodashWithStaticFields,
185-
},
186-
}
187-
: null),
188-
...(overrides?.ArrayConstructor &&
189-
precooked.attemptToVisitGlobals.has("Array")
190-
? {
191-
Array: overrides.ArrayConstructor,
192-
}
193-
: null),
194-
...(overrides?.ObjectWithStaticFields &&
195-
precooked.attemptToVisitGlobals.has("Object")
196-
? {
197-
Object: {
198-
...(globalVariables.Object as typeof Object),
199-
...overrides.ObjectWithStaticFields,
200-
},
201-
}
202-
: null),
203-
}
204-
: globalVariables,
205-
ArrayConstructor: overrides?.ArrayConstructor,
206-
hooks: collector && {
207-
beforeEvaluate: collector.beforeEvaluate,
208-
beforeCall: collector.beforeCall,
209-
beforeBranch: collector.beforeBranch,
210-
},
211-
debug: !!debuggerOverrides,
212-
}) as Function;
175+
// Do not use transformed functions when debugging.
176+
const transformed = !overrides && !collector && fn.transformed;
177+
if (transformed) {
178+
const globalVariables = getGlobalVariables(transformed.globals);
179+
// Spread globals as params to prevent accessing forbidden globals.
180+
// NOTE: in native mode, forbidden globals are declared as `undefined`,
181+
// thus accessing them will not throw a ReferenceError.
182+
fn.cooked = new Function(
183+
...transformed.globals,
184+
`"use strict";return (${transformed.source})`
185+
)(
186+
...transformed.globals.map((key) =>
187+
hasOwnProperty(globalVariables, key)
188+
? globalVariables[key]
189+
: undefined
190+
)
191+
);
192+
} else {
193+
const precooked = precookFunction(fn.source, {
194+
typescript: fn.typescript,
195+
hooks: collector && {
196+
beforeVisit: collector.beforeVisit,
197+
},
198+
cacheKey: fn,
199+
});
200+
const globalVariables = getGlobalVariables(
201+
precooked.attemptToVisitGlobals
202+
);
203+
fn.cooked = cook(precooked.function, fn.source, {
204+
rules: {
205+
noVar: true,
206+
},
207+
globalVariables: overrides
208+
? {
209+
...globalVariables,
210+
...(overrides?.LodashWithStaticFields &&
211+
precooked.attemptToVisitGlobals.has("_")
212+
? {
213+
_: {
214+
...(globalVariables._ as typeof _),
215+
...overrides.LodashWithStaticFields,
216+
},
217+
}
218+
: null),
219+
...(overrides?.ArrayConstructor &&
220+
precooked.attemptToVisitGlobals.has("Array")
221+
? {
222+
Array: overrides.ArrayConstructor,
223+
}
224+
: null),
225+
...(overrides?.ObjectWithStaticFields &&
226+
precooked.attemptToVisitGlobals.has("Object")
227+
? {
228+
Object: {
229+
...(globalVariables.Object as typeof Object),
230+
...overrides.ObjectWithStaticFields,
231+
},
232+
}
233+
: null),
234+
}
235+
: globalVariables,
236+
ArrayConstructor: overrides?.ArrayConstructor,
237+
hooks: collector && {
238+
beforeEvaluate: collector.beforeEvaluate,
239+
beforeCall: collector.beforeCall,
240+
beforeBranch: collector.beforeBranch,
241+
},
242+
debug: !!debuggerOverrides,
243+
}) as Function;
244+
}
213245
fn.processed = true;
214246
return fn.cooked;
215247
}
@@ -234,6 +266,7 @@ export function StoryboardFunctionRegistryFactory({
234266
registeredFunctions.set(name, {
235267
source: data.source,
236268
typescript: data.typescript,
269+
transformed: data.transformed,
237270
deps,
238271
hasPermissionsCheck,
239272
});

packages/runtime/src/internal/compute/getGeneralGlobals.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export type PartialMicroApp = Pick<MicroApp, "id" | "isBuildPush">;
2323
// `GeneralGlobals` are globals which are page-state-agnostic,
2424
// thus they can be used both in storyboard expressions and functions.
2525
export function getGeneralGlobals(
26-
attemptToVisitGlobals: Set<string>,
26+
attemptToVisitGlobals: Set<string> | string[],
2727
options: GeneralGlobalsOptions
2828
): Record<string, unknown> {
2929
const globalVariables: Record<string, unknown> = {};

packages/supply/src/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@ import moment from "moment";
33
import { pipes } from "@next-core/pipes";
44

55
export function supply(
6-
attemptToVisitGlobals: Set<string>,
6+
attemptToVisitGlobals: Set<string> | string[],
77
providedGlobalVariables?: Record<string, unknown>,
88
mock?: boolean
99
): Record<string, unknown> {
10-
const globalVariables: Record<string, unknown> = {
11-
undefined,
12-
};
10+
const globalVariables: Record<string, unknown> = {};
1311
// Allow limited browser builtin values.
1412
for (const variableName of attemptToVisitGlobals) {
1513
if (!Object.prototype.hasOwnProperty.call(globalVariables, variableName)) {
16-
if (
14+
if (variableName === "undefined") {
15+
globalVariables[variableName] = undefined;
16+
} else if (
1717
providedGlobalVariables &&
1818
Object.prototype.hasOwnProperty.call(
1919
providedGlobalVariables,

packages/types/src/manifest.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1357,6 +1357,22 @@ export interface StoryboardFunction {
13571357
deps?: string[];
13581358
/** [Compiled] 是否有使用 `PERMISSIONS.check` */
13591359
perm?: boolean;
1360+
/**
1361+
* [Compiled] 编译后的函数
1362+
*
1363+
* 在函数内先声明 "native mode",打包时会将函数转换为原生 JS 代码,以提高执行性能。
1364+
*/
1365+
transformed?: TransformedFunction;
1366+
}
1367+
1368+
/**
1369+
* 编译后的函数
1370+
*/
1371+
export interface TransformedFunction {
1372+
/** 编译后的 JS 代码 */
1373+
source: string;
1374+
/** 要访问的全局对象列表 */
1375+
globals: string[];
13601376
}
13611377

13621378
/**

0 commit comments

Comments
 (0)