Skip to content

Commit a4cfe6d

Browse files
authored
Merge pull request #4573 from easyops-cn/steve/v2-native-function
feat(): support native mode for storyboard functions
2 parents 6c196a3 + 74f5144 commit a4cfe6d

File tree

7 files changed

+123
-43
lines changed

7 files changed

+123
-43
lines changed

etc/brick-kit.api.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import { StoryboardFunction } from '@next-core/brick-types';
8484
import { StoryConf } from '@next-core/brick-types';
8585
import { Subtract } from 'react-i18next';
8686
import { TemplatePackage } from '@next-core/brick-types';
87+
import { TransformedFunction } from '@next-core/brick-types';
8788
import { UseBrickConf } from '@next-core/brick-types';
8889
import { UseProviderResolveConf } from '@next-core/brick-types';
8990
import { UserInfo } from '@next-core/brick-types';
@@ -587,6 +588,8 @@ export interface RuntimeStoryboardFunction {
587588
// (undocumented)
588589
source: string;
589590
// (undocumented)
591+
transformed?: TransformedFunction;
592+
// (undocumented)
590593
typescript?: boolean;
591594
}
592595

@@ -602,7 +605,7 @@ export function SingleBrickAsComponentFactory(React: typeof _React): React_2.Mem
602605
// Warning: (ae-internal-missing-underscore) The name "StoryboardFunctionPatch" should be prefixed with an underscore because the declaration is marked as @internal
603606
//
604607
// @internal (undocumented)
605-
export type StoryboardFunctionPatch = Pick<StoryboardFunction, "source" | "typescript">;
608+
export type StoryboardFunctionPatch = Pick<StoryboardFunction, "source" | "typescript" | "transformed">;
606609

607610
// Warning: (ae-internal-missing-underscore) The name "StoryboardFunctionRegistry" should be prefixed with an underscore because the declaration is marked as @internal
608611
//

