Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions etc/runtime.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type { BatchUpdateContextItem } from '@next-core/types';
import type { BootstrapData } from '@next-core/types';
import { BreadcrumbItemConf } from '@next-core/types';
import type { BrickConf } from '@next-core/types';
import type { BrickEventHandler } from '@next-core/types';
import type { BrickEventHandlerCallback } from '@next-core/types';
import type { BrickEventsMap } from '@next-core/types';
import type { BrickLifeCycle } from '@next-core/types';
Expand Down Expand Up @@ -58,8 +57,6 @@ declare namespace __secret_internals {
updateStoryboardByTemplate,
updateTemplatePreviewSettings,
updateStoryboardBySnippet,
getContextValue,
getAllContextValues,
getBrickPackagesById,
loadBricks,
loadEditors,
Expand Down Expand Up @@ -187,9 +184,6 @@ export function fetchByProvider(provider: string, args: unknown[], options?: Res
// @public (undocumented)
function getAddedContracts(storyboardPatch: PreviewStoryboardPatch, { appId, updateStoryboardType, collectUsedContracts }: PreviewOption): Promise<string[]>;

// @public (undocumented)
function getAllContextValues({ tplStateStoreId, }: DataValueOption): Record<string, unknown>;

// @public @deprecated (undocumented)
export function getAuth(): object | undefined;

Expand All @@ -202,9 +196,6 @@ function getBrickPackages(): BrickPackage[];
// @public (undocumented)
function getBrickPackagesById(id: string): BrickPackage | undefined;

// @public (undocumented)
function getContextValue(name: string, { tplStateStoreId }: DataValueOption): unknown;

// @public (undocumented)
export function getCssPropertyValue(name: string, el?: HTMLElement): string;

Expand Down Expand Up @@ -333,6 +324,10 @@ function mountUseBrick({ renderRoot, rendererContext, scopedStores }: RenderUseB

// @public (undocumented)
interface MountUseBrickResult {
// Warning: (ae-forgotten-export) The symbol "Dispose" needs to be exported by the entry point index.d.ts
//
// (undocumented)
dispose: Dispose;
Comment on lines +327 to +330
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Script executed:

#!/bin/bash
# Find the entry point file for the runtime package
find . -path "*/packages/runtime/*" -type f \( -name "index.ts" -o -name "index.d.ts" \) | head -10

Repository: easyops-cn/next-core


<shell_result>
Repository: easyops-cn/next-core
Exit code: 0

stdout:

./packages/runtime/src/index.ts

</shell_result>


Script executed:

#!/bin/bash
# Read the entry point file to check current exports
cat -n ./packages/runtime/src/index.ts

Repository: easyops-cn/next-core


<shell_result>
Repository: easyops-cn/next-core
Exit code: 0

stdout:

     1	export * from "./auth.js";
     2	export * from "./CustomEditors.js";
     3	export * from "./CustomProcessors.js";
     4	export { customTemplates } from "./CustomTemplates.js";
     5	export * from "./fetchByProvider.js";
     6	export * from "./getBasePath.js";
     7	export * from "./getPageInfo.js";
     8	export * from "./handleHttpError.js";
     9	export * from "./history.js";
    10	export * from "./createRoot.js";
    11	export * from "./getRealValue.js";
    12	export {
    13	  createRuntime,
    14	  getRuntime,
    15	  type RuntimeOptions,
    16	  type RuntimeHooks,
    17	  type RuntimeHooksMenuHelpers,
    18	  type ImagesFactory,
    19	  type PageViewInfo,
    20	} from "./internal/Runtime.js";
    21	import * as __secret_internals from "./internal/secret_internals.js";
    22	export { __secret_internals };
    23	export { __test_only } from "./internal/test_only.js";
    24	export {
    25	  getCssPropertyValue,
    26	  getCurrentTheme,
    27	  getCurrentMode,
    28	  getThemeVariant,
    29	  batchSetAppsLocalTheme,
    30	  applyTheme,
    31	} from "./themeAndMode.js";
    32	export {
    33	  checkIfOfComputed,
    34	  checkIfByTransform,
    35	} from "./internal/compute/checkIf.js";
    36	export { registerWidgetFunctions } from "./internal/compute/WidgetFunctions.js";
    37	export { registerWidgetI18n } from "./internal/compute/WidgetI18n.js";
    38	export { StoryboardFunctionRegistryFactory } from "./StoryboardFunctionRegistry.js";
    39	export { matchPath, type MatchOptions } from "./internal/matchPath.js";
    40	export { Notification, type NotificationOptions } from "./Notification.js";
    41	export { Dialog, type DialogOptions } from "./Dialog.js";
    42	export * from "./getV2RuntimeFromDll.js";
    43	export { setUIVersion } from "./setUIVersion.js";
    44	export * from "./ModalStack.js";
    45	export * from "./isNetworkError.js";
    46	export * from "./shouldReloadForError.js";

</shell_result>


Script executed:

#!/bin/bash
# Search for MountUseBrickResult to see where it's defined and exported
rg -n "MountUseBrickResult" --type ts -A 2 -B 2 | head -40

Repository: easyops-cn/next-core


<shell_result>
Repository: easyops-cn/next-core
Exit code: 0

stdout:

packages/runtime/src/internal/secret_internals.ts-132-}
packages/runtime/src/internal/secret_internals.ts-133-
packages/runtime/src/internal/secret_internals.ts:134:export interface MountUseBrickResult {
packages/runtime/src/internal/secret_internals.ts-135-  portal?: HTMLElement;
packages/runtime/src/internal/secret_internals.ts-136-  dispose: Dispose;
--
packages/runtime/src/internal/secret_internals.ts-140-  { renderRoot, rendererContext, scopedStores }: RenderUseBrickResult,
packages/runtime/src/internal/secret_internals.ts-141-  element: HTMLElement
packages/runtime/src/internal/secret_internals.ts:142:): MountUseBrickResult {
packages/runtime/src/internal/secret_internals.ts-143-  let portal: HTMLElement | undefined;
packages/runtime/src/internal/secret_internals.ts-144-  renderRoot.createPortal = () => {
--
packages/runtime/src/internal/secret_internals.ts-170-export function unmountUseBrick(
packages/runtime/src/internal/secret_internals.ts-171-  { rendererContext }: RenderUseBrickResult,
packages/runtime/src/internal/secret_internals.ts:172:  mountResult: MountUseBrickResult
packages/runtime/src/internal/secret_internals.ts-173-): void {
packages/runtime/src/internal/secret_internals.ts-174-  // if (mountResult.mainBrick) {
--
packages/runtime/src/internal/Renderer.spec.ts-108-  "my-use-brick",
packages/runtime/src/internal/Renderer.spec.ts-109-  class MyUseBrick extends HTMLElement {
packages/runtime/src/internal/Renderer.spec.ts:110:    #mountResult: __secret_internals.MountUseBrickResult | undefined;
packages/runtime/src/internal/Renderer.spec.ts-111-    #renderResult: __secret_internals.RenderUseBrickResult | undefined;
packages/runtime/src/internal/Renderer.spec.ts-112-    async connectedCallback() {
--
packages/react-runtime/src/ReactUseBrick.tsx-35-  const [renderResult, setRenderResult] =
packages/react-runtime/src/ReactUseBrick.tsx-36-    useState<__secret_internals.RenderUseBrickResult | null>(null);
packages/react-runtime/src/ReactUseBrick.tsx:37:  const mountResult = useRef<__secret_internals.MountUseBrickResult>();
packages/react-runtime/src/ReactUseBrick.tsx-38-  const [renderKey, setRenderKey] = useState<number>();
packages/react-runtime/src/ReactUseBrick.tsx-39-  const IdCounterRef = useRef(0);

</shell_result>


Script executed:

#!/bin/bash
# Check the import of Dispose in secret_internals.ts
head -50 ./packages/runtime/src/internal/secret_internals.ts | grep -n "Dispose\|import"

Repository: easyops-cn/next-core


<shell_result>
Repository: easyops-cn/next-core
Exit code: 0

stdout:

1:import type {
12:import { pick } from "lodash";
13:import {
19:import {
24:import { RendererContext } from "./RendererContext.js";
25:import type { DataStore } from "./data/DataStore.js";
26:import { resolveData } from "./data/resolveData.js";
27:import { asyncComputeRealValue } from "./compute/computeRealValue.js";
28:import type {
39:  Dispose,
41:import { mountTree, unmountTree } from "./mount.js";
42:import { RenderTag } from "./enums.js";
43:import { isStrictMode } from "../isStrictMode.js";
44:import { customTemplates } from "../CustomTemplates.js";
45:import { registerAppI18n } from "./registerAppI18n.js";
46:import { getTplStateStore } from "./CustomTemplates/utils.js";
47:import {

</shell_result>


Script executed:

#!/bin/bash
# Verify the full import statement for Dispose
sed -n '1,45p' ./packages/runtime/src/internal/secret_internals.ts | grep -A 15 "^import type {"

Repository: easyops-cn/next-core


<shell_result>
Repository: easyops-cn/next-core
Exit code: 0

stdout:

import type {
  BrickConf,
  CustomTemplate,
  RouteConf,
  Storyboard,
  UseSingleBrickConf,
  RuntimeSnippet,
  ContextConf,
  BrickPackage,
  RouteConfOfBricks,
} from "@next-core/types";
import { pick } from "lodash";
import {
  _internalApiGetRenderId,
  _internalApiGetRuntimeContext,
  _internalApiGetStoryboardInBootstrapData,
--
import type { DataStore } from "./data/DataStore.js";
import { resolveData } from "./data/resolveData.js";
import { asyncComputeRealValue } from "./compute/computeRealValue.js";
import type {
  DataValueOption,
  PreviewOption,
  PreviewStoryboardPatch,
  RenderRoot,
  RuntimeContext,
  DebugDataValue,
  RuntimeDataVale,
  RuntimeDataValueOption,
  RenderBrick,
  RenderChildNode,
  Dispose,
} from "./interfaces.js";
import { mountTree, unmountTree } from "./mount.js";
import { RenderTag } from "./enums.js";
import { isStrictMode } from "../isStrictMode.js";

</shell_result>


Add Dispose type to entry point exports to resolve API Extractor warning

The warning is valid: Dispose is used in the public API (MountUseBrickResult interface exported via __secret_internals) but not exported from the main entry point. The type is imported from ./internal/interfaces.ts into secret_internals.ts but never re-exported from packages/runtime/src/index.ts. This prevents users from directly referencing the Dispose type despite encountering it in the public API.

To fix this, add Dispose to the exports in packages/runtime/src/index.ts:

export {
  createRuntime,
  getRuntime,
  type RuntimeOptions,
  type RuntimeHooks,
  type RuntimeHooksMenuHelpers,
  type ImagesFactory,
  type PageViewInfo,
  type Dispose,  // Add this line
} from "./internal/Runtime.js";

Or add a separate export from interfaces.ts if Dispose is not already exported from Runtime.js.

🤖 Prompt for AI Agents
In etc/runtime.api.md around lines 335 to 338, the API Extractor warning shows
the Dispose type is referenced in the public API but not exported from the
package entry point; fix by exporting Dispose from the runtime entry
(packages/runtime/src/index.ts) — add Dispose to the export list (or if Dispose
is only declared in interfaces.ts, re-export it from Runtime.js or directly from
interfaces.ts) so the public surface exposes the Dispose type alongside the
other exported runtime types.

// (undocumented)
portal?: HTMLElement;
}
Expand Down
43 changes: 43 additions & 0 deletions mock-micro-apps/memory-leak/storyboard.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
app:
name: Memory Leak
id: memory-leak
homepage: /memory-leak
noAuthGuard: true
standaloneMode: true

routes:

- path: '${APP.homepage}'
incrementalSubRoutes: true
bricks:
- brick: h1
properties:
textContent: Debugging with memory leak
- brick: p
children:
- brick: eo-link
properties:
url: '${APP.homepage}/1'
textContent: Go to 1
- brick: p
children:
- brick: eo-link
properties:
url: '${APP.homepage}/2'
textContent: Go to 2
- brick: div
slots:
'':
type: routes
routes:
- path: '${APP.homepage}/1'
bricks:
- brick: ai-portal.elevo-card
properties:
cardTitle: HR
description: Provide standard HR workflows.
- path: '${APP.homepage}/2'
bricks:
- brick: p
properties:
textContent: This is page 2
9 changes: 8 additions & 1 deletion packages/react-element/src/ReactNextElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export abstract class ReactNextElement extends NextElement {
}
const ctor = this.constructor as typeof ReactNextElement;
if (ctor.shadowOptions) {
const shadowRoot = this.attachShadow(ctor.shadowOptions);
const shadowRoot =
this.shadowRoot || this.attachShadow(ctor.shadowOptions);
if (supportsAdoptingStyleSheets()) {
if (ctor.styleTexts?.length) {
const styleSheet = new CSSStyleSheet();
Expand All @@ -39,6 +40,12 @@ export abstract class ReactNextElement extends NextElement {

disconnectedCallback() {
this.#root?.render(null);
this.__secret_internal_dispose();
}

__secret_internal_dispose() {
this.#root?.unmount();
this.#root = undefined;
}

protected _render() {
Expand Down
11 changes: 9 additions & 2 deletions packages/runtime/src/createRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import {
} from "./internal/Renderer.js";
import { RendererContext } from "./internal/RendererContext.js";
import { DataStore } from "./internal/data/DataStore.js";
import type { RenderRoot, RuntimeContext } from "./internal/interfaces.js";
import type {
Dispose,
RenderRoot,
RuntimeContext,
} from "./internal/interfaces.js";
import { mountTree, unmountTree } from "./internal/mount.js";
import { applyMode, applyTheme, setMode, setTheme } from "./themeAndMode.js";
import { RenderTag } from "./internal/enums.js";
Expand Down Expand Up @@ -107,6 +111,7 @@ export function unstable_createRoot(
let unmounted = false;
let rendererContext: RendererContext | undefined;
let clearI18nBundles: Function | undefined;
let disposeMount: Dispose | undefined;
const isolatedRoot = scope === "page" ? undefined : Symbol("IsolatedRoot");

return {
Expand Down Expand Up @@ -253,7 +258,7 @@ export function unstable_createRoot(
applyMode();
}

mountTree(renderRoot);
disposeMount = mountTree(renderRoot);

if (scope === "page") {
window.scrollTo(0, 0);
Expand Down Expand Up @@ -283,6 +288,8 @@ export function unstable_createRoot(
isolatedFunctionRegistry.delete(isolatedRoot);
isolatedTemplateRegistryMap.delete(isolatedRoot);
}
disposeMount?.();
disposeMount = undefined;
unmountTree(container);
if (portal) {
unmountTree(portal);
Expand Down
7 changes: 7 additions & 0 deletions packages/runtime/src/internal/RendererContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,11 +346,18 @@ export class RendererContext {

unbindTemplateProxy(brick);
delete brick.element?.$$tplStateStore;
brick.element?.$$disposes?.forEach((dispose) => dispose());
delete brick.element?.$$disposes;
// Also remove the element
brick.element?.remove();
// Dispose context listeners
brick.disposes?.forEach((dispose) => dispose());
delete brick.disposes;
(
brick.element as {
__secret_internal_dispose?: () => void;
}
)?.__secret_internal_dispose?.();
}

// Dispatch unmount events
Expand Down
25 changes: 23 additions & 2 deletions packages/runtime/src/internal/Router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
} from "./Runtime.js";
import { getPageInfo } from "../getPageInfo.js";
import type {
Dispose,
MenuRequestNode,
RenderRoot,
RuntimeContext,
Expand Down Expand Up @@ -84,6 +85,7 @@ export class Router {
#runtimeContext?: RuntimeContext;
#rendererContext?: RendererContext;
#rendererContextTrashCan = new Set<RendererContext | undefined>();
#runtimeContextTrashCan = new Set<RuntimeContext | undefined>();
#redirectCount = 0;
#renderId?: string;
#currentApp?: MicroApp;
Expand Down Expand Up @@ -354,10 +356,12 @@ export class Router {
// Set `Router::#currentApp` before calling `getFeatureFlags()`
const flags = getRuntime().getFeatureFlags();
const prevRendererContext = this.#rendererContext;
const prevRuntimeContext = this.#runtimeContext;

const redirectTo = (to: string, state?: NextHistoryState): void => {
finishPageView?.({ status: "redirected" });
this.#rendererContextTrashCan.add(prevRendererContext);
this.#runtimeContextTrashCan.add(prevRuntimeContext);
this.#safeRedirect(to, state, location);
};

Expand All @@ -376,7 +380,11 @@ export class Router {
createPortal: portal,
};

let disposeMount: Dispose | undefined;

const cleanUpPreviousRender = (): void => {
disposeMount?.();
disposeMount = undefined;
unmountTree(main);
unmountTree(portal);

Expand All @@ -389,6 +397,18 @@ export class Router {
}
}
this.#rendererContextTrashCan.clear();

this.#runtimeContextTrashCan.add(prevRuntimeContext);
this.#runtimeContextTrashCan.forEach((item) => {
if (item) {
const stores = getDataStores(item);
for (const store of stores) {
store.dispose();
}
}
});
this.#runtimeContextTrashCan.clear();

hooks?.messageDispatcher?.reset();

if (appChanged) {
Expand Down Expand Up @@ -439,6 +459,7 @@ export class Router {
return;
} else if (error instanceof HttpAbortError) {
this.#rendererContextTrashCan.add(prevRendererContext);
this.#runtimeContextTrashCan.add(prevRuntimeContext);
return;
} else {
const noAuthGuardLoginPath =
Expand Down Expand Up @@ -547,7 +568,7 @@ export class Router {
applyMode();

setUIVersion(currentApp?.uiVersion);
mountTree(renderRoot);
disposeMount = mountTree(renderRoot);

// Scroll to top after each rendering.
// See https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/guides/scroll-restoration.md
Expand Down Expand Up @@ -606,7 +627,7 @@ export class Router {
);
renderRoot.child = node;

mountTree(renderRoot);
disposeMount = mountTree(renderRoot);

// Scroll to top after each rendering.
window.scrollTo(0, 0);
Expand Down
4 changes: 4 additions & 0 deletions packages/runtime/src/internal/bindListeners.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,7 @@ describe("listenerFactory for handleHttpError", () => {

describe("listenerFactory for event.*", () => {
test("event.preventDefault", () => {
consoleError.mockReturnValue();
const event = { preventDefault: jest.fn() } as any;
listenerFactory(
{
Expand All @@ -684,9 +685,11 @@ describe("listenerFactory for event.*", () => {
runtimeContext
)(event);
expect(event.preventDefault).toHaveBeenCalledWith();
consoleError.mockReset();
});

test("event.stopPropagation", () => {
consoleError.mockReturnValue();
const event = { stopPropagation: jest.fn() } as any;
listenerFactory(
{
Expand All @@ -695,6 +698,7 @@ describe("listenerFactory for event.*", () => {
runtimeContext
)(event);
expect(event.stopPropagation).toHaveBeenCalledWith();
consoleError.mockReset();
});

test("non-Event object", () => {
Expand Down
39 changes: 18 additions & 21 deletions packages/runtime/src/internal/bindListeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { isPreEvaluated } from "./compute/evaluate.js";
import { setProperties } from "./compute/setProperties.js";
import { applyMode, applyTheme } from "../themeAndMode.js";
import type {
Dispose,
ElementHolder,
RuntimeBrickElement,
RuntimeContext,
Expand All @@ -45,39 +46,35 @@ export function bindListeners(
brick: RuntimeBrickElement,
eventsMap: BrickEventsMap | undefined,
runtimeContext: RuntimeContext
): void {
): Dispose {
if (!eventsMap) {
return;
return () => {};
}

const disposables: Dispose[] = [];

Object.entries(eventsMap).forEach(([eventType, handlers]) => {
const listener = listenerFactory(handlers, runtimeContext, {
element: brick,
});
brick.addEventListener(eventType, listener);

// Remember added listeners for unbinding.
if (!brick.$$listeners) {
brick.$$listeners = [];
}
brick.$$listeners.push([eventType, listener]);

// Remember added listeners for devtools.
if (!brick.$$eventListeners) {
brick.$$eventListeners = [];
}
for (const handler of ([] as BrickEventHandler[]).concat(handlers)) {
brick.$$eventListeners.push([eventType, null, handler]);
}
const dispose = () => {
brick.removeEventListener(eventType, listener);
};
disposables.push(dispose);
brick.$$disposes ??= [];
brick.$$disposes.push(dispose);
});
}

export function unbindListeners(brick: RuntimeBrickElement): void {
if (brick.$$listeners) {
for (const [eventType, listener] of brick.$$listeners) {
brick.removeEventListener(eventType, listener);
return () => {
for (const dispose of disposables) {
dispose();
}
brick.$$listeners.length = 0;
}
disposables.length = 0;
delete brick.$$disposes;
};
}

export function isBuiltinHandler(
Expand Down
Loading
Loading