diff --git a/.size-limit.js b/.size-limit.js index 7898a251b1..2c250c767c 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -53,7 +53,7 @@ module.exports = [ }, { path: "packages/brick-utils/dist/index.esm.js", - limit: "136 KB", + limit: "138 KB", }, { path: "packages/editor-bricks-helper/dist/index.esm.js", diff --git a/dll/editor-bricks-helper/manifest.snapshot.json b/dll/editor-bricks-helper/manifest.snapshot.json index ebc9ff5f46..f759de926b 100644 --- a/dll/editor-bricks-helper/manifest.snapshot.json +++ b/dll/editor-bricks-helper/manifest.snapshot.json @@ -21,6 +21,7 @@ "asyncProcessBrick", "asyncProcessStoryboard", "collectBricksByCustomTemplates", + "computeConstantCondition", "computeRealRoutePath", "convertValueByPrecision", "cook", @@ -54,6 +55,8 @@ "precookFunction", "preevaluate", "prefetchScript", + "removeDeadConditions", + "removeDeadConditionsInTpl", "resolveContextConcurrently", "restoreDynamicTemplates", "scanAppGetMenuInAny", diff --git a/etc/brick-types.api.md b/etc/brick-types.api.md index e5f3f87fab..b66f331aca 100644 --- a/etc/brick-types.api.md +++ b/etc/brick-types.api.md @@ -1844,6 +1844,8 @@ export interface RuntimeMisc { // // @internal (undocumented) export interface RuntimeStoryboard extends Storyboard { + // (undocumented) + $$deadConditionsRemoved?: boolean; // (undocumented) $$depsProcessed?: boolean; // (undocumented) diff --git a/packages/brick-dll/manifest.snapshot.json b/packages/brick-dll/manifest.snapshot.json index 62ceccb292..c406390e4c 100644 --- a/packages/brick-dll/manifest.snapshot.json +++ b/packages/brick-dll/manifest.snapshot.json @@ -3506,6 +3506,7 @@ "asyncProcessBrick", "asyncProcessStoryboard", "collectBricksByCustomTemplates", + "computeConstantCondition", "computeRealRoutePath", "convertValueByPrecision", "cook", @@ -3539,6 +3540,8 @@ "precookFunction", "preevaluate", "prefetchScript", + "removeDeadConditions", + "removeDeadConditionsInTpl", "resolveContextConcurrently", "restoreDynamicTemplates", "scanAppGetMenuInAny", diff --git a/packages/brick-kit/src/checkIf.ts b/packages/brick-kit/src/checkIf.ts index 88c6e50286..5cfd73b22c 100644 --- a/packages/brick-kit/src/checkIf.ts +++ b/packages/brick-kit/src/checkIf.ts @@ -17,7 +17,7 @@ export interface IfContainer { * * ```yaml * - brick: your.any-brick - * if: '<% FLAGS['your-feature-flag'] %>' + * if: '<% FLAGS["your-feature-flag"] %>' * ``` */ if?: unknown; diff --git a/packages/brick-kit/src/core/CustomTemplates/registerCustomTemplate.ts b/packages/brick-kit/src/core/CustomTemplates/registerCustomTemplate.ts index b1efbb5713..09d9e03c5c 100644 --- a/packages/brick-kit/src/core/CustomTemplates/registerCustomTemplate.ts +++ b/packages/brick-kit/src/core/CustomTemplates/registerCustomTemplate.ts @@ -1,10 +1,13 @@ import { CustomTemplateConstructor } from "@next-core/brick-types"; +import { removeDeadConditionsInTpl } from "@next-core/brick-utils"; +import { getRuntime } from "../../runtime"; import { appRegistered, customTemplateRegistry } from "./constants"; export function registerCustomTemplate( tplName: string, tplConstructor: CustomTemplateConstructor, - appId?: string + appId?: string, + deadCOnditionsRemoved?: boolean ): void { let tagName = tplName; // When a template is registered by an app, its namespace maybe missed. @@ -29,6 +32,12 @@ export function registerCustomTemplate( ); } } + if (!deadCOnditionsRemoved && process.env.NODE_ENV !== "test") { + removeDeadConditionsInTpl(tplConstructor, { + constantFeatureFlags: true, + featureFlags: getRuntime().getFeatureFlags(), + }); + } // Now we allow re-register custom template customTemplateRegistry.set(tagName, { ...tplConstructor, diff --git a/packages/brick-kit/src/core/Kernel.spec.ts b/packages/brick-kit/src/core/Kernel.spec.ts index 85a93cba43..31ebc555a1 100644 --- a/packages/brick-kit/src/core/Kernel.spec.ts +++ b/packages/brick-kit/src/core/Kernel.spec.ts @@ -277,7 +277,8 @@ describe("Kernel", () => { proxy: {}, bricks: [], }, - "app-a" + "app-a", + true ); spyOnLoadScript.mockClear(); diff --git a/packages/brick-kit/src/core/Kernel.ts b/packages/brick-kit/src/core/Kernel.ts index 081b69cda1..ab513c4df1 100644 --- a/packages/brick-kit/src/core/Kernel.ts +++ b/packages/brick-kit/src/core/Kernel.ts @@ -641,7 +641,8 @@ export class Kernel { proxy: tpl.proxy, state: tpl.state, }, - storyboard.app?.id + storyboard.app?.id, + true ); } } diff --git a/packages/brick-kit/src/core/Router.spec.ts b/packages/brick-kit/src/core/Router.spec.ts index ad0f1abad5..c82967f663 100644 --- a/packages/brick-kit/src/core/Router.spec.ts +++ b/packages/brick-kit/src/core/Router.spec.ts @@ -32,7 +32,10 @@ jest.mock("../auth"); jest.mock("../themeAndMode"); jest.mock("../runtime"); jest.mock("../internal/checkPermissions"); -jest.mock("../internal/getStandaloneInstalledApps"); +jest.mock("../internal/getStandaloneInstalledApps", () => ({ + getStandaloneInstalledApps: jest.fn().mockReturnValue([]), + preFetchStandaloneInstalledApps: jest.fn().mockResolvedValue(undefined), +})); jest.mock("@next-core/easyops-analytics", () => ({ apiAnalyzer: { create: () => jest.mock, diff --git a/packages/brick-kit/src/core/Router.ts b/packages/brick-kit/src/core/Router.ts index 76726a2a05..50be47102d 100644 --- a/packages/brick-kit/src/core/Router.ts +++ b/packages/brick-kit/src/core/Router.ts @@ -13,7 +13,9 @@ import { scanStoryboard, mapCustomApisToNameAndNamespace, CustomApiInfo, + removeDeadConditions, } from "@next-core/brick-utils"; +import { HttpResponseError } from "@next-core/brick-http"; import { apiAnalyzer, userAnalytics } from "@next-core/easyops-analytics"; import { LocationContext, @@ -48,7 +50,6 @@ import { preCheckPermissions } from "../internal/checkPermissions"; import { clearPollTimeout } from "../internal/poll"; import { shouldBeDefaultCollapsed } from "../internal/shouldBeDefaultCollapsed"; import { registerStoryboardFunctions } from "./StoryboardFunctions"; -import { HttpResponseError } from "@next-core/brick-http"; import { registerMock } from "./MockRegistry"; import { registerFormRenderer } from "./CustomForms/registerFormRenderer"; import { @@ -252,22 +253,35 @@ export class Router { if (storyboard) { await this.kernel.fulfilStoryboard(storyboard); + removeDeadConditions(storyboard, { + constantFeatureFlags: true, + featureFlags: this.featureFlags, + }); + // 将动态解析后的模板还原,以便重新动态解析。 restoreDynamicTemplates(storyboard); + const parallelRequests: Promise[] = []; + // 预加载权限信息 if (isLoggedIn() && !getAuth().isAdmin) { - await preCheckPermissions(storyboard); + parallelRequests.push(preCheckPermissions(storyboard)); } // Standalone App 需要额外读取 Installed App 信息 if (window.STANDALONE_MICRO_APPS && !window.NO_AUTH_GUARD) { // TODO: get standalone apps when NO_AUTH_GUARD, maybe from conf.yaml - await preFetchStandaloneInstalledApps(storyboard); - this.kernel.bootstrapData.offSiteStandaloneApps = - getStandaloneInstalledApps(); + parallelRequests.push( + preFetchStandaloneInstalledApps(storyboard).then(() => { + this.kernel.bootstrapData.offSiteStandaloneApps = + getStandaloneInstalledApps(); + }) + ); } + // `loadDepsOfStoryboard()` may requires these data. + await Promise.all(parallelRequests); + // 如果找到匹配的 storyboard,那么根据路由匹配得到的 sub-storyboard 加载它的依赖库。 const subStoryboard = this.locationContext.getSubStoryboardByRoute(storyboard); diff --git a/packages/brick-types/src/manifest.ts b/packages/brick-types/src/manifest.ts index db43ff6401..0843046ca2 100644 --- a/packages/brick-types/src/manifest.ts +++ b/packages/brick-types/src/manifest.ts @@ -295,6 +295,7 @@ export interface RuntimeStoryboard extends Storyboard { $$fulfilled?: boolean; $$fulfilling?: Promise; $$i18nFulfilled?: boolean; + $$deadConditionsRemoved?: boolean; } export function isRouteConfOfBricks( diff --git a/packages/brick-utils/src/index.ts b/packages/brick-utils/src/index.ts index 64d31f53c3..c1d9a06c51 100644 --- a/packages/brick-utils/src/index.ts +++ b/packages/brick-utils/src/index.ts @@ -31,3 +31,4 @@ export * from "./visitStoryboard"; export * from "./debounceByAnimationFrame"; export * from "./scanInstalledAppsInStoryboard"; export * from "./makeThrottledAggregation"; +export * from "./removeDeadConditions"; diff --git a/packages/brick-utils/src/removeDeadConditions.spec.ts b/packages/brick-utils/src/removeDeadConditions.spec.ts new file mode 100644 index 0000000000..e3f2124692 --- /dev/null +++ b/packages/brick-utils/src/removeDeadConditions.spec.ts @@ -0,0 +1,425 @@ +import type { + BrickConf, + CustomTemplate, + CustomTemplateConstructor, + RuntimeStoryboard, +} from "@next-core/brick-types"; +import { + removeDeadConditions, + removeDeadConditionsInTpl, +} from "./removeDeadConditions"; + +type MaybeArray = T | T[]; + +describe("removeDeadConditions", () => { + it.each<[MaybeArray>, MaybeArray>]>([ + [{ brick: "a" }, { brick: "a" }], + [{ brick: "a", if: false }, null], + [ + { brick: "a", if: '<% FLAGS["enabled"] %>' }, + { brick: "a", if: true }, + ], + [{ brick: "a", if: '<% !FLAGS["enabled"] %>' }, null], + [ + // Logical expressions + [ + { brick: "a", if: '<% FLAGS["disabled"] || CTX.any %>' }, + { brick: "b", if: "<% FLAGS.disabled && CTX.any %>" }, + { brick: "c", if: '<% CTX.any && !FLAGS["enabled"] %>' }, + { brick: "d", if: "<% !(FLAGS.enabled || CTX.any) %>" }, + { brick: "e", if: "<% !(CTX.any || !null) %>" }, + { brick: "f", if: "<% 0 && CTX.any %>" }, + { brick: "g", if: "<% !(CTX.any || !undefined) %>" }, + { brick: "h", if: "<% undefined && CTX.any %>" }, + { brick: "i", if: "<% HASH && CTX.any %>" }, + { brick: "j", if: "<% !HASH && CTX.any %>" }, + { brick: "k", if: "<% FLAGS[disabled] && CTX.any %>" }, + ], + [ + { brick: "a", if: '<% FLAGS["disabled"] || CTX.any %>' }, + { brick: "i", if: "<% HASH && CTX.any %>" }, + { brick: "j", if: "<% !HASH && CTX.any %>" }, + { brick: "k", if: "<% FLAGS[disabled] && CTX.any %>" }, + ], + ], + [ + // Multiple items. + [ + { brick: "a", if: false }, + { brick: "b", if: '<% FLAGS["enabled"] %>' }, + ], + [{ brick: "b", if: true }], + ], + [ + // Slots + { + brick: "a", + slots: { + b: { + type: "bricks", + bricks: [ + { + brick: "c", + if: '<% FLAGS["disabled"] %>', + }, + ], + }, + d: { + type: "routes", + routes: [ + { + path: "/d", + if: '<% FLAGS["disabled"] %>', + bricks: [], + }, + ], + }, + }, + }, + { + brick: "a", + slots: { + b: { + type: "bricks", + bricks: [], + }, + d: { + type: "routes", + routes: [], + }, + }, + }, + ], + [ + // Events + { + brick: "a", + events: { + click: { + useProvider: "c", + if: '<% FLAGS["disabled"] %>', + }, + dblclick: [ + { + useProvider: "d", + if: '<% FLAGS["enabled"] %>', + callback: { + success: { + action: "console.log", + if: '<% FLAGS["disabled"] %>', + }, + }, + }, + { + useProvider: "e", + if: '<% FLAGS["disabled"] %>', + }, + ], + contextmenu: { + useProvider: "f", + callback: { + success: { + action: "console.log", + if: '<% FLAGS["enabled"] %>', + }, + }, + }, + oops: null, + }, + }, + { + brick: "a", + events: { + dblclick: [ + { + useProvider: "d", + if: true, + callback: {}, + }, + ], + contextmenu: { + useProvider: "f", + callback: { + success: { + action: "console.log", + if: true, + }, + }, + }, + oops: null, + }, + }, + ], + [ + // LifeCycle + { + brick: "a", + lifeCycle: { + useResolves: [ + { + useProvider: "b", + if: '<% FLAGS["enabled"] %>', + }, + { + useProvider: "c", + if: '<% FLAGS["disabled"] %>', + }, + ], + onPageLoad: [ + { + action: "console.log", + if: '<% FLAGS["disabled"] %>', + }, + { + action: "console.warn", + if: '<% FLAGS["enabled"] %>', + }, + ], + onPageLeave: { + action: "console.log", + if: '<% FLAGS["disabled"] %>', + }, + onMessage: { + channel: "any", + handlers: [ + { + action: "console.log", + if: '<% FLAGS["disabled"] %>', + }, + { + action: "console.warn", + if: '<% FLAGS["enabled"] %>', + }, + ], + }, + }, + }, + { + brick: "a", + lifeCycle: { + useResolves: [ + { + useProvider: "b", + if: true, + }, + ], + onPageLoad: [ + { + action: "console.warn", + if: true, + }, + ], + onMessage: { + channel: "any", + handlers: [ + { + action: "console.warn", + if: true, + }, + ], + }, + }, + }, + ], + [ + // UseBrick + { + brick: "a", + properties: { + useBrick: [ + { + brick: "b", + properties: { + b1: [ + { + useBrick: { + brick: "c", + if: true, + }, + }, + ], + b2: { + useBrick: { + brick: "d", + if: '<% FLAGS["disabled"] %>', + }, + }, + }, + }, + { + brick: "e", + if: '<% FLAGS["enabled"] %>', + slots: { + e1: { + bricks: [ + { brick: "f" }, + { brick: "g", if: '<% FLAGS["disabled"] %>' }, + ], + }, + }, + }, + { + brick: "e", + if: '<% FLAGS["disabled"] %>', + }, + ], + }, + }, + { + brick: "a", + properties: { + useBrick: [ + { + brick: "b", + properties: { + b1: [ + { + useBrick: { + brick: "c", + if: true, + }, + }, + ], + b2: { + useBrick: { + brick: "div", + if: false, + }, + }, + }, + }, + { + brick: "e", + if: true, + slots: { + e1: { + bricks: [{ brick: "f" }], + }, + }, + }, + ], + }, + }, + ], + ])("should work for bricks", (input, output) => { + const storyboard = { + routes: [{ bricks: [].concat(input) }], + } as RuntimeStoryboard; + + removeDeadConditions(storyboard, { + constantFeatureFlags: true, + featureFlags: { + enabled: true, + }, + }); + + expect(storyboard).toEqual({ + $$deadConditionsRemoved: true, + routes: [{ bricks: [].concat(output).filter(Boolean) }], + }); + }); + + it("should work for routes", () => { + const storyboard = { + routes: [ + { + type: "routes", + routes: [ + { + bricks: [{ brick: "a" }], + if: '<% FLAGS["disabled"] %>', + }, + { + bricks: [{ brick: "b" }], + if: '<% FLAGS["enabled"] %>', + }, + ], + }, + { + type: "bricks", + if: '<% FLAGS["disabled"] %>', + bricks: [ + { + brick: "a", + }, + ], + }, + ], + } as RuntimeStoryboard; + + removeDeadConditions(storyboard, { + constantFeatureFlags: true, + featureFlags: { + enabled: true, + }, + }); + + expect(storyboard).toEqual({ + $$deadConditionsRemoved: true, + routes: [ + { + type: "routes", + routes: [ + { + bricks: [{ brick: "b" }], + if: true, + }, + ], + }, + ], + }); + + // Do nothing when do it again. + removeDeadConditions(storyboard); + }); + + it("should work for custom templates", () => { + const tplConstructor = { + name: "tpl-test", + bricks: [ + { brick: "a", if: '<% FLAGS["disabled"] %>' }, + { brick: "b", if: '<% FLAGS["enabled"] %>' }, + ], + proxy: {}, + } as CustomTemplate; + + const storyboard = { + meta: { + customTemplates: [tplConstructor], + }, + } as RuntimeStoryboard; + + removeDeadConditions(storyboard, { + constantFeatureFlags: true, + featureFlags: { + enabled: true, + }, + }); + + expect(tplConstructor).toEqual({ + name: "tpl-test", + bricks: [{ brick: "b", if: true }], + proxy: {}, + }); + }); + + it("should warn for potential dead if", () => { + const tplConstructor = { + bricks: [{ brick: "a", if: null }], + } as CustomTemplateConstructor; + + const consoleWarn = jest.spyOn(console, "warn").mockImplementation(); + removeDeadConditionsInTpl(tplConstructor); + expect(consoleWarn).toBeCalledTimes(1); + expect(consoleWarn).toBeCalledWith( + "[potential dead if]:", + "object", + null, + expect.anything() + ); + + expect(tplConstructor).toEqual({ + bricks: [{ brick: "a", if: null }], + }); + }); +}); diff --git a/packages/brick-utils/src/removeDeadConditions.ts b/packages/brick-utils/src/removeDeadConditions.ts new file mode 100644 index 0000000000..e0751da47a --- /dev/null +++ b/packages/brick-utils/src/removeDeadConditions.ts @@ -0,0 +1,373 @@ +import type { + BrickConf, + BrickEventHandler, + BrickEventsMap, + BrickLifeCycle, + ContextConf, + CustomTemplateConstructor, + FeatureFlags, + MessageConf, + RouteConf, + RouteConfOfBricks, + RuntimeStoryboard, + ScrollIntoViewConf, + UseProviderEventHandler, + UseSingleBrickConf, +} from "@next-core/brick-types"; +import { + cook, + EstreeLiteral, + EstreeNode, + isEvaluable, + preevaluate, +} from "@next-core/cook"; +import { pull } from "lodash"; +import { hasOwnProperty } from "./hasOwnProperty"; +import { isObject } from "./isObject"; + +export interface RemoveDeadConditionsOptions { + constantFeatureFlags?: boolean; + featureFlags?: FeatureFlags; +} + +/** + * Remove dead conditions in storyboard like `if: '<% FLAGS["your-feature-flag"] %>'` when + * `FLAGS["your-feature-flag"]` is falsy. + */ +export function removeDeadConditions( + storyboard: RuntimeStoryboard, + options?: RemoveDeadConditionsOptions +): void { + if (storyboard.$$deadConditionsRemoved) { + return; + } + removeDeadConditionsInRoutes(storyboard.routes, options); + const { customTemplates } = storyboard.meta ?? {}; + if (Array.isArray(customTemplates)) { + for (const tpl of customTemplates) { + removeDeadConditionsInTpl(tpl, options); + } + } + storyboard.$$deadConditionsRemoved = true; +} + +/** + * Like `removeDeadConditions` but applied to a custom template. + */ +export function removeDeadConditionsInTpl( + tplConstructor: CustomTemplateConstructor, + options?: RemoveDeadConditionsOptions +): void { + removeDeadConditionsInBricks(tplConstructor.bricks, options); +} + +function removeDeadConditionsInRoutes( + routes: RouteConf[], + options: RemoveDeadConditionsOptions +): void { + removeDeadConditionsInArray(routes, options, (route) => { + removeDeadConditionsInContext(route.context, options); + if (route.type === "routes") { + removeDeadConditionsInRoutes(route.routes, options); + } else { + removeDeadConditionsInBricks( + (route as RouteConfOfBricks).bricks, + options + ); + } + }); +} + +function removeDeadConditionsInBricks( + bricks: BrickConf[], + options: RemoveDeadConditionsOptions +): void { + removeDeadConditionsInArray(bricks, options, (brick) => { + if (brick.slots) { + for (const slot of Object.values(brick.slots)) { + if (slot.type === "routes") { + removeDeadConditionsInRoutes(slot.routes, options); + } else { + removeDeadConditionsInBricks(slot.bricks, options); + } + } + } + removeDeadConditionsInLifeCycle(brick.lifeCycle, options); + removeDeadConditionsInEvents(brick.events, options); + removeDeadConditionsInContext(brick.context, options); + removeDeadConditionsInProperties(brick.properties, options); + }); +} + +function removeDeadConditionsInProperties( + value: unknown, + options: RemoveDeadConditionsOptions +): void { + if (Array.isArray(value)) { + for (const item of value) { + removeDeadConditionsInProperties(item, options); + } + } else if (isObject(value)) { + if (value.useBrick) { + if (Array.isArray(value.useBrick)) { + // For useBrick as array, just remove dead items. + removeDeadConditionsInArray(value.useBrick, options, (useBrick) => { + removeDeadConditionsInUseBrick( + useBrick as UseSingleBrickConf, + options + ); + }); + } else { + // For useBrick as single one, we have to keep it, + // and we change it to an empty
. + computeConstantCondition(value.useBrick, options); + if (value.useBrick.if === false) { + value.useBrick = { + brick: "div", + if: false, + }; + } else { + removeDeadConditionsInUseBrick( + value.useBrick as UseSingleBrickConf, + options + ); + } + } + } else { + for (const item of Object.values(value)) { + removeDeadConditionsInProperties(item, options); + } + } + } +} + +function removeDeadConditionsInUseBrick( + useBrick: UseSingleBrickConf, + options: RemoveDeadConditionsOptions +): void { + removeDeadConditionsInProperties(useBrick.properties, options); + removeDeadConditionsInEvents(useBrick.events, options); + if (useBrick.slots) { + for (const slot of Object.values(useBrick.slots)) { + removeDeadConditionsInBricks(slot.bricks as BrickConf[], options); + } + } +} + +function removeDeadConditionsInEvents( + events: BrickEventsMap, + options: RemoveDeadConditionsOptions +): void { + if (isObject(events)) { + for (const eventType of Object.keys(events)) { + removeDeadConditionsInEvent(events, eventType, options); + } + } +} + +function removeDeadConditionsInEvent< + T extends string, + P extends Partial> +>(events: P, eventType: T, options: RemoveDeadConditionsOptions): void { + const handlers = events[eventType]; + if (!handlers) { + return; + } + if (Array.isArray(handlers)) { + removeDeadConditionsInArray(handlers, options, (handler) => { + if ((handler as UseProviderEventHandler).callback) { + removeDeadConditionsInEvents( + (handler as UseProviderEventHandler).callback as BrickEventsMap, + options + ); + } + }); + } else { + computeConstantCondition(handlers, options); + if (handlers.if === false) { + delete events[eventType]; + return; + } + if ((handlers as UseProviderEventHandler).callback) { + removeDeadConditionsInEvents( + (handlers as UseProviderEventHandler).callback as BrickEventsMap, + options + ); + } + } +} + +function removeDeadConditionsInContext( + context: ContextConf[], + options: RemoveDeadConditionsOptions +): void { + removeDeadConditionsInArray(context, options); +} + +function removeDeadConditionsInArray( + list: T[], + options: RemoveDeadConditionsOptions, + callback?: (item: T) => void +): void { + if (Array.isArray(list)) { + const removes: T[] = []; + for (const item of list) { + computeConstantCondition(item, options); + if (item.if === false) { + removes.push(item); + continue; + } + callback?.(item); + } + pull(list, ...removes); + } +} + +function removeDeadConditionsInLifeCycle( + lifeCycle: BrickLifeCycle, + options: RemoveDeadConditionsOptions +): void { + if (lifeCycle) { + removeDeadConditionsInArray(lifeCycle.useResolves, options); + + for (const key of [ + "onPageLoad", + "onPageLeave", + "onAnchorLoad", + "onAnchorUnload", + "onMessageClose", + "onBeforePageLoad", + "onBeforePageLeave", + "onMediaChange", + ] as const) { + removeDeadConditionsInEvent(lifeCycle, key, options); + } + for (const key of ["onMessage", "onScrollIntoView"] as const) { + for (const withHandlers of ( + [] as (MessageConf | ScrollIntoViewConf)[] + ).concat(lifeCycle[key])) { + if (withHandlers) { + removeDeadConditionsInEvent(withHandlers, "handlers", options); + } + } + } + } +} + +export interface IfContainer { + if?: unknown; +} + +export function computeConstantCondition( + ifContainer: IfContainer, + options: RemoveDeadConditionsOptions = {} +): void { + if (hasOwnProperty(ifContainer, "if")) { + if (typeof ifContainer.if === "string" && isEvaluable(ifContainer.if)) { + try { + const { expression, attemptToVisitGlobals, source } = preevaluate( + ifContainer.if + ); + let hasOtherThanFlags = false; + for (const item of attemptToVisitGlobals) { + if (item !== "undefined" && item !== "FLAGS") { + hasOtherThanFlags = true; + break; + } + } + if (hasOtherThanFlags) { + if (isConstantLogical(expression, false, options)) { + if (process.env.NODE_ENV === "development") { + // eslint-disable-next-line no-console + console.warn("[removed dead if]:", ifContainer.if, ifContainer); + } + ifContainer.if = false; + } + return; + } + const { constantFeatureFlags, featureFlags } = options; + if (constantFeatureFlags) { + const originalIf = ifContainer.if; + ifContainer.if = !!cook(expression, source, { + globalVariables: { + undefined: undefined, + FLAGS: featureFlags, + }, + }); + if ( + process.env.NODE_ENV === "development" && + ifContainer.if === false + ) { + // eslint-disable-next-line no-console + console.warn("[removed dead if]:", originalIf, ifContainer); + } + } + } catch (error) { + // eslint-disable-next-line no-console + console.error("Parse storyboard expression failed:", error); + } + } else if (!ifContainer.if && ifContainer.if !== false) { + // eslint-disable-next-line no-console + console.warn( + "[potential dead if]:", + typeof ifContainer.if, + ifContainer.if, + ifContainer + ); + } + } +} + +/** + * We can safely remove the code for the following use cases, + * even though they contain runtime variables such as CTX: + * + * - if: '<% false && CTX.any %>' + * - if: '<% FLAGS["disabled"] && CTX.any %>' + * - if: '<% !FLAGS["enabled"] && CTX.any %>' + * - if: '<% !(FLAGS["enabled"] || CTX.any) %>' + * + * Since these logics will always get a falsy result. + * + * Here we simply only consider these kinds of AST node: + * + * - LogicalExpression: with operator of '||' or '&&' + * - UnaryExpression: with operator of '!' + * - Literal: such as boolean/number/string/null/regex + * - MemberExpression: of 'FLAGS["disabled"]' or 'FLAGS.disabled' + * - Identifier: of 'undefined' + */ +function isConstantLogical( + node: EstreeNode, + expect: boolean, + options: RemoveDeadConditionsOptions +): boolean { + const { constantFeatureFlags, featureFlags } = options; + return node.type === "LogicalExpression" + ? node.operator === (expect ? "||" : "&&") && + [node.left, node.right].some((item) => + isConstantLogical(item, expect, options) + ) + : node.type === "UnaryExpression" + ? node.operator === "!" && + isConstantLogical(node.argument, !expect, options) + : (node as unknown as EstreeLiteral).type === "Literal" + ? !!(node as unknown as EstreeLiteral).value === expect + : node.type === "Identifier" + ? node.name === "undefined" + ? !expect + : false + : constantFeatureFlags && + node.type === "MemberExpression" && + node.object.type === "Identifier" && + node.object.name === "FLAGS" && + (node.computed + ? (node.property as unknown as EstreeLiteral).type === "Literal" && + typeof (node.property as unknown as EstreeLiteral).value === + "string" && + !!featureFlags[ + (node.property as unknown as EstreeLiteral).value as string + ] === expect + : node.property.type === "Identifier" && + !!featureFlags[node.property.name] === expect); +} diff --git a/packages/brick-utils/src/scanStoryboard.ts b/packages/brick-utils/src/scanStoryboard.ts index fd7d12d620..be9e55a43c 100644 --- a/packages/brick-utils/src/scanStoryboard.ts +++ b/packages/brick-utils/src/scanStoryboard.ts @@ -13,6 +13,7 @@ import { ResolveConf, MessageConf, UseBackendConf, + ScrollIntoViewConf, } from "@next-core/brick-types"; import { uniq } from "lodash"; import { isObject } from "./isObject"; @@ -110,6 +111,10 @@ export function collectBricksInBrickConf( onAnchorUnload, onMessage, onMessageClose, + onBeforePageLoad, + onBeforePageLeave, + onMediaChange, + onScrollIntoView, } = brickConf.lifeCycle; if (Array.isArray(useResolves)) { useResolves.forEach((useResolve) => { @@ -120,8 +125,8 @@ export function collectBricksInBrickConf( }); } - const messageLifeCycleHandlers = ([] as MessageConf[]) - .concat(onMessage) + const specialHandlers = ([] as (MessageConf | ScrollIntoViewConf)[]) + .concat(onMessage, onScrollIntoView) .filter(Boolean) .reduce( (previousValue, currentValue) => @@ -136,7 +141,10 @@ export function collectBricksInBrickConf( onAnchorLoad, onAnchorUnload, onMessageClose, - onMessage: messageLifeCycleHandlers, + onBeforePageLoad, + onBeforePageLeave, + onMediaChange, + specialHandlers, }, collection );