etc/brick-types.api.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2296,6 +2296,7 @@ export interface StoryboardContextItemFreeVariable {
22962296
export interface StoryboardFunction {
22972297
name: string;
22982298
source: string;
2299+
transformed?: TransformedFunction;
22992300
typescript?: boolean;
23002301
}
23012302

@@ -2554,6 +2555,12 @@ export interface TemplatePackage {
25542555
// @internal (undocumented)
25552556
export type TemplateRegistry<T> = Map<string, T>;
25562557

2558+
// @public
2559+
export interface TransformedFunction {
2560+
globals: string[];
2561+
source: string;
2562+
}
2563+
25572564
// @public
25582565
export interface TransformItem {
25592566
// (undocumented)

packages/brick-kit/src/core/StoryboardFunctionRegistryFactory.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,24 @@ describe("StoryboardFunctions", () => {
8282
}
8383
`,
8484
},
85+
{
86+
name: "nativeMode",
87+
source: `
88+
function nativeMode() {
89+
"native mode";
90+
return ["from", "normal", "mode"];
91+
}
92+
`,
93+
transformed: {
94+
globals: ["_", "document"],
95+
source: `
96+
function nativeMode() {
97+
"native mode";
98+
return document ?? _.split("from native mode", " ");
99+
}
100+
`,
101+
},
102+
},
85103
],
86104
{
87105
id: "my-app",
@@ -118,6 +136,7 @@ describe("StoryboardFunctions", () => {
118136
expect(fn.checkPermissions("my:action-b")).toBe(false);
119137
expect(fn.getUniqueId()).not.toBe("42");
120138
expect(fn.getUniqueId("test-")).not.toBe("test-42");
139+
expect(fn.nativeMode()).toEqual(["from", "native", "mode"]);
121140

122141
expect(window.STORYBOARD_FUNCTIONS_PERF?.length).toBe(6);
123142
});

packages/brick-kit/src/core/StoryboardFunctionRegistryFactory.ts

Lines changed: 75 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ import {
22
MicroApp,
33
SimpleFunction,
44
StoryboardFunction,
5+
type TransformedFunction,
56
} from "@next-core/brick-types";
6-
import { cook, precookFunction, EstreeNode } from "@next-core/brick-utils";
7+
import {
8+
cook,
9+
precookFunction,
10+
EstreeNode,
11+
hasOwnProperty,
12+
} from "@next-core/brick-utils";
713
import { supply } from "@next-core/supply";
814
import { getGeneralGlobals } from "../internal/getGeneralGlobals";
915

@@ -15,7 +21,7 @@ export type ReadonlyStoryboardFunctions = Readonly<
1521
/** @internal */
1622
export type StoryboardFunctionPatch = Pick<
1723
StoryboardFunction,
18-
"source" | "typescript"
24+
"source" | "typescript" | "transformed"
1925
>;
2026

2127
/** @internal */
@@ -39,6 +45,7 @@ export interface RuntimeStoryboardFunction {
3945
typescript?: boolean;
4046
processed?: boolean;
4147
cooked?: SimpleFunction;
48+
transformed?: TransformedFunction;
4249
}
4350

4451
/** @internal */
@@ -105,11 +112,29 @@ export function StoryboardFunctionRegistryFactory({
105112
registeredFunctions.set(fn.name, {
106113
source: fn.source,
107114
typescript: fn.typescript,
115+
transformed: fn.transformed,
108116
});
109117
}
110118
}
111119
}
112120

121+
function getGlobalVariables(
122+
globals: Set<string> | string[]
123+
): Record<string, unknown> {
124+
return supply(
125+
globals,
126+
getGeneralGlobals(globals, {
127+
collectCoverage,
128+
widgetId,
129+
widgetVersion,
130+
app: currentApp,
131+
storyboardFunctions,
132+
isStoryboardFunction: true,
133+
}),
134+
!!collectCoverage
135+
);
136+
}
137+
113138
function getStoryboardFunction(name: string): SimpleFunction {
114139
const fn = registeredFunctions.get(name);
115140
if (!fn) {
@@ -122,44 +147,53 @@ export function StoryboardFunctionRegistryFactory({
122147
if (collectCoverage) {
123148
collector = collectCoverage.createCollector(name);
124149
}
125-
const precooked = precookFunction(fn.source, {
126-
cacheKey: fn,
127-
typescript: fn.typescript,
128-
hooks: collector && {
129-
beforeVisit: collector.beforeVisit,
130-
},
131-
});
132-
fn.cooked = cook(precooked.function, fn.source, {
133-
rules: {
134-
noVar: true,
135-
},
136-
globalVariables: supply(
137-
precooked.attemptToVisitGlobals,
138-
getGeneralGlobals(precooked.attemptToVisitGlobals, {
139-
collectCoverage,
140-
widgetId,
141-
widgetVersion,
142-
app: currentApp,
143-
storyboardFunctions,
144-
isStoryboardFunction: true,
145-
}),
146-
!!collectCoverage
147-
),
148-
hooks: {
149-
perfCall: needPerf
150-
? (duration) => {
151-
perf(name, fn.source, duration);
152-
}
153-
: undefined,
154-
...(collector
155-
? {
156-
beforeEvaluate: collector.beforeEvaluate,
157-
beforeCall: collector.beforeCall,
158-
beforeBranch: collector.beforeBranch,
159-
}
160-
: null),
161-
},
162-
}) as SimpleFunction;
150+
151+
// Do not use transformed functions when collecting coverage.
152+
const transformed = !collector && fn.transformed;
153+
if (transformed) {
154+
const globalVariables = getGlobalVariables(transformed.globals);
155+
// Spread globals as params to prevent accessing forbidden globals.
156+
// NOTE: in native mode, forbidden globals are declared as `undefined`,
157+
// thus accessing them will not throw a ReferenceError.
158+
fn.cooked = new Function(
159+
...transformed.globals,
160+
`"use strict";return (${transformed.source})`
161+
)(
162+
...transformed.globals.map((key) =>
163+
hasOwnProperty(globalVariables, key)
164+
? globalVariables[key]
165+
: undefined
166+
)
167+
);
168+
} else {
169+
const precooked = precookFunction(fn.source, {
170+
cacheKey: fn,
171+
typescript: fn.typescript,
172+
hooks: collector && {
173+
beforeVisit: collector.beforeVisit,
174+
},
175+
});
176+
fn.cooked = cook(precooked.function, fn.source, {
177+
rules: {
178+
noVar: true,
179+
},
180+
globalVariables: getGlobalVariables(precooked.attemptToVisitGlobals),
181+
hooks: {
182+
perfCall: needPerf
183+
? (duration) => {
184+
perf(name, fn.source, duration);
185+
}
186+
: undefined,
187+
...(collector
188+
? {
189+
beforeEvaluate: collector.beforeEvaluate,
190+
beforeCall: collector.beforeCall,
191+
beforeBranch: collector.beforeBranch,
192+
}
193+
: null),
194+
},
195+
}) as SimpleFunction;
196+
}
163197
fn.processed = true;
164198
return fn.cooked;
165199
}
@@ -174,6 +208,7 @@ export function StoryboardFunctionRegistryFactory({
174208
registeredFunctions.set(name, {
175209
source: data.source,
176210
typescript: data.typescript,
211+
transformed: data.transformed,
177212
});
178213
},
179214
};

packages/brick-kit/src/internal/getGeneralGlobals.ts

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

packages/brick-types/src/manifest.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1683,6 +1683,22 @@ export interface StoryboardFunction {
16831683
source: string;
16841684
/** 是否使用 TypeScript。 */
16851685
typescript?: boolean;
1686+
/**
1687+
* [Compiled] 编译后的函数
1688+
*
1689+
* 在函数内先声明 "native mode",打包时会将函数转换为原生 JS 代码,以提高执行性能。
1690+
*/
1691+
transformed?: TransformedFunction;
1692+
}
1693+
1694+
/**
1695+
* 编译后的函数
1696+
*/
1697+
export interface TransformedFunction {
1698+
/** 编译后的 JS 代码 */
1699+
source: string;
1700+
/** 要访问的全局对象列表 */
1701+
globals: string[];
16861702
}
16871703

16881704
/**

packages/supply/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { SimpleFunction } from "@next-core/brick-types";
44
import { pipes } from "@next-core/pipes";
55

66
export function supply(
7-
attemptToVisitGlobals: Set<string>,
7+
attemptToVisitGlobals: Set<string> | string[],
88
providedGlobalVariables?: Record<string, unknown>,
99
mock?: boolean
1010
): Record<string, unknown> {

0 commit comments

Comments
 (0)