Skip to content

refactor(portal-framework-core, portal-plugin-dashboard): refactor widget system with improved area management and grid layout#458

Merged
pcfreak30 merged 1 commit intodevelopfrom
libs/portal-plugin-dashboard
Aug 22, 2025
Merged

refactor(portal-framework-core, portal-plugin-dashboard): refactor widget system with improved area management and grid layout#458
pcfreak30 merged 1 commit intodevelopfrom
libs/portal-plugin-dashboard

Conversation

@pcfreak30
Copy link
Copy Markdown
Member

@pcfreak30 pcfreak30 commented Aug 22, 2025

  • Introduced new widget types and interfaces for better type safety
  • Implemented proper grid layout management with column/row spanning
  • Added widget area definitions and registration methods
  • Updated WidgetArea component to use new grid system
  • Improved error handling and validation for widget registration
  • Added sorting by order and column position

Summary by CodeRabbit

  • New Features

    • Framework exposes portal metadata/URL and initialization status; context provides app name and widget-area/widget lookup.
    • Remote component loading improved with safer default-export handling and clearer loading/fallback behavior.
    • New grid helpers and widget-sorting utility for consistent layout.
  • Refactor

    • Widgets reorganized into explicit areas with automatic registration; widget rendering now uses a framework-driven grid and WidgetArea prop changed to id.
  • Style

    • Dashboard Card updated to full-height vertical layout with bottom-aligned content.
  • Chores

    • Dashboard plugin and registrations updated to the new widget/area model.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Aug 22, 2025

⚠️ No Changeset found

Latest commit: 520dbda

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pcfreak30
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Aug 22, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Aug 22, 2025

Warning

Rate limit exceeded

@pcfreak30 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 8 minutes and 28 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 30839ea and 520dbda.

📒 Files selected for processing (42)
  • libs/portal-framework-core/src/api/builder.ts (3 hunks)
  • libs/portal-framework-core/src/api/framework.spec.ts (4 hunks)
  • libs/portal-framework-core/src/api/framework.ts (8 hunks)
  • libs/portal-framework-core/src/components/ErrorDisplay.stories.tsx (2 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundary.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.spec.tsx (2 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteLoading.spec.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteLoading.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.spec.tsx (4 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.tsx (1 hunks)
  • libs/portal-framework-core/src/contexts/framework.tsx (3 hunks)
  • libs/portal-framework-core/src/env.ts (1 hunks)
  • libs/portal-framework-core/src/index.ts (1 hunks)
  • libs/portal-framework-core/src/plugins/context-bridge.tsx (3 hunks)
  • libs/portal-framework-core/src/plugins/manager.spec.ts (1 hunks)
  • libs/portal-framework-core/src/plugins/manager.ts (3 hunks)
  • libs/portal-framework-core/src/plugins/remoteComponentLoader.spec.tsx (1 hunks)
  • libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (6 hunks)
  • libs/portal-framework-core/src/types/api.ts (1 hunks)
  • libs/portal-framework-core/src/types/capabilities.ts (1 hunks)
  • libs/portal-framework-core/src/types/navigation.ts (3 hunks)
  • libs/portal-framework-core/src/types/plugin.ts (2 hunks)
  • libs/portal-framework-core/src/types/portal.ts (1 hunks)
  • libs/portal-framework-core/src/types/widget.ts (1 hunks)
  • libs/portal-framework-core/src/util/dependencyGraph.spec.ts (4 hunks)
  • libs/portal-framework-core/src/util/dependencyGraph.ts (3 hunks)
  • libs/portal-framework-core/src/util/domain.spec.ts (1 hunks)
  • libs/portal-framework-core/src/util/framework-initializer.ts (1 hunks)
  • libs/portal-framework-core/src/util/namespace.spec.ts (2 hunks)
  • libs/portal-framework-core/src/util/portalMeta.spec.ts (5 hunks)
  • libs/portal-framework-core/src/util/portalMeta.ts (2 hunks)
  • libs/portal-framework-core/src/utils/grid.ts (1 hunks)
  • libs/portal-framework-core/src/utils/widget.ts (1 hunks)
  • libs/portal-framework-core/src/vite/index.ts (1 hunks)
  • libs/portal-plugin-dashboard/src/index.ts (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/components/Card.tsx (3 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/account.profile.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/account.security.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/dashboard.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/widgetRegistrations.ts (1 hunks)

Walkthrough

Reworks widget model to typed areas and runtime registries, replaces WidgetArea rendering with a framework-driven grid, migrates plugins to export areas/widgets, extends remote component loader types, adds grid/widget utilities, and adjusts tests, stories, and public exports.

Changes

Cohort / File(s) Summary
Framework & widget core
libs/portal-framework-core/src/api/framework.ts, libs/portal-framework-core/src/api/builder.ts, libs/portal-framework-core/src/contexts/framework.tsx, libs/portal-framework-core/src/index.ts, libs/portal-framework-core/src/types/widget.ts, libs/portal-framework-core/src/types/plugin.ts
Introduces WidgetArea/WidgetDefinition types and runtime maps; adds register/get APIs, meta/portalUrl getters and isInitialized(); initialize auto-registers areas/widgets; builder import reorder and stricter non-null assertion when loading remote modules.
WidgetArea UI & grid util
libs/portal-framework-core/src/components/WidgetArea.tsx, libs/portal-framework-core/src/utils/grid.ts, libs/portal-framework-core/src/utils/widget.ts
Replaces remote-wrapper/packing layout with framework-driven grid rendering; component prop renamed widgetAreaIdid; adds grid helpers and sortWidgets utility.
Plugin dashboard migration
libs/portal-plugin-dashboard/src/widgetRegistrations.ts, libs/portal-plugin-dashboard/src/index.ts, libs/portal-plugin-dashboard/src/ui/routes/*, libs/portal-plugin-dashboard/src/ui/components/Card.tsx
Plugin now exports { areas, widgets } and exposes widgets property; widget entries reshaped to areaId/id/position; WidgetArea usages updated to id; added DashRefineConfigCapability; Card accepts headerClassName and layout tweaks.
Remote component loader
libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx, libs/portal-framework-core/src/plugins/remoteComponentLoader.spec.tsx
Expands public types (RemoteComponentParams/Props/RenderFnParams), introduces LazyRemoteComponentInfo alias, changes BridgeResult.destroy to synchronous void, and refines module/default extraction for component resolution.
Context, plugin manager & context-bridge
libs/portal-framework-core/src/plugins/manager.ts, libs/portal-framework-core/src/plugins/context-bridge.tsx, libs/portal-framework-core/src/plugins/manager.spec.ts
Minor method reordering and formatting; context-bridge.register signature tidied; loadFeature now resolves to feature instance; tests formatting adjusted.
Types & navigation
libs/portal-framework-core/src/types/navigation.ts, libs/portal-framework-core/src/types/api.ts, libs/portal-framework-core/src/types/capabilities.ts, libs/portal-framework-core/src/types/portal.ts, libs/portal-framework-core/src/index.ts
Adds navigation fields (disabled/hidden/icon/parentId), reorders several type declarations, converts Identity type→interface, exposes new widget types in public index and adjusts exported type orderings.
Utilities & dependency graph
libs/portal-framework-core/src/util/dependencyGraph.ts, .../dependencyGraph.spec.ts, libs/portal-framework-core/src/util/framework-initializer.ts, libs/portal-framework-core/src/util/portalMeta.ts, .../portalMeta.spec.ts
Adds DependencyGraph APIs (addDependency, getDependencies/getDependents, topologicalSort), simplifies initializer predicate, adds __test_clearCache for portal meta, and updates tests/formatting.
Stories & tests formatting
libs/portal-framework-core/src/components/*stories*.tsx, .../components/RouteErrorBoundaryFallback.spec.tsx, .../components/RouteLoading.spec.tsx, .../components/WidgetArea*.tsx, libs/portal-framework-core/src/api/framework.spec.ts
Mostly quote/order/formatting updates; tests updated to assert auto-registration via Framework.initialize and to use new widget APIs and prop names.
Env / exports / vite
libs/portal-framework-core/src/env.ts, libs/portal-framework-core/src/vite/index.ts
Reorders env key placement (VITE_PORTAL_ALLOW_LOCALHOST) and export ordering; adds util/domain export; no semantic behavior changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App
  participant Provider as FrameworkProvider
  participant Framework
  participant PluginMgr as PluginManager
  participant Plugins
  participant UI as WidgetArea

  App->>Provider: mount
  Provider->>Framework: initialize()
  activate Framework
  Framework->>PluginMgr: initializePlugins()
  PluginMgr-->>Plugins: load modules
  Plugins-->>Framework: modules (areas, widgets)
  Framework->>Framework: registerWidgetArea(area)
  Framework->>Framework: registerWidgetsFromPlugin(pluginId, widgets)
  Framework-->>Provider: initialized
  deactivate Framework

  App->>UI: render <WidgetArea id="...">
  UI->>Provider: useFramework().getWidgetArea(id)
  Provider->>Framework: getWidgetArea(id)
  Framework-->>Provider: WidgetAreaDefinition
  UI->>Provider: useFramework().getWidgetsForArea(id)
  Provider->>Framework: getWidgetsForArea(id)
  Framework-->>Provider: WidgetDefinition[]
  Provider-->>UI: area + widgets
  UI->>UI: compute grid styles & render components
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

A rabbit nudges bytes with care,
Areas planted, widgets share.
Plugins hop in, tidy and neat,
Grids align each tiny seat.
Framework hums — the dashboard's fair. 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch libs/portal-plugin-dashboard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
libs/portal-framework-core/src/plugins/manager.ts (1)

420-425: Critical: retryFailedPlugins calls loadFeature with a plugin id

loadFeature expects a feature id. Passing a plugin id will either throw or no-op depending on ids, preventing actual retry of failed plugins. Retrying plugins should re-attempt plugin initialization, not feature loading. Two viable fixes:

  • Option A (retry plugin init): re-initialize each failed plugin.
  • Option B (retry failed features): maintain/inspect a list of failed feature ids and retry those.

Below is Option A, which aligns with the method name.

   async retryFailedPlugins() {
     const failedPlugins = Array.from(this.#pluginStates.entries())
       .filter(
         ([_, state]) =>
           state.loadState === "failed" || state.initState === "failed",
       )
       .map(([id]) => id);

     for (const pluginId of failedPlugins) {
       try {
-        await this.loadFeature(pluginId);
+        // Re-attempt plugin activation/initialization
+        this.getOrActivatePlugin(pluginId);
+        const plugin = this.#loadedPlugins.get(pluginId);
+        if (plugin?.initialize) {
+          this.#markPluginInitializing(pluginId);
+          await plugin.initialize(this.framework);
+          this.#markPluginInitialized(pluginId);
+        }
       } catch (error) {
         console.error(`Retry failed for plugin ${pluginId}:`, error);
       }
     }
   }

If you prefer retrying failed features instead, I can provide an alternative patch that iterates #featureStates for state === "failed" and calls loadFeature with those feature ids.

Want me to push a follow-up commit for your chosen option?

libs/portal-framework-core/src/types/plugin.ts (2)

47-49: Avoid extending Node’s Module here; simplify PluginModule type

Extending node:module is fragile (CJS/ESM types) and unnecessary for tests that only rely on default(). Recommend removing the extends and the import.

-import * as Module from "node:module";
...
-export interface PluginModule extends Module {
-  default: () => Plugin;
-}
+export interface PluginModule {
+  default: () => Plugin;
+}

3-3: Absolute import path not configured in this package

The import in libs/portal-framework-core/src/types/plugin.ts

import { Framework } from "libs/portal-framework-core/src/api/framework";

relies on a libs/... path alias, but

  • libs/portal-framework-core/tsconfig.json has no compilerOptions.baseUrl or paths defined
  • there is no root tsconfig.json configuring such an alias

As‐written, this will fail TypeScript’s and Node’s module resolution. Please replace it with a relative import:

- import { Framework } from "libs/portal-framework-core/src/api/framework";
+ import { Framework } from "../api/framework";
libs/portal-framework-core/src/util/framework-initializer.ts (2)

101-112: Don’t cache partially failed framework state; gate on result.success

Currently, the state is cached whenever framework.isInitialized() is true, even if plugin registration failed (errors collected) or initResult.success is false. That prevents retries on subsequent calls.

-    // Store successful initialization state
-    // Use the public getter instead of accessing the private field
-    if (result.framework.isInitialized()) {
+    // Store state only when initialization fully succeeded
+    if (result.success && result.framework.isInitialized()) {
       initializationState.set(appName, {
         builder: result.builder,
         framework: result.framework,
       });
     } else {
       // If initialization failed, remove from state to allow retry
       initializationState.delete(appName);
     }

114-133: Avoid returning an undefined framework via non-null assertion in error path

In the catch block, framework! may be undefined at runtime, leaking an invalid object shape and causing downstream crashes.

Two safe options:

  • If framework is unavailable, rethrow; callers cannot proceed without it.
  • Or relax InitializationResult to make framework optional when success: false.

Minimal code change (rethrow when framework unavailable):

   } catch (err) {
     // Handle unexpected system-level errors
     // Ensure builder is defined before returning
     if (!builder) {
       // If builder creation failed, we can't return a builder
       throw err; // Re-throw if builder is null
     }
-    return {
-      builder: builder,
-      errors: [
-        {
-          category: "system",
-          error: err instanceof Error ? err : new Error(String(err)),
-          id: "system-initialization",
-        },
-      ],
-      framework: framework!, // framework might be undefined if error happened before builder.framework
-      success: false,
-    };
+    if (!framework) {
+      // Without a framework instance, propagating the error is safer than returning an invalid object.
+      throw err;
+    }
+    return {
+      builder,
+      errors: [
+        {
+          category: "system",
+          error: err instanceof Error ? err : new Error(String(err)),
+          id: "system-initialization",
+        },
+      ],
+      framework,
+      success: false,
+    };

If you’d prefer to return a structured failure without rethrowing, I can draft a change to InitializationResult to allow framework?: Framework on failure.

libs/portal-framework-core/src/util/dependencyGraph.ts (1)

22-28: getDependencies/getDependents throw for unknown nodes

new Set(this.#dependencies.get(node)) throws when node hasn’t been added. Return an empty set instead.

  getDependencies(node: T): Set<T> {
-    return new Set(this.#dependencies.get(node));
+    return new Set(this.#dependencies.get(node) ?? []);
  }

  getDependents(node: T): Set<T> {
-    return new Set(this.#reverseLookup.get(node));
+    return new Set(this.#reverseLookup.get(node) ?? []);
  }

Optional: call this.addNode(node) at the start of each getter if you prefer to auto-register unknown nodes.

🧹 Nitpick comments (56)
libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.spec.tsx (1)

69-81: Make console spy resilient and always restored; avoid brittle call-count assertion

If another internal log occurs (e.g., React dev/StrictMode warnings), toHaveBeenCalledTimes(1) can make this test flaky. Also, ensure the spy is restored even when an assertion fails by using try/finally.

Apply this diff to the test block:

     const consoleErrorSpy = vi
       .spyOn(console, "error")
       .mockImplementation(() => {});
+    try {
       const error = { some: "weird format" };
       render(<RouteErrorBoundaryFallback error={error} />);
 
       expect(screen.getByText("An unknown error occurred")).toBeInTheDocument();
-    expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
       expect(consoleErrorSpy).toHaveBeenCalledWith(
         "RouteErrorBoundaryFallback received an unhandled error format:",
         error,
       );
-    consoleErrorSpy.mockRestore();
+    } finally {
+      consoleErrorSpy.mockRestore();
+    }

If you still want a count assertion, consider toHaveBeenCalled() instead of an exact count to reduce flakiness in dev environments.

libs/portal-framework-core/src/plugins/remoteComponentLoader.spec.tsx (3)

77-95: Isolate module state between tests to avoid subtle leakage.

You dynamically import mocked modules inside beforeEach but never reset the module registry. Consider adding vi.resetModules() (or a top-level vi.mocked module getter + explicit mock resets) to guarantee fresh module state across tests, especially as the suite grows.

   beforeEach(async () => {
-    vi.clearAllMocks();
+    vi.clearAllMocks();
+    await vi.resetModules(); // ensure fresh module cache per test
     clearStore();

289-299: Strengthen the LoadingComponent assertion.

You render args.loading and check presence, which is good. You can also assert the element’s type to ensure the correct component was provided.

   const args = mockFrameworkCreateRemoteComponent.mock.calls[0][0];
   render(args.loading);
   expect(screen.getByTestId("loading-component")).toBeInTheDocument();
+  // Confirm the element is of the expected component type
+  expect((args.loading as any).type).toBe(MockLoadingComponent);

56-75: Avoid relying on private React internals in test helper.

clearStore accesses ctx._currentValue which is private and may change across React versions. Prefer exposing a test-only reset on the context store or tracking registered IDs in the test and reinitializing with known defaults.

Happy to draft a minimal test-only reset API for context-bridge’s store if you’d like.

libs/portal-framework-core/src/components/RouteLoading.spec.tsx (1)

13-15: Reduce brittleness by avoiding className-based selectors.

The test currently depends on exact CSS utility classes. Consider adding data-testid hooks (or role/label semantics) to the skeleton parts and querying those instead to make future refactors less fragile.

- expect(container.querySelectorAll(".h-4.bg-gray-200.rounded")).toHaveLength(
-   3,
- );
+ // e.g., <div data-testid="skeleton-line" />
+ expect(screen.getAllByTestId("skeleton-line")).toHaveLength(3);
libs/portal-framework-core/src/types/capabilities.ts (1)

22-23: Optional: add a runtime enum-like object for guards.

If you later need runtime validation or to expose a list of statuses, consider exporting a const object alongside the type.

 export type CapabilityStatus = "active" | "error" | "inactive";
+export const CAPABILITY_STATUS = {
+  ACTIVE: "active",
+  ERROR: "error",
+  INACTIVE: "inactive",
+} as const;
+export type CapabilityStatusValue =
+  (typeof CAPABILITY_STATUS)[keyof typeof CAPABILITY_STATUS];
libs/portal-plugin-dashboard/src/ui/components/Card.tsx (4)

35-39: Border color without border width; consider adding border utility as well

border && "border-border" only sets color. If UICard doesn't already apply a border width, the visual border may not render. Consider adding the width utility too.

-      className={cn(
-        border && "border-border",
-        "h-full flex flex-col",
-        className,
-      )}>
+      className={cn(
+        border && "border border-border",
+        "h-full flex flex-col",
+        className,
+      )}>

35-39: Prevent flex overflow in nested layouts

Cards now use a column flex layout and will often live inside grid/flex parents. Adding min-h-0 helps prevent overflow issues when children manage their own scrolling.

-        "h-full flex flex-col",
+        "h-full min-h-0 flex flex-col",

49-49: Unconditional bottom-alignment may be surprising; make it opt-in

mt-auto forces the content section to the bottom for every usage. Recommend making this behavior optional via a prop so existing cards that expect top-aligned content are unaffected.

 interface CardProps {
   border?: boolean;
   children: ReactNode;
   className?: string;
   contentClassName?: string;
   description?: string;
   icon?: LucideIcon;
   title?: string;
   titleClassName?: string;
+  bottomAlignContent?: boolean; // default false
 }
 
 export function Card({
   border = false,
   children,
   className = "",
   contentClassName = "",
   description,
   icon: Icon,
   title,
   titleClassName = "",
+  bottomAlignContent = false,
 }: CardProps) {
   return (
     <UICard
       className={cn(
         border && "border-border",
         "h-full flex flex-col",
         className,
       )}>
 ...
-      <UICardContent className={cn("space-y-4 mt-auto", contentClassName)}>
+      <UICardContent
+        className={cn("space-y-4", bottomAlignContent && "mt-auto", contentClassName)}
+      >
         {children}
       </UICardContent>

40-40: Avoid passing container className down to the header

CardHeader inherits the same className intended for the outer container. This can leak layout utilities (e.g., spacing/height/flex) into the header. Consider a dedicated headerClassName prop.

-interface CardProps {
+interface CardProps {
   ...
   titleClassName?: string;
+  headerClassName?: string;
 }
 
-export function Card({ ..., titleClassName = "", ... }: CardProps) {
+export function Card({ ..., titleClassName = "", headerClassName = "", ... }: CardProps) {
   ...
-  <CardHeader className={className}>
+  <CardHeader className={headerClassName}>
libs/portal-framework-core/src/util/domain.spec.ts (1)

47-50: Use boolean instead of string for env boolean

VITE_PORTAL_DOMAIN_IS_ROOT is typed as a boolean. Assigning a string ("true") works only if downstream coercion is relied upon. Prefer actual boolean to match types and avoid subtle bugs.

-      env.VITE_PORTAL_DOMAIN_IS_ROOT = "true";
+      env.VITE_PORTAL_DOMAIN_IS_ROOT = true;
libs/portal-framework-core/src/plugins/manager.ts (2)

373-385: Avoid instantiating plugin factories during dependency checks

Dependency validation calls each registered factory to inspect features. If factories have side effects or are heavy, this can cause duplication and unintended behavior at registration time. Consider requiring factories to expose static metadata (ids/features) without instantiation, or cache factory inspection results.


476-480: Don’t always mark loadState as failed on init failures

When isInitFailure is true, the plugin has already loaded but failed to initialize. Overwriting loadState to "failed" loses that nuance and can complicate retries and status reporting.

   #markPluginFailed(
     id: NamespacedId,
     error: Error,
     isInitFailure = false,
   ): void {
     const state = this.#pluginStates.get(id);
     this.#pluginStates.set(id, {
       ...state,
       error,
-      initState: isInitFailure ? "failed" : state?.initState || "failed",
-      loadState: "failed",
+      initState: isInitFailure ? "failed" : state?.initState || "failed",
+      loadState: isInitFailure ? (state?.loadState ?? "loaded") : "failed",
       retryCount: (state?.retryCount ?? 0) + 1,
     });
   }
libs/portal-framework-core/src/types/portal.ts (1)

1-9: Type alias → interface: confirm intended openness and add timestamp doc

Switching Identity from a type to an interface enables declaration merging. If consumers augment this type via ambient declarations, this change could alter behavior. If that’s intentional, LGTM; otherwise consider reverting or documenting. Also, consider documenting the timestamp format to avoid ambiguity.

Apply this small docs tweak:

 export interface Identity {
-  created_at: string;
+  /** ISO-8601 timestamp from the API, e.g. "2025-08-21T12:34:56Z". */
+  created_at: string;
   email: string;
   firstName: string;
   id: string;
   lastName: string;
   otp: boolean;
   verified: boolean;
 }
libs/portal-framework-core/src/util/portalMeta.ts (4)

53-56: Test-only helper should be marked internal to avoid accidental prod usage

Exporting __test_clearCache from a production module can invite accidental imports. At minimum, mark it as internal in code. Optionally, re-export it only from a test-only entrypoint.

-// Test helper to clear memoization cache
-export async function __test_clearCache() {
+/** @internal Test helper to clear memoization cache. Not for production use. */
+export async function __test_clearCache() {
   await _fetchPortalMeta.clear?.();
 }

62-66: Support keys with dots by allowing a string[] path overload

Current API can’t access keys that literally contain dots. Consider allowing key to be either a dot-path string or a pre-split string[].

-export function getPluginMeta<T = Record<string, unknown>>(
-  meta: PortalMeta | undefined,
-  pluginName: string,
-  key?: string,
-): T | undefined {
+export function getPluginMeta<T = Record<string, unknown>>(
+  meta: PortalMeta | undefined,
+  pluginName: string,
+  key?: string | string[],
+): T | undefined {

71-71: Complementary change for array-path support

Adapt the resolution logic to accept both string and string[].

-  return key.split(".").reduce((acc, part) => acc?.[part], pluginMeta) as T;
+  const parts = Array.isArray(key) ? key : key.split(".");
+  return parts.reduce((acc: any, part) => acc?.[part], pluginMeta) as T;

18-21: Minor: URL normalization branch is unreachable after new URL(...)

After new URL(portalUrl).toString(), fullEndpoint will always start with a scheme, making the startsWith("http") check effectively dead code. Either remove it or expand support for scheme-less inputs (e.g., "example.com") by normalizing before constructing URL.

libs/portal-framework-core/src/util/portalMeta.spec.ts (2)

88-99: Add a test to assert memoization is keyed by portalUrl

You assert de-duplication for identical URLs. Add a counterpart test to ensure different URLs don’t share cache entries.

Proposed test:

it("memoizes per portalUrl (different URLs produce separate fetches)", async () => {
  vi.mocked(fetch).mockResolvedValue({
    json: () => Promise.resolve(mockMeta),
    ok: true,
  } as Response);

  await fetchPortalMeta("https://example.com");
  await fetchPortalMeta("https://api.example.com");

  expect(fetch).toHaveBeenCalledTimes(2);
  expect(fetch).toHaveBeenNthCalledWith(1, "https://example.com/api/meta");
  expect(fetch).toHaveBeenNthCalledWith(2, "https://api.example.com/api/meta");
});

77-86: Consider a test for invalid portal URL handling

Add a case for a scheme-less or malformed URL (e.g., "example.com"), asserting the specific error string. This locks the dev DX behavior.

Example:

it("throws on invalid portal URL", async () => {
  await expect(fetchPortalMeta("example.com")).rejects.toThrow(
    "Invalid portal URL: example.com",
  );
});
libs/portal-framework-core/src/plugins/context-bridge.tsx (1)

34-53: Public API nit: keep explicit name: string annotation and tighten getName return type

Default value inference is fine; however, this is a public API and explicit string can be clearer for consumers. Also, while in this area, getName (Line 22) currently returns any; suggest string | undefined.

-  register<T>(context: React.Context<T>, name = ""): symbol {
+  register<T>(context: React.Context<T>, name: string = ""): symbol {

Additionally (outside this hunk):

// Line 22
getName(id: symbol): string | undefined {
  return this.contextNameMap.get(id);
}
libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.stories.tsx (2)

22-24: Replace alert with Storybook action for non-blocking UX

Use @storybook/addon-actions to log retries instead of blocking alerts.

+import { action } from "@storybook/addon-actions";
...
 export const WithRetry: Story = {
   args: {
-    error: new Error("Error with retry option"),
-    resetErrorBoundary: () => alert("Retry clicked!"),
+    error: new Error("Error with retry option"),
+    resetErrorBoundary: action("retry-clicked"),
   },
 };

5-9: Prefer “satisfies Meta<...>” for stricter prop checking on meta

Using satisfies guards against extra/incorrect fields without losing inference.

-const meta: Meta<typeof RouteErrorBoundaryFallback> = {
-  component: RouteErrorBoundaryFallback,
-  tags: ["autodocs"],
-  title: "Components/RouteErrorBoundaryFallback",
-};
+const meta = {
+  component: RouteErrorBoundaryFallback,
+  tags: ["autodocs"],
+  title: "Components/RouteErrorBoundaryFallback",
+} satisfies Meta<typeof RouteErrorBoundaryFallback>;
libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (6)

124-130: RemoteComponentProps exposes fallback/loading but they’re unused

You ignore props.fallback and props.loading in createRemoteComponent. Either wire them up to override info.fallback/info.loading or drop them to avoid confusion.

Option A – remove unused props:

-export interface RemoteComponentProps<T = Record<string, unknown>> {
-  [key: string]: unknown;
-  fallback?: React.ComponentType<{ error: Error }>;
-  loading?: React.ReactNode;
-  props?: T;
-}
+export interface RemoteComponentProps<T = Record<string, unknown>> {
+  [key: string]: unknown;
+  props?: T;
+}

Option B – allow per-instance overrides (then use them in the component):

 export interface RemoteComponentProps<T = Record<string, unknown>> {
   [key: string]: unknown;
-  fallback?: React.ComponentType<{ error: Error }>;
+  fallback?: React.ComponentType<FallbackProps>;
   loading?: React.ReactNode;
   props?: T;
 }

…then, in createRemoteComponent:

-  return forwardRef<HTMLDivElement, RemoteComponentProps>((props, ref) => {
-    const { props: componentProps } = props;
+  return function RemoteWrapper(props: RemoteComponentProps) {
+    const { props: componentProps, fallback, loading } = props;
     return (
-      <ErrorBoundary FallbackComponent={info.fallback as React.ComponentType<FallbackProps>}>
-        <React.Suspense fallback={info.loading}>
+      <ErrorBoundary FallbackComponent={(fallback ?? info.fallback) as React.ComponentType<FallbackProps>}>
+        <React.Suspense fallback={loading ?? info.loading}>
           {componentProps !== undefined ? (
             //@ts-ignore
             <LazyComponent {...componentProps} />
           ) : (
             //@ts-ignore
             <LazyComponent />
           )}
         </React.Suspense>
       </ErrorBoundary>
     );
-  });
+  };

178-202: Remove unused forwardRef or attach the ref

You declare forwardRef<HTMLDivElement, ...> but don’t attach ref to any element. Either forward it to a wrapper div or drop forwardRef for a simpler FC.

Minimal fix (drop forwardRef):

-  return forwardRef<HTMLDivElement, RemoteComponentProps>((props, ref) => {
+  return function RemoteWrapper(props: RemoteComponentProps) {
     const { props: componentProps } = props;
     return (
       <ErrorBoundary
         FallbackComponent={info.fallback as React.ComponentType<FallbackProps>}>
         <React.Suspense fallback={info.loading}>
           {componentProps !== undefined ? (
             //@ts-ignore
             <LazyComponent {...componentProps} />
           ) : (
             //@ts-ignore
             <LazyComponent />
           )}
         </React.Suspense>
       </ErrorBoundary>
     );
-  });
+  };

191-197: Eliminate //@ts-ignore by improving generics on LazyComponent

ts-ignore hides type drift. A pragmatic fix is to cast LazyComponent once where it’s used.

-            //@ts-ignore
-            <LazyComponent {...componentProps} />
+            {(LazyComponent as React.ComponentType<any>)?.({...componentProps})}
...
-            //@ts-ignore
-            <LazyComponent />
+            {(LazyComponent as React.ComponentType<any>)?.({})}

Longer-term: tighten createLazyRemoteComponent<T, E>() so it returns React.LazyExoticComponent<T["default"]> which aligns with the underlying export type.


48-50: Clarify return type with overloads for discriminated union

createRemoteComponentLoader returns two incompatible shapes depending on strategy. Overloads make call sites type-safe and self-documenting.

export function createRemoteComponentLoader(
  config: RemoteComponentConfig & { strategy: "bridge" },
  framework: Framework,
  options: RemoteComponentOptions,
): () => Promise<BridgeResult<any>>;
export function createRemoteComponentLoader(
  config: RemoteComponentConfig & { strategy?: "no-bridge" },
  framework: Framework,
  options: RemoteComponentOptions,
): React.ComponentType<RemoteComponentProps>;

68-69: Prefer nullish coalescing for default export fallback

Safer than || in the unlikely event default is a falsey object.

-      const Component = module.default || module;
+      const Component = module.default ?? module;

80-89: Consider enabling named exports in the “no-bridge” path for parity

Right now this loader only supports default exports. If you want feature parity with createRemoteComponent (which supports info.export), pass through the entire module and let createLazyRemoteComponent select the export.

-      loader: async (): Promise<{ default: React.ComponentType<any> }> => {
+      loader: async (): Promise<Record<string, any>> => {
         const module = await loadRemoteModule();
-        const Component = module.default;
-        if (typeof Component !== "function" && typeof Component !== "object") {
-          throw new Error(
-            `Remote module ${pluginId}:${componentPath} did not export a default React component.`,
-          );
-        }
-        return { default: Component as React.ComponentType<any> };
+        return module;
       },
libs/portal-plugin-dashboard/src/index.ts (1)

11-11: Migration to widgets property looks correct; minor naming nit

The plugin now exposing widgets aligns with the new API. Consider renaming the imported symbol for clarity since it no longer represents “registrations”.

-import dashboardWidgets from "./widgetRegistrations";
+import widgets from "./widgetRegistrations";
...
-    widgets: dashboardWidgets,
+    widgets,

Also applies to: 17-17

libs/portal-plugin-dashboard/src/ui/routes/account.security.tsx (2)

9-12: Prop rename to id is correct; simplify JSX

Good move to the new prop. You can also self-close the element.

-      <WidgetArea id={"core:dashboard:security"}></WidgetArea>
+      <WidgetArea id="core:dashboard:security" />

4-4: Rename component to match file/route

Function name AccountProfile in account.security.tsx is misleading. Rename for clarity:

-export default function AccountProfile() {
+export default function AccountSecurity() {
libs/portal-plugin-dashboard/src/ui/routes/account.profile.tsx (1)

11-11: Prefer self-closing + literal string for clarity

No children are passed. A self-closing tag and a plain string literal improve readability.

-      <WidgetArea id={"core:dashboard:profile"}></WidgetArea>
+      <WidgetArea id="core:dashboard:profile" />
libs/portal-framework-core/src/util/framework-initializer.ts (2)

35-70: Consider de-duplicating concurrent initializations per appName

Two overlapping calls can both see no state and perform duplicate work. A per-app in-flight Promise would serialize initialization and avoid races.

I can sketch a lightweight in-flight registry keyed by appName that resolves all waiters to the same result.


49-66: Refactor manifest iteration naming and stabilize module IDs

The getPortalPluginManifests function returns a plain array of URLs, so the local variable manifestsMap (and its “Map” suffix) is misleading. Also, using the array index to build moduleId ties the ID to the manifest order, which may change and break caching or remotes loading.

– Rename manifestsMap (lines 49–66 in framework-initializer.ts) to something like manifestUrls or manifests to reflect its string[] type.
– Keep using .map(...) on the array, or switch to a for…of loop if you need sequential control, but iteration itself is correct.
– Instead of

const moduleId = `remote-${index}`;

derive a stable ID from the manifest URL, for example:

import { createHash } from 'crypto';

const slug = new URL(manifestUrl).hostname.replace(/\./g, '-');
const shortHash = createHash('sha1').update(manifestUrl).digest('hex').slice(0, 8);
const moduleId = `remote-${slug}-${shortHash}`;

This ensures moduleId remains consistent across runs and tied to the plugin’s identity rather than its position.

libs/portal-framework-core/src/types/navigation.ts (3)

16-16: Use ComponentType over React.FC for icon components

React.FC implicitly includes children and can be restrictive; React.ComponentType is more flexible for stateless icons.

-  icon?: React.FC<NavigationItemIconProps>;
+  icon?: React.ComponentType<NavigationItemIconProps>;

20-23: parentId addition: verify tree-building invariants

The new parentId?: NamespacedId is useful. Ensure the route/nav builders validate that:

  • parentId refers to an existing node (or fail fast),
  • cycles are rejected, and
  • namespacing rules are applied once.

I can add guard logic and unit tests in the resolvers to enforce these invariants if desired.


3-3: Use a relative import for internal types

Verified that tsconfig.base.json does not define a path alias for @lumeweb/portal-framework-core, so importing via the deep workspace path bypasses your package’s exports and can break type identity in some build setups. Please update the import in libs/portal-framework-core/src/types/navigation.ts:

-import { NamespacedId } from "libs/portal-framework-core/src/types/plugin";
+import { NamespacedId } from "./plugin";
libs/portal-framework-core/src/util/dependencyGraph.ts (1)

6-12: Consider guarding against self-dependencies

Allowing addDependency(x, x) creates trivial cycles that always fail topo sort. If that’s not desired, reject them early with a clear error.

  addDependency(from: T, to: T): void {
+    if (from === to) {
+      throw new Error(`Self-dependency is not allowed: ${String(from)}`);
+    }
     this.addNode(from);
     this.addNode(to);

     this.#dependencies.get(from)!.add(to);
     this.#reverseLookup.get(to)!.add(from);
   }
libs/portal-framework-core/src/api/framework.spec.ts (3)

29-32: Avoid double-restoring the console.warn spy

vi.restoreAllMocks() already restores spies. The extra consoleWarnSpy.mockRestore() is redundant and can sometimes trip tests if the spy has been restored. Prefer a single restore call.

   afterEach(() => {
-    vi.restoreAllMocks();
-    consoleWarnSpy.mockRestore(); // Restore the spy after each test
+    vi.restoreAllMocks();
   });

88-91: Make assertion resilient to defaulted grid props

If Framework adds default gap/rowHeight to the returned area, toEqual will fail. Use partial matching to assert only on the essentials.

-    expect(framework.getWidgetArea("dashboard")).toEqual({
-      grid: { columns: 12 },
-      id: "dashboard",
-    });
+    expect(framework.getWidgetArea("dashboard")).toEqual(
+      expect.objectContaining({
+        id: "dashboard",
+        grid: expect.objectContaining({ columns: 12 }),
+      }),
+    );

174-185: Clarify the initialize-once assertions

PluginManager.initializePlugins is mocked, so we can’t reliably assert calls to plugin.initialize. The current “0 times” assertion reads like a correctness check but may be misleading. Recommend removing it and updating the comment.

-    // Verify the plugin's initialize method was NOT called again
-    expect(mockPluginInitialize).toHaveBeenCalledTimes(0); // PluginManager.initializePlugins calls this
+    // We don't assert plugin.initialize here because PluginManager.initializePlugins is mocked and may not delegate.
libs/portal-framework-core/src/contexts/framework.tsx (3)

48-48: Don’t hardcode dev mode

Hardcoding isDev = true will log errors in production. Use a standard env check.

-const isDev = true;
+const isDev = process.env.NODE_ENV !== "production";

120-138: Stabilize context callbacks to reduce rerenders

getAppName, getWidgetArea, and getWidgetsForArea are recreated each render. Memoize them to avoid unnecessary child rerenders, especially in large dashboards.

Example (apply as needed):

-  const contextValue: FrameworkContextValue = {
+  const contextValue: FrameworkContextValue = React.useMemo(() => ({
     error: state.error,
     framework: state.framework,
-    getAppName: () => appName,
-    getWidgetArea: (id: string) => {
+    getAppName: () => appName,
+    getWidgetArea: (id: string) => {
       if (!state.framework) {
         throw new Error("Framework not initialized");
       }
       return state.framework.getWidgetArea(id);
     },
-    getWidgetsForArea: (id: string) => {
+    getWidgetsForArea: (id: string) => {
       if (!state.framework) {
         throw new Error("Framework not initialized");
       }
       return state.framework.getWidgetsForArea(id);
     },
     isLoading: state.isLoading,
     reinitialize: initializeFrameworkInstance,
-  };
+  }), [appName, state.error, state.framework, state.isLoading, initializeFrameworkInstance]);

10-11: Unused import

NamespacedId isn’t used in this module. Drop it to keep the bundle lean.

-import type { NamespacedId } from "../types/plugin";
 import type { WidgetAreaDefinition, WidgetDefinition } from "../types/widget";
libs/portal-plugin-dashboard/src/widgetRegistrations.ts (2)

6-31: Reduce duplication of identical grid configs

The three areas share the same grid config. Extract a shared constant to keep things DRY and easier to change.

+const DEFAULT_GRID = { columns: 12, gap: 16, rowHeight: "auto" } as const;
 export const widgetAreas: WidgetAreaDefinition[] = [
   {
-    grid: {
-      columns: 12,
-      gap: 16,
-      rowHeight: "auto",
-    },
+    grid: DEFAULT_GRID,
     id: "core:dashboard:header",
   },
   {
-    grid: {
-      columns: 12,
-      gap: 16,
-      rowHeight: "auto",
-    },
+    grid: DEFAULT_GRID,
     id: "core:dashboard:profile",
   },
   {
-    grid: {
-      columns: 12,
-      gap: 16,
-      rowHeight: "auto",
-    },
+    grid: DEFAULT_GRID,
     id: "core:dashboard:security",
   },
 ];

57-66: Avoid id collision with area id

Widget id core:dashboard:profile matches the area id core:dashboard:profile. While registries may be separate, this is confusing and error-prone when debugging.

-    id: "core:dashboard:profile",
+    id: "core:dashboard:profile-widget",
libs/portal-framework-core/src/components/WidgetArea.stories.tsx (1)

9-9: Remove unused top-level MockWidget

You re-declare MockWidget inside _createRemoteComponent. The top-level component is unused.

-const MockWidget = () => <div className="p-4 border rounded">Mock Widget</div>;
libs/portal-framework-core/src/utils/grid.ts (4)

15-27: Type against WidgetAreaDefinition to avoid drift

Inline typing duplicates the grid shape. Prefer Pick<WidgetAreaDefinition, "grid"> so changes to the source type propagate automatically.

-import function getGridStyles(area: {
-  grid: {
-    columns: number;
-    gap?: number;
-    rowHeight?: "auto" | number;
-  };
-}) {
+export function getGridStyles(area: Pick<WidgetAreaDefinition, "grid">) {

You’ll need to import the type:

-import { DEFAULT_WIDGET_AREA_DEFINITION } from "../types/widget";
+import { DEFAULT_WIDGET_AREA_DEFINITION } from "../types/widget";
+import type { WidgetAreaDefinition } from "../types/widget";

3-7: Guard against invalid row heights

Clamp numeric rowHeight to a sane minimum to avoid invalid CSS from negative or zero values.

 export function getGridAutoRows(
   rowHeight: "auto" | number = DEFAULT_WIDGET_AREA_DEFINITION.rowHeight,
 ): string {
-  return rowHeight === "auto" ? "auto" : `${rowHeight}px`;
+  if (rowHeight === "auto") return "auto";
+  const safe = Math.max(1, Math.floor(rowHeight));
+  return `${safe}px`;
 }

9-13: Clamp gap to non-negative

Prevent negative gaps producing invalid CSS.

 export function getGridGap(
   gap: number = DEFAULT_WIDGET_AREA_DEFINITION.gap,
 ): string {
-  return `${gap}px`;
+  const safe = Math.max(0, Math.floor(gap));
+  return `${safe}px`;
 }

29-31: Validate columns ≥ 1

Ensure at least one column to avoid repeat(0, …) which is invalid.

 export function getGridTemplateColumns(columns: number): string {
-  return `repeat(${columns}, minmax(0, 1fr))`;
+  const safe = Math.max(1, Math.floor(columns));
+  return `repeat(${safe}, minmax(0, 1fr))`;
 }
libs/portal-framework-core/src/types/widget.ts (1)

18-24: Optional: tighten default grid constants.

Marking DEFAULT_WIDGET_AREA_DEFINITION as const preserves literal types and prevents accidental mutation.

Apply this diff:

 export const DEFAULT_WIDGET_AREA_DEFINITION: Pick<
   WidgetAreaDefinition["grid"],
   "gap" | "rowHeight"
-> = {
+> = {
   gap: 16,
   rowHeight: 50,
-};
+} as const;
libs/portal-framework-core/src/components/WidgetArea.tsx (1)

20-22: Empty-state and gap handling are inconsistent; rely on getGridStyles(area).

  • Empty-state div isn’t a grid and ignores computed gap/columns.
  • className includes gap-4 while inline style already sets gap; the inline style wins but the class is redundant.

Apply this diff:

-  if (!widgets.length) {
-    return <div className="gap-4"></div>;
-  }
+  if (!widgets.length) {
+    return <div className="grid" style={getGridStyles(area)} />;
+  }
@@
-    <div className="grid gap-4" style={getGridStyles(area)}>
+    <div className="grid" style={getGridStyles(area)}>

Also applies to: 25-25

libs/portal-framework-core/src/api/framework.ts (2)

287-289: Optional: prevent silent overrides of widget areas.

Currently, duplicate area IDs overwrite existing entries. Consider guarding or warning.

Apply this diff:

   registerWidgetArea(area: WidgetAreaDefinition): void {
-    this.widgetAreas.set(area.id, area);
+    if (this.widgetAreas.has(area.id)) {
+      console.warn(`Overwriting existing widget area: ${area.id}`);
+    }
+    this.widgetAreas.set(area.id, area);
   }

139-161: Autoregistration flow looks good. Minor request: validate area/widget IDs.

If IDs are user-provided, a quick validate (non-empty, normalized) can catch mistakes early. Not blocking.

Happy to add lightweight validators if you want.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 779109e and 7daf9ca.

📒 Files selected for processing (41)
  • libs/portal-framework-core/src/api/builder.ts (3 hunks)
  • libs/portal-framework-core/src/api/framework.spec.ts (4 hunks)
  • libs/portal-framework-core/src/api/framework.ts (8 hunks)
  • libs/portal-framework-core/src/components/ErrorDisplay.stories.tsx (2 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundary.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.spec.tsx (2 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteLoading.spec.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteLoading.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.spec.tsx (1 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.tsx (1 hunks)
  • libs/portal-framework-core/src/contexts/framework.tsx (3 hunks)
  • libs/portal-framework-core/src/env.ts (1 hunks)
  • libs/portal-framework-core/src/index.ts (1 hunks)
  • libs/portal-framework-core/src/plugins/context-bridge.tsx (3 hunks)
  • libs/portal-framework-core/src/plugins/manager.spec.ts (1 hunks)
  • libs/portal-framework-core/src/plugins/manager.ts (3 hunks)
  • libs/portal-framework-core/src/plugins/remoteComponentLoader.spec.tsx (1 hunks)
  • libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (5 hunks)
  • libs/portal-framework-core/src/types/api.ts (1 hunks)
  • libs/portal-framework-core/src/types/capabilities.ts (1 hunks)
  • libs/portal-framework-core/src/types/navigation.ts (3 hunks)
  • libs/portal-framework-core/src/types/plugin.ts (2 hunks)
  • libs/portal-framework-core/src/types/portal.ts (1 hunks)
  • libs/portal-framework-core/src/types/widget.ts (1 hunks)
  • libs/portal-framework-core/src/util/dependencyGraph.spec.ts (4 hunks)
  • libs/portal-framework-core/src/util/dependencyGraph.ts (3 hunks)
  • libs/portal-framework-core/src/util/domain.spec.ts (1 hunks)
  • libs/portal-framework-core/src/util/framework-initializer.ts (1 hunks)
  • libs/portal-framework-core/src/util/namespace.spec.ts (2 hunks)
  • libs/portal-framework-core/src/util/portalMeta.spec.ts (5 hunks)
  • libs/portal-framework-core/src/util/portalMeta.ts (2 hunks)
  • libs/portal-framework-core/src/utils/grid.ts (1 hunks)
  • libs/portal-framework-core/src/vite/index.ts (1 hunks)
  • libs/portal-plugin-dashboard/src/index.ts (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/components/Card.tsx (2 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/account.profile.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/account.security.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/dashboard.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/widgetRegistrations.ts (1 hunks)
🔇 Additional comments (44)
libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.spec.tsx (1)

1-2: Import reordering standardization — LGTM

Purely stylistic and consistent with the PR’s stated goal; no behavioral impact.

libs/portal-framework-core/src/util/namespace.spec.ts (2)

2-2: No-op formatting change: OK to keep.

The blank line after the vitest import is harmless and improves readability.


38-41: No callers rely on property enumeration order of parseNamespacedId

I searched the entire codebase for any uses of Object.keys or Object.entries on parseNamespacedId results and found none. Since toEqual ignores property order, flipping the key order in your test is purely cosmetic.

• If you do want to enforce a canonical enumeration order for documentation or serialization, feel free to add the optional Object.keys(…) assertion from the original suggestion.
• Otherwise, no changes are strictly necessary—the test already guarantees the correct values.

libs/portal-framework-core/src/plugins/remoteComponentLoader.spec.tsx (1)

9-9: Formatting-only change — no functional impact.

Blank line after vitest import is fine and keeps sections visually separated.

libs/portal-framework-core/src/components/RouteErrorBoundary.stories.tsx (1)

8-8: Storybook meta reordering looks good.

Moving title after tags is consistent with the repo’s formatting convention; no behavior change.

libs/portal-framework-core/src/components/RouteLoading.stories.tsx (1)

1-9: Quote normalization and meta ordering — LGTM.

Switching to double quotes and placing title after tags is consistent and non-functional.

libs/portal-framework-core/src/components/RouteLoading.spec.tsx (1)

3-3: Formatting-only whitespace — LGTM.

libs/portal-framework-core/src/types/capabilities.ts (1)

22-23: Type alias reordering is safe; API unchanged.

Referencing CapabilityStatus from BaseCapability before its declaration is valid in TypeScript. No behavioral change.

libs/portal-framework-core/src/types/api.ts (1)

16-16: LGTM: Type reordering only

Reordering of the union literals and property placement is non-functional. No issues.

Also applies to: 23-23

libs/portal-framework-core/src/env.ts (1)

6-9: LGTM: Key reordering only

Re-ordering client env keys is a no-op for behavior and typing. Nothing to change.

libs/portal-framework-core/src/util/domain.spec.ts (2)

9-10: LGTM: Mock formatting adjustments

Mocked getCurrentLocation shape remains the same; only style changes. All good.


16-17: LGTM: Env mock ordering

Reordered/trimmed mock aligns with env changes; no functional impact.

libs/portal-framework-core/src/plugins/manager.ts (2)

110-112: LGTM: Method placement

getFeatureState repositioning is organizational; no behavior change.


321-323: LGTM: Promise formatting

Multiline initialize(...).then(() => feature) is stylistic; semantics unchanged.

libs/portal-framework-core/src/components/ErrorDisplay.stories.tsx (1)

1-9: LGTM: stylistic cleanup and CSF metadata order

Quote normalization and moving title under tags are fine; Storybook doesn’t rely on property order. No functional changes observed.

libs/portal-framework-core/src/util/portalMeta.spec.ts (5)

5-9: Nice: explicit cache clearing ensures test isolation

Importing and invoking __test_clearCache avoids cross-test bleed from memoization. Good discipline.


15-33: Mock shape aligns with current types and behaviors

Adding other-plugin and keeping version within meta matches PortalMetaPlugin.meta usage. Test data reads clean and representative.


128-135: LGTM: dot-path retrieval for nested values

Covers the happy path for nested lookups via dot notation.


138-144: LGTM: invalid nested path returns undefined

Good negative test to guard against exceptions when traversing.


151-164: LGTM: generic type safety exercised for full and nested retrievals

Validates both object-shape inference and primitive typed retrievals.

libs/portal-framework-core/src/plugins/context-bridge.tsx (2)

112-114: LGTM: condensed debug log improves readability without behavior change

The single-line template literal is a nice cleanup.


168-170: LGTM: consistent debug style in remote bridge

Consistent logging across host/remote makes tracing easier.

libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.stories.tsx (1)

27-35: Confirm RouterError shape matches what the component expects

You’re passing an object with { status, statusText, data:{message} }. Ensure RouteErrorBoundaryFallback handles both Error instances and router-style error responses (e.g., from React Router) so stories reflect real-world inputs.

libs/portal-framework-core/src/plugins/manager.spec.ts (1)

142-151: LGTM – stylistic reflow only

Reformatting of pluginWithFeatureDep instantiation and field order is non-functional. Tests read clearer now.

libs/portal-framework-core/src/util/dependencyGraph.spec.ts (1)

2-2: Whitespace-only cleanup looks good

Readability improved; no behavioral changes. Tests remain clear and deterministic.

Also applies to: 8-8, 12-12, 21-21, 31-31, 41-41

libs/portal-framework-core/src/components/WidgetArea.spec.tsx (1)

38-40: Align mock shape of useFramework with actual context API

Confirm whether getAppName belongs to the hook’s return object (FrameworkContextValue) or to the framework instance itself. Right now it’s top-level. If production moved it onto framework, tests will silently diverge.

  • If getAppName is on framework, move it into mockFramework and remove the top-level property:
-    vi.mocked(useFramework).mockReturnValue({
-      error: null,
-      framework: mockFramework as unknown as Framework,
-      getAppName: () => "test-app",
-      isLoading: false,
-      reinitialize: vi.fn(),
-    });
+    // Add to mockFramework outside this block:
+    // (mockFramework as any).getAppName = vi.fn(() => "test-app");
+    vi.mocked(useFramework).mockReturnValue({
+      error: null,
+      framework: mockFramework as unknown as Framework,
+      isLoading: false,
+      reinitialize: vi.fn(),
+    });
  • If getAppName is top-level by design, keep as-is but tighten types to guard future API drift by typing against the actual FrameworkContextValue.

Would you like me to update the test to reference the new widget retrieval API (getWidgetsForArea/area definitions) as used by the refactored WidgetArea? Happy to draft that once you confirm the intended context shape.

libs/portal-framework-core/src/api/builder.ts (3)

7-7: Import reorder is fine

No functional impact.


22-22: Minor typing polish looks good

Using (() => Promise<void>)[] is clearer and idiomatic.


60-60: Replace non-null assertion with explicit checks in builder.ts

File: libs/portal-framework-core/src/api/builder.ts
Lines: 58–62

The current code uses a non-null assertion on the result of loadRemote, which can lead to a confusing runtime error if the remote module isn’t found or is misconfigured. Since we only have this one call site with ! and remotes are always initialized via init/registerRemotes in framework-initializer.ts before builder.registerRemoteModule runs, it’s safe to remove the non-null assertion and add explicit validation:

-       const mod = (await loadRemote(moduleId))!;
+       const mod = await loadRemote<PluginModule>(moduleId);
+       if (!mod || typeof mod !== "object") {
+         throw new Error(
+           `Remote module not resolved: ${moduleId} (remoteEntry: ${remoteEntry})`
+         );
+       }
+       if (typeof mod.default !== "function") {
+         throw new Error(
+           `Remote plugin module missing default factory: ${moduleId}`
+         );
+       }
        const { id: pluginId } = getPluginInfo(mod);
  • Ensures mod is defined and shaped like a module before calling getPluginInfo.
  • Guarantees mod.default is a function factory.

No other loadRemote(...)! usages were found in the codebase—this single change centralizes null-checking and improves type safety.

libs/portal-plugin-dashboard/src/ui/routes/account.profile.tsx (1)

11-11: No action needed: “core:dashboard:profile” is defined and registered
The widget area for “core:dashboard:profile” is declared in widgetAreas in widgetRegistrations.ts (lines 21, 57, 68) and that file’s default export ({ areas, widgets }) is imported as dashboardWidgets in index.ts and passed to the Plugin’s widgets field, ensuring it’s registered at startup.

libs/portal-framework-core/src/util/framework-initializer.ts (1)

136-144: Simplified shouldInitialize reads well and matches the intent

Returning !framework?.isInitialized() correctly handles undefined/null and initialized states.

libs/portal-framework-core/src/vite/index.ts (1)

1-1: Export reorder is harmless; surface remains stable

No runtime impact. Keeps named exports consistent with the plugin module.

libs/portal-framework-core/src/types/navigation.ts (1)

25-28: Icon prop ordering change is benign

Reordering optional fields in a props type is non-breaking. Looks good.

libs/portal-framework-core/src/util/dependencyGraph.ts (1)

30-70: Topo sort traversal and error messaging look good

The post-order DFS ensures dependencies precede dependents; the clearer circular-dependency error is a nice improvement.

libs/portal-framework-core/src/api/framework.spec.ts (1)

54-70: LGTM: Good coverage of auto-registration path

The mock plugin shape (widgets.areas + widgets.widgets) and the verification of both area and widget registration exercise the new initialization pathway well.

libs/portal-plugin-dashboard/src/widgetRegistrations.ts (2)

102-105: Export shape aligns with new plugin API

Default export { areas, widgets } matches the new plugin.widgets shape consumed by Framework.initialize. Good consistency.


33-100: I’ve added scripts to:

  • Show the import statements in widgetRegistrations.ts (lines 1–50) to locate where WidgetRegistration comes from.
  • Search the entire repo (TS/TSX files) for occurrences of WidgetRegistration and its definition.

Once the output is available, we can confirm whether position.location and order exist on the type and proceed accordingly.

libs/portal-framework-core/src/components/WidgetArea.stories.tsx (2)

13-18: LGTM: Remote component factory shape

Returning a function that resolves to a component matches the remote-component pattern used in core.


20-29: Ignore deprecated API migration suggestion: new getters not present

I searched the portal-framework-core source and found no definitions for getWidgetArea or getWidgetsForArea on the Framework interface—only the existing getWidgetRegistrations method is implemented. Updating the stories and mocks to use non-existent APIs would break the build.

• The Framework interface in libs/portal-framework-core does not declare getWidgetArea or getWidgetsForArea.
• All usages in WidgetArea.stories.tsx, withFramework.tsx mocks, and tests rely on getWidgetRegistrations.
• Until the core library officially adds and exports these area-based getters, the current stories and mocks should continue using getWidgetRegistrations.

If/when the core API is extended with getWidgetArea and getWidgetsForArea, we can revisit migrating the stories and mocks.

Likely an incorrect or invalid review comment.

libs/portal-framework-core/src/utils/grid.ts (1)

22-26: LGTM: Centralized grid styles helper

The consolidated getGridStyles exposes a clean API for components and enforces consistent defaults.

libs/portal-framework-core/src/components/WidgetArea.tsx (1)

26-34: Avoid double-sorting widgets; manual verification required

I wasn’t able to automatically confirm that all client-side sorts on the widgets array have been removed. After fixing the central sort in Framework.getWidgetsForArea, remove the inline sort in WidgetArea.tsx:

-      {widgets
-        .sort((a, b) => {
-          // Sort by order if specified, then by column if specified
-          const orderA = a.order ?? Number.MAX_SAFE_INTEGER;
-          const orderB = b.order ?? Number.MAX_SAFE_INTEGER;
-          const colA = a.position.location?.column ?? Number.MAX_SAFE_INTEGER;
-          const colB = b.position.location?.column ?? Number.MAX_SAFE_INTEGER;
-          return orderA - orderB || colA - colB;
-        })
-        .map((widget) => {
+      {widgets.map((widget) => {

Please manually verify across all components that no other .sort(...) calls remain on the widgets array or directly after getWidgetsForArea(...) before merging.

libs/portal-framework-core/src/index.ts (2)

62-62: LGTM: domain utilities exposure.

Making util/domain public is consistent with other utility exports.


57-61: Export PluginWidgets for plugin authors.

Plugins need the PluginWidgets type to correctly type their widget configurations. It’s defined in libs/portal-framework-core/src/types/widget.ts but isn’t exposed from the package root.

• File: libs/portal-framework-core/src/index.ts
• Location: around lines 57–61

Apply this diff:

 export type {
   WidgetAreaDefinition,
   WidgetDefinition,
   WidgetRegistration,
+  PluginWidgets,
 } from "./types/widget";

Before merging, please verify that no consumers are importing directly from the deep path …/types/widget. For example, run:

rg -n --type ts --type tsx -P "from\s+['\"][^'\"]*/types/widget['\"]" -C2 .

and confirm there are no matches.

libs/portal-framework-core/src/api/framework.ts (1)

319-338: Portal meta fallback is robust.

Good defensive handling with env and current location fallback; clear logs.

@pcfreak30 pcfreak30 force-pushed the libs/portal-plugin-dashboard branch from 7daf9ca to 5759699 Compare August 22, 2025 01:10
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libs/portal-framework-core/src/util/dependencyGraph.ts (1)

22-28: Handle missing nodes in getDependencies/getDependents

The current implementations of getDependencies and getDependents will throw a TypeError when called with a node that hasn’t been added to the graph (because this.#dependencies.get(node) or this.#reverseLookup.get(node) returns undefined, and new Set(undefined) is invalid). We need to guard against this by providing a safe default (or throwing a clearer error).

— Proposed change (default to empty Set):

   getDependencies(node: T): Set<T> {
-    return new Set(this.#dependencies.get(node));
+    const deps = this.#dependencies.get(node);
+    return new Set(deps ?? []);
   }

   getDependents(node: T): Set<T> {
-    return new Set(this.#reverseLookup.get(node));
+    const rdeps = this.#reverseLookup.get(node);
+    return new Set(rdeps ?? []);
   }

If you’d rather enforce strictness, replace the ?? [] guard with an explicit error throw (e.g.:

if (!this.#dependencies.has(node)) {
  throw new Error(`Node ${String(node)} is not in the graph`);
}

). Please confirm which behavior callers expect so tests can be updated accordingly.

♻️ Duplicate comments (1)
libs/portal-framework-core/src/types/plugin.ts (1)

31-32: Missing import for PluginWidgets causes type errors

Plugin.widgets references PluginWidgets but there’s no import.

Apply this diff near the top-level imports:

 import { RouteDefinition } from "./navigation";
+import type { PluginWidgets } from "./widget";
🧹 Nitpick comments (15)
libs/portal-framework-core/src/util/dependencyGraph.ts (3)

6-12: Guard against self-dependencies at insertion time.

Adding a self-edge immediately creates a cycle; fail fast in addDependency to surface a clearer error and avoid doing work you’ll just reject in topologicalSort.

Apply this diff:

   addDependency(from: T, to: T): void {
     this.addNode(from);
     this.addNode(to);
 
+    if (from === to) {
+      throw new Error(`Self-dependency is not allowed: ${String(from)}`);
+    }
+
     this.#dependencies.get(from)!.add(to);
     this.#reverseLookup.get(to)!.add(from);
   }

35-40: Improve cycle error to include the full path of the cycle.

The current message names only the node; including the traversal path drastically speeds up debugging.

Minimal change, preserving your DFS:

-    const visit = (node: T) => {
+    const visit = (node: T, stack: T[] = []) => {
       if (temp.has(node)) {
-        throw new Error(
-          `Circular dependency detected involving ${String(node)}`,
-        );
+        const cycleStart = stack.indexOf(node);
+        const cyclePath =
+          cycleStart >= 0 ? [...stack.slice(cycleStart), node] : [...stack, node];
+        throw new Error(
+          `Circular dependency detected: ${cyclePath.map(String).join(' -> ')}`,
+        );
       }
 
       if (!visited.has(node)) {
         temp.add(node);
 
-        for (const dep of this.#dependencies.get(node)!) {
-          visit(dep);
+        for (const dep of this.#dependencies.get(node)!) {
+          visit(dep, [...stack, node]);
         }

Call sites (Lines 60-67) remain valid because stack has a default value.

Also applies to: 45-47


55-58: Confirm intent: starting DFS from “no dependents” (sinks) rather than “no dependencies” (sources).

This still yields a valid topo order (dependencies come before dependents due to post-order push), but it’s atypical and can surprise readers. If you want the conventional approach, seed with nodes that have no dependencies.

Alternative seeding:

-    // Start with nodes that have no dependents
-    const rootNodes = Array.from(this.#nodes).filter(
-      (node) => this.#reverseLookup.get(node)!.size === 0,
-    );
+    // Start with nodes that have no dependencies (sources)
+    const rootNodes = Array.from(this.#nodes).filter(
+      (node) => this.#dependencies.get(node)!.size === 0,
+    );

If the current choice is deliberate (e.g., because callers expect sinks to be visited first for presentation), consider renaming rootNodes to sourceNodes/sinkNodes or adding a brief doc comment to encode the intent.

libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (1)

189-189: Unnecessary type casting with improved typing

The type casting as React.ComponentType<FallbackProps> is no longer needed since info.fallback is already properly typed as React.ComponentType<FallbackProps>.

Apply this diff to remove the unnecessary casting:

-        FallbackComponent={info.fallback as React.ComponentType<FallbackProps>}>
+        FallbackComponent={info.fallback}>
libs/portal-plugin-dashboard/src/widgetRegistrations.ts (3)

6-31: DRY up repeated grid config.

The identical 12-column grid with same gap/rowHeight appears in all areas. Factor it into a shared constant to reduce duplication and prevent drift.

Apply:

@@
-import type {
-  WidgetAreaDefinition,
-  WidgetRegistration,
-} from "@lumeweb/portal-framework-core";
+import type {
+  WidgetAreaDefinition,
+  WidgetRegistration,
+} from "@lumeweb/portal-framework-core";
+
+const COMMON_GRID: WidgetAreaDefinition["grid"] = {
+  columns: 12,
+  gap: 16,
+  rowHeight: "auto",
+};
@@
-export const widgetAreas: WidgetAreaDefinition[] = [
-  {
-    grid: {
-      columns: 12,
-      gap: 16,
-      rowHeight: "auto",
-    },
-    id: "core:dashboard:header",
-  },
-  {
-    grid: {
-      columns: 12,
-      gap: 16,
-      rowHeight: "auto",
-    },
-    id: "core:dashboard:profile",
-  },
-  {
-    grid: {
-      columns: 12,
-      gap: 16,
-      rowHeight: "auto",
-    },
-    id: "core:dashboard:security",
-  },
-];
+export const widgetAreas: WidgetAreaDefinition[] = [
+  { grid: COMMON_GRID, id: "core:dashboard:header" },
+  { grid: COMMON_GRID, id: "core:dashboard:profile" },
+  { grid: COMMON_GRID, id: "core:dashboard:security" },
+];

59-59: Avoid ID collision with area ID core:dashboard:profile.

This widget’s id equals the area’s id. Even if areas and widgets are keyed separately, overlapping IDs are confusing in logs and tooling. Rename the widget id to be distinct.

Apply:

-    id: "core:dashboard:profile",
+    id: "core:dashboard:profile-card",

33-100: Add explicit ordering/columns for deterministic layout.

The PR mentions sorting by order and column position. None of these registrations define order, and most omit position.location.column. Adding them will make the grid stable and intention-revealing.

Example adjustments:

@@
   {
     areaId: "core:dashboard:header",
     componentName: "widgets/account/emailVerificationBanner",
     id: "core:dashboard:email-verification",
+    order: 0,
     position: {
       size: {
         height: 1,
         width: 12,
       },
     },
   },
@@
   {
     areaId: "core:dashboard:profile",
     componentName: "widgets/account/bio",
     id: "core:dashboard:bio",
+    order: 11,
     position: {
       size: {
         height: 1,
         width: 4,
       },
+      location: { column: 9 },
     },
   },
@@
   {
     areaId: "core:dashboard:profile",
     componentName: "widgets/account/profile",
-    id: "core:dashboard:profile-card",
+    id: "core:dashboard:profile-card",
+    order: 10,
     position: {
       size: {
         height: 2,
         width: 8,
       },
+      location: { column: 1 },
     },
   },
@@
   {
     areaId: "core:dashboard:profile",
     componentName: "widgets/account/delete",
     id: "core:dashboard:delete",
+    order: 12,
     position: {
       size: {
         height: 1,
         width: 4,
       },
+      location: { column: 9 },
     },
   },
@@
   {
     areaId: "core:dashboard:security",
     componentName: "widgets/account/password",
     id: "core:dashboard:password",
+    order: 10,
     position: {
       size: {
         height: 2,
         width: 6,
       },
+      location: { column: 1 },
     },
   },
@@
   {
     areaId: "core:dashboard:security",
     componentName: "widgets/account/2fa",
     id: "core:dashboard:2fa",
+    order: 11,
     position: {
       size: {
         height: 2,
         width: 6,
       },
+      location: { column: 7 },
     },
   },

If the grid auto-placement already guarantees this, feel free to skip; otherwise these hints lock in the layout.

libs/portal-framework-core/src/components/WidgetArea.stories.tsx (2)

9-9: Remove unused top-level MockWidget (shadowed).

A different MockWidget is defined inside _createRemoteComponent. Drop the top-level one to avoid shadowing.

Apply:

-const MockWidget = () => <div className="p-4 border rounded">Mock Widget</div>;

13-19: Remote component factory ignores identifiers; acceptable for a stub.

For Storybook it’s fine to always return the same mock. If you want parity with production, accept (pluginId, componentName) and log them to surface mismatches.

Example:

-  _createRemoteComponent: () => {
+  _createRemoteComponent: () => {
     const MockWidget = () => (
       <div className="p-4 border rounded">Mock Widget</div>
     );
-    return () => MockWidget;
+    return (_pluginId?: string, _componentName?: string) => MockWidget;
   },
libs/portal-framework-core/src/types/navigation.ts (1)

25-28: Prefer React.ComponentType over React.FC for icon type.

Icons don’t typically use children; ComponentType is a bit more flexible and avoids the FC implicit children/type semantics.

Apply:

-export interface NavigationItemIconProps {
+export interface NavigationItemIconProps {
   className?: string;
   size?: number | string;
 }
@@
-  icon?: React.FC<NavigationItemIconProps>;
+  icon?: React.ComponentType<NavigationItemIconProps>;
libs/portal-framework-core/src/components/WidgetArea.tsx (2)

25-25: Avoid conflicting gap sources; rely on getGridStyles(area) only

Inline style from getGridStyles(area) sets gap; keeping Tailwind’s gap-4 is redundant and potentially confusing.

-    <div className="grid gap-4" style={getGridStyles(area)}>
+    <div className="grid" style={getGridStyles(area)}>

37-40: Prefer nullish coalescing for column start; ensure width/height are defined

Truthiness checks can misread 0; nullish checks convey intent better. Also ensure size.width/height are always ≥1 to prevent invalid CSS.

-          const columnStart = widget.position.location?.column 
-            ? `${widget.position.location.column} / span ${widget.position.size.width}`
-            : `auto / span ${widget.position.size.width}`;
+          const columnStart = `${
+            widget.position.location?.column ?? "auto"
+          } / span ${widget.position.size.width}`;

Confirm WidgetDefinition guarantees position.size.width/height >= 1. If not, default to 1:

-                gridRow: `span ${widget.position.size.height}`,
+                gridRow: `span ${Math.max(1, widget.position.size.height ?? 1)}`,

Also applies to: 46-47

libs/portal-framework-core/src/types/plugin.ts (2)

3-3: Use a relative import for Framework to avoid path alias fragility

Using an absolute path here can break TS path mapping and cause duplicate module instances. Prefer relative like neighbors below.

-import { Framework } from "libs/portal-framework-core/src/api/framework";
+import { Framework } from "../api/framework";

15-16: Nit: unify status type aliases or export both from a common module

FeatureStateStatus and PluginLoadStatus share the same union. Consider consolidating to a single exported Status type to reduce duplication.

If keeping both for domain clarity, add a brief comment to explain the distinction.

libs/portal-framework-core/src/index.ts (1)

57-61: Re-export PluginWidgets for better plugin author DX

Plugins will often need the PluginWidgets shape. Consider re-exporting it alongside WidgetAreaDefinition, WidgetDefinition, WidgetRegistration to avoid deep imports.

 export type {
   WidgetAreaDefinition,
   WidgetDefinition,
   WidgetRegistration,
+  PluginWidgets,
 } from "./types/widget";
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7daf9ca and 5759699.

📒 Files selected for processing (41)
  • libs/portal-framework-core/src/api/builder.ts (3 hunks)
  • libs/portal-framework-core/src/api/framework.spec.ts (4 hunks)
  • libs/portal-framework-core/src/api/framework.ts (8 hunks)
  • libs/portal-framework-core/src/components/ErrorDisplay.stories.tsx (2 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundary.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.spec.tsx (2 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteLoading.spec.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteLoading.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.spec.tsx (1 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.tsx (1 hunks)
  • libs/portal-framework-core/src/contexts/framework.tsx (3 hunks)
  • libs/portal-framework-core/src/env.ts (1 hunks)
  • libs/portal-framework-core/src/index.ts (1 hunks)
  • libs/portal-framework-core/src/plugins/context-bridge.tsx (3 hunks)
  • libs/portal-framework-core/src/plugins/manager.spec.ts (1 hunks)
  • libs/portal-framework-core/src/plugins/manager.ts (3 hunks)
  • libs/portal-framework-core/src/plugins/remoteComponentLoader.spec.tsx (1 hunks)
  • libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (5 hunks)
  • libs/portal-framework-core/src/types/api.ts (1 hunks)
  • libs/portal-framework-core/src/types/capabilities.ts (1 hunks)
  • libs/portal-framework-core/src/types/navigation.ts (3 hunks)
  • libs/portal-framework-core/src/types/plugin.ts (2 hunks)
  • libs/portal-framework-core/src/types/portal.ts (1 hunks)
  • libs/portal-framework-core/src/types/widget.ts (1 hunks)
  • libs/portal-framework-core/src/util/dependencyGraph.spec.ts (4 hunks)
  • libs/portal-framework-core/src/util/dependencyGraph.ts (3 hunks)
  • libs/portal-framework-core/src/util/domain.spec.ts (1 hunks)
  • libs/portal-framework-core/src/util/framework-initializer.ts (1 hunks)
  • libs/portal-framework-core/src/util/namespace.spec.ts (2 hunks)
  • libs/portal-framework-core/src/util/portalMeta.spec.ts (5 hunks)
  • libs/portal-framework-core/src/util/portalMeta.ts (2 hunks)
  • libs/portal-framework-core/src/utils/grid.ts (1 hunks)
  • libs/portal-framework-core/src/vite/index.ts (1 hunks)
  • libs/portal-plugin-dashboard/src/index.ts (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/components/Card.tsx (2 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/account.profile.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/account.security.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/dashboard.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/widgetRegistrations.ts (1 hunks)
✅ Files skipped from review due to trivial changes (4)
  • libs/portal-framework-core/src/env.ts
  • libs/portal-framework-core/src/components/RouteErrorBoundary.stories.tsx
  • libs/portal-framework-core/src/util/namespace.spec.ts
  • libs/portal-framework-core/src/util/domain.spec.ts
🚧 Files skipped from review as they are similar to previous changes (27)
  • libs/portal-framework-core/src/util/framework-initializer.ts
  • libs/portal-framework-core/src/plugins/remoteComponentLoader.spec.tsx
  • libs/portal-plugin-dashboard/src/ui/routes/account.security.tsx
  • libs/portal-framework-core/src/vite/index.ts
  • libs/portal-plugin-dashboard/src/ui/routes/dashboard.tsx
  • libs/portal-framework-core/src/util/dependencyGraph.spec.ts
  • libs/portal-framework-core/src/contexts/framework.tsx
  • libs/portal-framework-core/src/types/widget.ts
  • libs/portal-framework-core/src/types/portal.ts
  • libs/portal-framework-core/src/components/WidgetArea.spec.tsx
  • libs/portal-framework-core/src/utils/grid.ts
  • libs/portal-framework-core/src/components/RouteLoading.spec.tsx
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.spec.tsx
  • libs/portal-framework-core/src/components/ErrorDisplay.stories.tsx
  • libs/portal-plugin-dashboard/src/ui/components/Card.tsx
  • libs/portal-framework-core/src/plugins/manager.ts
  • libs/portal-framework-core/src/api/framework.spec.ts
  • libs/portal-framework-core/src/plugins/context-bridge.tsx
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.stories.tsx
  • libs/portal-framework-core/src/util/portalMeta.spec.ts
  • libs/portal-framework-core/src/components/RouteLoading.stories.tsx
  • libs/portal-plugin-dashboard/src/ui/routes/account.profile.tsx
  • libs/portal-framework-core/src/util/portalMeta.ts
  • libs/portal-framework-core/src/api/framework.ts
  • libs/portal-framework-core/src/types/capabilities.ts
  • libs/portal-framework-core/src/plugins/manager.spec.ts
  • libs/portal-framework-core/src/api/builder.ts
🧰 Additional context used
🧬 Code graph analysis (7)
libs/portal-framework-core/src/types/api.ts (1)
libs/portal-framework-core/src/types/plugin.ts (1)
  • NamespacedId (18-18)
libs/portal-plugin-dashboard/src/widgetRegistrations.ts (1)
libs/portal-framework-core/src/types/widget.ts (2)
  • WidgetAreaDefinition (1-16)
  • WidgetRegistration (49-65)
libs/portal-framework-core/src/components/WidgetArea.tsx (1)
libs/portal-framework-core/src/utils/grid.ts (1)
  • getGridStyles (15-27)
libs/portal-plugin-dashboard/src/index.ts (1)
libs/portal-framework-core/src/types/plugin.ts (1)
  • Plugin (20-32)
libs/portal-framework-core/src/types/navigation.ts (1)
libs/portal-framework-core/src/types/plugin.ts (1)
  • NamespacedId (18-18)
libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (1)
libs/portal-framework-core/src/plugins/manager.ts (1)
  • RemoteModule (16-20)
libs/portal-framework-core/src/types/plugin.ts (1)
libs/portal-framework-core/src/types/widget.ts (1)
  • PluginWidgets (26-29)
🔇 Additional comments (22)
libs/portal-framework-core/src/util/dependencyGraph.ts (1)

1-1: LGTM: generic constraint tweak and field reordering are fine.

No functional concerns; constraint remains correct for Map/Set keys and the private field order has no behavioral impact.

Also applies to: 3-3

libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (8)

13-13: LGTM! - Proper import for error boundary types

Good addition of FallbackProps import to properly type the fallback components for error boundaries.


21-21: LGTM! - Improved typing for destroy method

The change from Promise<void> to void for the destroy method signature is correct and aligns with synchronous cleanup operations.


48-49: LGTM! - Better union type ordering

The reordered union type correctly prioritizes the bridge loader function over the React component type, improving type inference.


68-68: LGTM! - Consistent component extraction logic

The fallback to module when module.default is undefined ensures compatibility with both CommonJS and ES module exports.


113-122: Addresses previous review feedback correctly

The RemoteComponentParams interface now properly uses FallbackProps for the fallback type instead of the custom { error: Error } type, which resolves the type mismatch with ErrorBoundary.


124-129: LGTM! - Consistent fallback typing

The RemoteComponentProps interface now correctly uses FallbackProps for the fallback prop, maintaining consistency with the ErrorBoundary requirements.


131-133: LGTM! - Clear interface extension

The RenderFnParams interface appropriately extends ProviderParams with the dom property, providing clear typing for render function parameters.


137-145: Ignore duplicate RemoteModule interface names—no actual type conflict

The interface RemoteModule in remoteComponentLoader.tsx (line 137) is declared without export inside a TSX module and thus is scoped locally to that file. The export interface RemoteModule in manager.ts (line 16) lives in its own module. Because both files are ES modules, their RemoteModule types do not collide at compile time. You can safely keep both names.

Likely an incorrect or invalid review comment.

libs/portal-framework-core/src/types/api.ts (1)

16-16: Union order change is a no-op for TypeScript; safe to merge.

Reordering literal types in a union doesn’t affect assignability or narrowing. No functional impact.

libs/portal-plugin-dashboard/src/index.ts (2)

17-22: Capabilities update LGTM.

Adding DashRefineConfigCapability alongside RefineConfigCapability and SdkCapability is straightforward and non-breaking within plugin boundaries.


11-11: Default export shape confirmed & no legacy references found

  • The default export in libs/portal-plugin-dashboard/src/widgetRegistrations.ts is exactly
    { areas: widgetAreas, widgets: widgetRegistrations } (lines 102–105), matching the Plugin.widgets signature.
  • A repo-wide search for the identifier widgetRegistrations only surface:
    • the original export const widgetRegistrations: WidgetRegistration[] (lines 33–35)
    • the default export itself (lines 102–105)
    • the single import in index.ts (import dashboardWidgets from "./widgetRegistrations"; on line 11)

No other references to the old widgetRegistrations property remain—everything aligns with the refactor.

libs/portal-plugin-dashboard/src/widgetRegistrations.ts (1)

102-105: Default export shape is clear and aligns with Plugin.widgets.

Exporting { areas, widgets } keeps the plugin wiring ergonomic and typed.

libs/portal-framework-core/src/components/WidgetArea.stories.tsx (1)

20-28: Verify story uses the new widget-area API (getWidgetArea/getWidgetsForArea).

If WidgetArea now resolves data via getWidgetArea and getWidgetsForArea, this mock still exposing getWidgetRegistrations will break the story. Update the mock to the new shape.

Proposed update:

   const mockFramework = {
-  appName: "storybook",
-  getWidgetRegistrations: (areaId: string) => {
-    if (areaId === "dashboard") {
-      return [
-        { componentName: "Widget1", pluginId: "core:widgets" },
-        { componentName: "Widget2", pluginId: "core:widgets" },
-      ];
-    }
-    return [];
-  },
+  appName: "storybook",
+  getWidgetArea: (areaId: string) =>
+    areaId === "dashboard"
+      ? { id: "dashboard", grid: { columns: 12, gap: 16, rowHeight: "auto" as const } }
+      : undefined,
+  getWidgetsForArea: (areaId: string) =>
+    areaId === "dashboard"
+      ? [
+          {
+            id: "storybook:widget1",
+            areaId: "dashboard",
+            componentName: "Widget1",
+            position: { size: { width: 6, height: 1 }, location: { column: 1 } },
+          },
+          {
+            id: "storybook:widget2",
+            areaId: "dashboard",
+            componentName: "Widget2",
+            position: { size: { width: 6, height: 1 }, location: { column: 7 } },
+          },
+        ]
+      : [],
   },

If WidgetArea still relies on getWidgetRegistrations, disregard this change.

libs/portal-framework-core/src/types/navigation.ts (2)

11-23: NavigationItem extensions are sensible; no issues spotted.

New disabled, hidden, icon, and parentId fields are coherent with routing and menu needs.


52-56: RouteDefinition parentId relocation is fine.

Moving parentId near navigation improves cohesion; no type compatibility issues.

libs/portal-framework-core/src/components/WidgetArea.tsx (2)

17-19: Guard against missing widget area before computing styles

If getWidgetArea(id) returns undefined, getGridStyles(area) will throw (expects area.grid). Add a defensive check to fail safely.

   const area = framework.getWidgetArea(id);
   const widgets = framework.getWidgetsForArea(id);
 
-  if (!widgets.length) {
+  if (!area) {
+    return null;
+  }
+  if (!widgets.length) {
     return <div className="grid" style={getGridStyles(area)} />;
   }

If the framework guarantees area always exists for a given id, feel free to skip—otherwise this prevents a runtime crash path.

Also applies to: 25-25


13-15: LGTM: early return when framework is unavailable

Returning null avoids rendering errors before the framework is ready. This aligns with the new initialization flow.

libs/portal-framework-core/src/types/plugin.ts (2)

34-38: LGTM: new public types are clear and minimal

PluginDependency, PluginExports, and PluginLoadStatus look good and keep the public shape tidy.

Also applies to: 45-46


47-49: Sanity check: extending Node’s Module for PluginModule

Extending node:module’s Module for plugin module typing is unconventional. Typically dynamic imports use the ESM namespace. If this was intentional and working, ignore; otherwise we can re-type to match import() results.

Would you like me to propose a safer type for PluginModule that matches your loader’s return shape?

libs/portal-framework-core/src/index.ts (2)

62-62: Confirm intentional public exposure of util/domain

export * from "./util/domain" broadens the public surface. If these helpers are meant to be internal, re-exporting selectively or keeping them private avoids lock-in.

If this is deliberate, consider documenting it in the package README’s public API section.


34-38: LGTM: reorganized exports and type-only exports

Type-only exports reduce runtime bloat and the new widget type exports align with the refactor.

Also applies to: 46-55, 68-71

@pcfreak30 pcfreak30 force-pushed the libs/portal-plugin-dashboard branch from 5759699 to b31ae56 Compare August 22, 2025 01:47
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (1)

178-201: Remove @ts-ignore and pass remote props directly

Once props are inferred from the selected export, you can drop the wrapper props and the @ts-ignore escapes.

-export function createRemoteComponent<
-  T extends { default: React.ComponentType<any> },
-  E extends keyof T = keyof T,
->(info: LazyRemoteComponentInfo<T, E>) {
-  const LazyComponent = createLazyRemoteComponent(info);
-  return forwardRef<HTMLDivElement, RemoteComponentProps>((props, ref) => {
-    // Destructure to separate wrapper props from component props
-    const { props: componentProps } = props;
-
-    return (
-      <ErrorBoundary FallbackComponent={info.fallback}>
-        <React.Suspense fallback={info.loading}>
-          {componentProps !== undefined ? (
-            //@ts-ignore
-            <LazyComponent {...componentProps} />
-          ) : (
-            //@ts-ignore
-            <LazyComponent />
-          )}
-        </React.Suspense>
-      </ErrorBoundary>
-    );
-  });
-}
+export function createRemoteComponent<
+  M,
+  E extends keyof M & string = any
+>(info: LazyRemoteComponentInfo<M, E>) {
+  const LazyComponent = createLazyRemoteComponent(info);
+  return forwardRef<HTMLDivElement, RemoteComponentProps<M, E>>((props, _ref) => {
+    return (
+      <ErrorBoundary FallbackComponent={info.fallback}>
+        <React.Suspense fallback={info.loading}>
+          {/* Props type is inferred from the selected export */}
+          <LazyComponent {...(props as any)} />
+        </React.Suspense>
+      </ErrorBoundary>
+    );
+  });
+}
libs/portal-framework-core/src/util/framework-initializer.ts (1)

121-132: Fix undefined framework in catch block
In libs/portal-framework-core/src/util/framework-initializer.ts (around lines 121–132), the existing catch unconditionally returns framework!, which may be undefined if an error occurs before the await builder.framework assignment. This can lead to downstream runtime crashes.

Suggested patch: within the catch, if builder exists, attempt to load the framework before returning; if loading fails, re-throw the original error.

   } catch (err) {
     // Handle unexpected system-level errors
-    if (!builder) {
-      // If builder creation failed, we can't return a builder
-      throw err; // Re-throw if builder is null
-    }
-    return {
-      builder: builder,
-      errors: [
-        {
-          category: "system",
-          error: err instanceof Error ? err : new Error(String(err)),
-          id: "system-initialization",
-        },
-      ],
-      framework: framework!, // framework might be undefined if error happened before builder.framework
-      success: false,
-    };
+    if (!builder) {
+      // If builder creation failed, re-throw
+      throw err;
+    }
+    // Ensure we return a valid framework instance
+    try {
+      framework = framework ?? (await builder.framework);
+    } catch {
+      // If framework cannot be obtained, propagate original error
+      throw err;
+    }
+    return {
+      builder,
+      errors: [
+        {
+          category: "system",
+          error: err instanceof Error ? err : new Error(String(err)),
+          id: "system-initialization",
+        },
+      ],
+      framework,
+      success: false,
+    };
   }

Alternatively, if non-blocking flow is preferred, relax the InitializationResult type to framework?: Framework and remove the non-null assertion.

libs/portal-framework-core/src/components/WidgetArea.spec.tsx (4)

59-66: Provide minimal valid WidgetDefinition for single widget test

Return a component and size so the grid can render.

Apply:

-  mockFramework.getWidgetRegistrations.mockReturnValue([
-    { componentName: "WidgetA", pluginId: "core:widgets" },
-  ]);
+  mockFramework.getWidgetsForArea.mockImplementation((id: string) =>
+    id === "single-area"
+      ? [
+          {
+            id: "w1",
+            areaId: "single-area",
+            component: () => <div data-testid="mock-widget" />,
+            position: { size: { width: 4, height: 1 } },
+          },
+        ]
+      : [],
+  );

68-77: Provide multiple WidgetDefinition entries in order for multi-widget test

Use distinct IDs and optional columns to exercise ordering and rendering.

Apply:

-  mockFramework.getWidgetRegistrations.mockReturnValue([
-    { componentName: "WidgetA", pluginId: "core:widgets" },
-    { componentName: "WidgetB", pluginId: "core:widgets" },
-    { componentName: "WidgetC", pluginId: "core:widgets" },
-  ]);
+  mockFramework.getWidgetsForArea.mockImplementation((id: string) =>
+    id === "multi-area"
+      ? [
+          {
+            id: "w1",
+            areaId: "multi-area",
+            component: () => <div data-testid="mock-widget" />,
+            order: 1,
+            position: { location: { column: 1 }, size: { width: 4, height: 1 } },
+          },
+          {
+            id: "w2",
+            areaId: "multi-area",
+            component: () => <div data-testid="mock-widget" />,
+            order: 2,
+            position: { location: { column: 5 }, size: { width: 4, height: 1 } },
+          },
+          {
+            id: "w3",
+            areaId: "multi-area",
+            component: () => <div data-testid="mock-widget" />,
+            order: 3,
+            position: { size: { width: 4, height: 1 } },
+          },
+        ]
+      : [],
+  );

1-83: Update remaining getWidgetRegistrations usages in tests and stories

Several files still rely on the legacy getWidgetRegistrations API and need to be updated or removed to align with the new widget-loading patterns:

• libs/portal-storybook-config/src/decorators/withFramework.tsx
– Line 17: mockFramework.getWidgetRegistrations = (areaId: string) => {…}
• libs/portal-framework-core/src/components/WidgetArea.stories.tsx
– Line 20: getWidgetRegistrations: (areaId: string) => {…}
• libs/portal-framework-core/src/testing/MockFrameworkProvider.tsx
– Lines 20, 48, 50: Definitions and calls of getWidgetRegistrations
• libs/portal-framework-core/src/capabilities/manager.spec.ts
– Line 60: getWidgetRegistrations: vi.fn(),
• libs/portal-framework-core/src/components/WidgetArea.spec.tsx
– Multiple mocks and assertions around mockFramework.getWidgetRegistrations

Please refactor or remove these references to use the updated registration API so that all tests and stories remain consistent.


1-83: Update remaining mocks and stories to use the new getWidgetsForArea API

The following files still reference getWidgetRegistrations and need to be updated to call the new getWidgetsForArea method (and drop any legacy widgetAreaId prop, which no longer exists):

  • libs/portal-storybook-config/src/decorators/withFramework.tsx
    Replace
    mockFramework.getWidgetRegistrations = …
    with
    mockFramework.getWidgetsForArea = …

  • libs/portal-framework-core/src/capabilities/manager.spec.ts
    Update the stub in the mock capabilities object:

    // before
    getWidgetRegistrations: vi.fn(),
    // after
    getWidgetsForArea: vi.fn(),
  • libs/portal-framework-core/src/components/WidgetArea.stories.tsx
    Change the story’s mock implementation from getWidgetRegistrations to getWidgetsForArea.

  • libs/portal-framework-core/src/testing/MockFrameworkProvider.tsx
    Rename and implement the getWidgetRegistrations method to getWidgetsForArea, matching the new signature and return type.

No other occurrences of widgetAreaId were found, and the only call to sortWidgets remains inside WidgetArea.tsx (where it’s intended). Implementing these updates will fully retire the legacy API and ensure consistency across stories, specs, and plugins.

♻️ Duplicate comments (6)
libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (3)

68-69: Resolved: safe default extraction for bridge path

Using module.default || module avoids crashing when the remote lacks a default export.


80-83: Resolved: consistent default extraction for no-bridge path

module?.default ?? module mirrors the bridge path and prevents undefined component access.


188-189: Resolved: FallbackComponent now matches react-error-boundary

Passing FallbackComponent={info.fallback} removes the need for unsafe casts.

libs/portal-framework-core/src/components/WidgetArea.tsx (1)

23-27: Empty state keeps grid container (previous feedback addressed)

You now render the grid container and only omit children when there are no widgets, preserving layout. This resolves the earlier empty-state grid loss.

libs/portal-framework-core/src/types/plugin.ts (1)

31-36: Import PluginWidgets to fix type reference

Plugin references PluginWidgets but doesn’t import it, breaking type checking. Add the missing import.

Apply:

 import { RouteDefinition } from "./navigation";
+import type { PluginWidgets } from "./widget";
libs/portal-framework-core/src/api/framework.ts (1)

124-132: Sorting fix looks correct and type-safe.

Using sortWidgets aligns with the declared WidgetDefinition.position.location?.column shape and avoids the previous colStart pitfall.

🧹 Nitpick comments (19)
libs/portal-framework-core/src/plugins/manager.spec.ts (1)

142-151: LGTM, with a tiny consistency nit on async mocks

The restructuring reads well and the assertion is precise. For consistency with other mocks in this file (and to avoid accidental sync behavior if initialization ever gets awaited), consider making initialize/destroy return resolved promises.

-    pluginWithFeatureDep.features = [
+    pluginWithFeatureDep.features = [
       {
         dependencies: [{ id: "test:missing-feature" as NamespacedId }],
-        destroy: vi.fn(),
+        destroy: vi.fn().mockResolvedValue(undefined),
         id: "test:my-feature" as NamespacedId,
-        initialize: vi.fn(),
+        initialize: vi.fn().mockResolvedValue(undefined),
       },
     ];
libs/portal-framework-core/src/util/portalMeta.ts (1)

53-56: Make cache-clear helper synchronous (no need to await a sync clear)

memoizee’s .clear is synchronous; awaiting it is unnecessary (and slightly misleading).

-// Test helper to clear memoization cache
-export async function __test_clearCache() {
-  await _fetchPortalMeta.clear?.();
-}
+// Test helper to clear memoization cache
+export function __test_clearCache() {
+  _fetchPortalMeta.clear?.();
+}

Additionally, to make URL handling consistent with the portalUrl branch, consider ensuring a scheme on baseUrl as well (outside the touched lines):

// After: const baseUrl = getApiBaseUrl({ currentUrl: portalUrl });
const hasScheme = /^https?:\/\//i.test(baseUrl);
fullEndpoint = `${hasScheme ? "" : "https://"}${baseUrl}${endpoint}`;
libs/portal-framework-core/src/util/portalMeta.spec.ts (1)

46-56: Add a test for the no-explicit-portalUrl code path

Right now we only test the explicit portalUrl branch. A quick extra test that stubs getApiBaseUrl would cover the fallback path too.

+    it("falls back to getApiBaseUrl when portalUrl is not provided", async () => {
+      const { getApiBaseUrl } = await vi.importActual<typeof import("../util/getApiBaseUrl")>(
+        "../util/getApiBaseUrl",
+      );
+      vi.mock("../util/getApiBaseUrl", async () => ({
+        ...(await vi.importActual("../util/getApiBaseUrl")),
+        getApiBaseUrl: vi.fn().mockReturnValue("https://fallback.example.com"),
+      }));
+
+      vi.mocked(fetch).mockResolvedValue({
+        json: () => Promise.resolve(mockMeta),
+        ok: true,
+      } as Response);
+
+      const { fetchPortalMeta } = await import("./portalMeta");
+      const result = await fetchPortalMeta();
+      expect(result).toEqual(mockMeta);
+      expect(fetch).toHaveBeenCalledWith("https://fallback.example.com/api/meta");
+
+      // restore original
+      vi.doUnmock("../util/getApiBaseUrl");
+    });
libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (3)

32-37: Align ErrorComponent props type with FallbackProps

Minor type polish: declare ErrorComponent as React.ComponentType<FallbackProps> to keep it aligned with react-error-boundary and your usage below.

-export interface RemoteComponentOptions {
-  ErrorComponent: React.ComponentType<{
-    error: Error;
-    resetErrorBoundary: () => void;
-  }>;
-  LoadingComponent: React.ComponentType;
-}
+export interface RemoteComponentOptions {
+  ErrorComponent: React.ComponentType<FallbackProps>;
+  LoadingComponent: React.ComponentType;
+}

78-92: Support named exports in the no-bridge path

Right now the no-bridge branch always coerces to a default export, so consumers can’t target a named export via export. Pass export through to createRemoteComponent and let it pick the right export.

-  } else {
-    return createRemoteComponent({
-      fallback: ErrorFallback,
-      loader: async (): Promise<{ default: React.ComponentType<any> }> => {
-        const module = await loadRemoteModule();
-        const Component = module?.default ?? module;
-        if (typeof Component !== "function" && typeof Component !== "object") {
-          throw new Error(
-            `Remote module ${pluginId}:${componentPath} did not export a default React component.`,
-          );
-        }
-        return { default: Component as React.ComponentType<any> };
-      },
-      loading: LoadingElement,
-    });
-  }
+  } else {
+    return createRemoteComponent({
+      export: undefined, // allow default; see config change below to optionally set this
+      fallback: ErrorFallback,
+      loader: async () => loadRemoteModule(),
+      loading: LoadingElement,
+    });
+  }

And add an optional export selector to the config (outside the changed hunk):

// extend RemoteComponentConfig
export interface RemoteComponentConfig {
  componentPath: string;
  pluginId: NamespacedId;
  strategy?: "bridge" | "no-bridge";
  exportName?: string; // optional named export to render
}

Then thread export: config.exportName as any into the createRemoteComponent call if provided.


135-146: Remove unused local RemoteModule interface

This local interface isn’t referenced and can confuse readers because there’s another RemoteModule in the plugins manager.

-type LazyRemoteComponentInfo<T, E extends keyof T> = RemoteComponentParams<T>;
-
-interface RemoteModule {
-  [key: string]: any; // Allow indexing with any string key
-  [key: symbol]: any; // Allow indexing with any symbol key
-  provider?: () => {
-    destroy: (info: { dom: any }) => void;
-    // Make provider optional if not always present
-    render: (info: RenderFnParams) => void;
-  };
-}
+type LazyRemoteComponentInfo<T, E extends keyof T> = RemoteComponentParams<T>;
libs/portal-framework-core/src/components/WidgetArea.tsx (3)

3-7: Avoid double-sorting; getWidgetsForArea already returns sorted widgets

api/framework.getWidgetsForArea sorts via sortWidgets. Sorting again here is redundant work. Drop the local memoized sort and its import; render widgets directly.

Apply:

-import { sortWidgets } from "../utils/widget";
-import { WidgetDefinition } from "../types/widget";
+// (no local sort required)

-  const widgets = framework.getWidgetsForArea(id);
-  const sortedWidgets = React.useMemo(() => sortWidgets(widgets), [widgets]);
+  const widgets = framework.getWidgetsForArea(id);

If you want the component to own sort policy, consider returning unsorted data from the framework and keeping the memo here—but avoid both.

Also applies to: 19-26


24-24: Remove Tailwind gap-4; inline grid styles already set gap

getGridStyles(area) produces the gap from area.grid.gap. Having both a Tailwind class and inline style is confusing; inline styles win, but the duplicate can cause hydration diffs and misleads readers.

Apply:

-    <div className="grid gap-4" style={getGridStyles(area)}>
+    <div className="grid" style={getGridStyles(area)}>

6-6: Unused type import

WidgetDefinition is imported but not used after removing the local sort. Drop it to keep the file lean.

-import { WidgetDefinition } from "../types/widget";
libs/portal-framework-core/src/components/WidgetArea.spec.tsx (1)

11-11: Remove unused remoteComponentLoader mock

WidgetArea no longer uses remote component loading; the mock is dead code in this spec.

Apply:

-vi.mock("../plugins/remoteComponentLoader");
libs/portal-framework-core/src/components/WidgetArea.stories.tsx (1)

9-9: Remove unused top-level MockWidget

After switching to direct components in getWidgetsForArea, the top-level MockWidget is unused.

-const MockWidget = () => <div className="p-4 border rounded">Mock Widget</div>;
libs/portal-framework-core/src/utils/widget.ts (1)

3-16: Deterministic, non-mutating sort with sensible fallbacks

  • Clones the array prior to sort.
  • Prioritizes order, then column, then id for tie-breaks.
  • Uses MAX_SAFE_INTEGER to keep undefined values last without NaN traps.

Optional: if you keep sorting in the framework layer, you can avoid re-sorting in WidgetArea (see related comment).

libs/portal-framework-core/src/types/plugin.ts (1)

15-16: Duplicate string unions for status types

FeatureStateStatus and PluginLoadStatus have identical unions. Consider reusing a shared alias to reduce duplication and drift risk, e.g., type LoadStatus = "failed" | "loaded" | "loading"; then export type FeatureStateStatus = LoadStatus; export type PluginLoadStatus = LoadStatus;

If helpful, I can open a follow-up PR to consolidate.

Also applies to: 45-46

libs/portal-framework-core/src/api/framework.ts (6)

40-42: Use a type-only import for PortalMeta.

PortalMeta is used purely as a type in this file. Importing it as a value can bloat bundles and cause runtime import churn.

Apply this change outside the current range (Line 21):

-import { PortalMeta } from "../types/portal";
+import type { PortalMeta } from "../types/portal";

61-62: Maps for areas and widgets look good; consider freezing externally exposed definitions.

If WidgetAreaDefinition or WidgetDefinition objects are mutated after registration, ordering/sizing logic could drift. If that’s a concern, consider shallow-freezing copies upon insertion.


148-161: Auto-registering areas and widgets during initialize(): add duplicate guards and ordering determinism.

  • If multiple plugins register the same widget area ID, the last one wins silently.
  • Consider warning or throwing on duplicate area IDs; same for duplicate widget IDs at this stage to fail fast.
  • Optional: collect all areas first (across plugins), validate uniqueness, then register—this yields deterministic behavior regardless of plugin iteration order.

Apply this minimal guard:

-        plugin.widgets.areas?.forEach((area) => {
-          this.registerWidgetArea(area);
-        });
+        plugin.widgets.areas?.forEach((area) => {
+          if (this.widgetAreas.has(area.id)) {
+            console.warn(`Widget area ${area.id} already registered; skipping duplicate from plugin ${plugin.id}`);
+            return;
+          }
+          this.registerWidgetArea(area);
+        });

299-302: Guard duplicate area registration.

Consider warning or throwing if area.id is already present to avoid accidental overrides.

   registerWidgetArea(area: WidgetAreaDefinition): void {
-    this.widgetAreas.set(area.id, area);
+    if (this.widgetAreas.has(area.id)) {
+      console.warn(`Widget area ${area.id} already exists; skipping`);
+      return;
+    }
+    this.widgetAreas.set(area.id, area);
   }

303-318: Remove redundant spread and set pluginId explicitly; optional: validate pluginId.

componentName: widget.componentName is redundant after spreading widget. Also consider validating pluginId once here for consistency.

   registerWidgetsFromPlugin(
     pluginId: NamespacedId,
     widgets: WidgetRegistration[],
   ): void {
-    widgets.forEach((widget) => {
-      this.registerWidget({
-        ...widget,
-        componentName: widget.componentName,
-        pluginId,
-      });
-    });
+    // Optional: validateNamespacedId(pluginId);
+    widgets.forEach((widget) => {
+      this.registerWidget({
+        ...widget,
+        pluginId,
+      });
+    });
   }

331-350: Normalize fallback portal URL to include scheme.

When the env fallback is a bare domain (e.g., "example.com"), portalUrl becomes scheme-less. Align with the success path by prefixing https:// if needed.

   async #fetchAndSetPortalMeta(): Promise<void> {
@@
     } catch (error) {
       console.error("Failed to fetch portal meta:", error);
 
       // Fallback to env var or current location
-      const fallbackUrl = env.VITE_PORTAL_DOMAIN || getCurrentLocation().origin;
-      this.#portalUrl = fallbackUrl;
+      const raw = env.VITE_PORTAL_DOMAIN || getCurrentLocation().origin;
+      const fallbackUrl = raw.startsWith("http") ? raw : `https://${raw}`;
+      this.#portalUrl = fallbackUrl;
       console.warn(`Using fallback portal URL: ${fallbackUrl}`);
     }
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5759699 and b31ae56.

📒 Files selected for processing (42)
  • libs/portal-framework-core/src/api/builder.ts (3 hunks)
  • libs/portal-framework-core/src/api/framework.spec.ts (4 hunks)
  • libs/portal-framework-core/src/api/framework.ts (8 hunks)
  • libs/portal-framework-core/src/components/ErrorDisplay.stories.tsx (2 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundary.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.spec.tsx (2 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteLoading.spec.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteLoading.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.spec.tsx (4 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.tsx (1 hunks)
  • libs/portal-framework-core/src/contexts/framework.tsx (3 hunks)
  • libs/portal-framework-core/src/env.ts (1 hunks)
  • libs/portal-framework-core/src/index.ts (1 hunks)
  • libs/portal-framework-core/src/plugins/context-bridge.tsx (3 hunks)
  • libs/portal-framework-core/src/plugins/manager.spec.ts (1 hunks)
  • libs/portal-framework-core/src/plugins/manager.ts (3 hunks)
  • libs/portal-framework-core/src/plugins/remoteComponentLoader.spec.tsx (1 hunks)
  • libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (6 hunks)
  • libs/portal-framework-core/src/types/api.ts (1 hunks)
  • libs/portal-framework-core/src/types/capabilities.ts (1 hunks)
  • libs/portal-framework-core/src/types/navigation.ts (3 hunks)
  • libs/portal-framework-core/src/types/plugin.ts (2 hunks)
  • libs/portal-framework-core/src/types/portal.ts (1 hunks)
  • libs/portal-framework-core/src/types/widget.ts (1 hunks)
  • libs/portal-framework-core/src/util/dependencyGraph.spec.ts (4 hunks)
  • libs/portal-framework-core/src/util/dependencyGraph.ts (3 hunks)
  • libs/portal-framework-core/src/util/domain.spec.ts (1 hunks)
  • libs/portal-framework-core/src/util/framework-initializer.ts (1 hunks)
  • libs/portal-framework-core/src/util/namespace.spec.ts (2 hunks)
  • libs/portal-framework-core/src/util/portalMeta.spec.ts (5 hunks)
  • libs/portal-framework-core/src/util/portalMeta.ts (2 hunks)
  • libs/portal-framework-core/src/utils/grid.ts (1 hunks)
  • libs/portal-framework-core/src/utils/widget.ts (1 hunks)
  • libs/portal-framework-core/src/vite/index.ts (1 hunks)
  • libs/portal-plugin-dashboard/src/index.ts (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/components/Card.tsx (2 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/account.profile.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/account.security.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/dashboard.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/widgetRegistrations.ts (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • libs/portal-framework-core/src/vite/index.ts
  • libs/portal-framework-core/src/types/capabilities.ts
🚧 Files skipped from review as they are similar to previous changes (29)
  • libs/portal-framework-core/src/plugins/remoteComponentLoader.spec.tsx
  • libs/portal-framework-core/src/util/dependencyGraph.spec.ts
  • libs/portal-plugin-dashboard/src/ui/routes/dashboard.tsx
  • libs/portal-framework-core/src/plugins/manager.ts
  • libs/portal-framework-core/src/components/RouteErrorBoundary.stories.tsx
  • libs/portal-framework-core/src/util/namespace.spec.ts
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.spec.tsx
  • libs/portal-framework-core/src/utils/grid.ts
  • libs/portal-framework-core/src/components/RouteLoading.spec.tsx
  • libs/portal-framework-core/src/types/widget.ts
  • libs/portal-plugin-dashboard/src/ui/routes/account.profile.tsx
  • libs/portal-framework-core/src/types/api.ts
  • libs/portal-plugin-dashboard/src/ui/components/Card.tsx
  • libs/portal-framework-core/src/api/builder.ts
  • libs/portal-plugin-dashboard/src/index.ts
  • libs/portal-plugin-dashboard/src/widgetRegistrations.ts
  • libs/portal-plugin-dashboard/src/ui/routes/account.security.tsx
  • libs/portal-framework-core/src/env.ts
  • libs/portal-framework-core/src/util/dependencyGraph.ts
  • libs/portal-framework-core/src/util/domain.spec.ts
  • libs/portal-framework-core/src/components/ErrorDisplay.stories.tsx
  • libs/portal-framework-core/src/plugins/context-bridge.tsx
  • libs/portal-framework-core/src/types/portal.ts
  • libs/portal-framework-core/src/contexts/framework.tsx
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.stories.tsx
  • libs/portal-framework-core/src/components/RouteLoading.stories.tsx
  • libs/portal-framework-core/src/index.ts
  • libs/portal-framework-core/src/api/framework.spec.ts
  • libs/portal-framework-core/src/types/navigation.ts
🧰 Additional context used
🧬 Code graph analysis (11)
libs/portal-framework-core/src/utils/widget.ts (1)
libs/portal-framework-core/src/types/widget.ts (1)
  • WidgetDefinition (35-51)
libs/portal-framework-core/src/plugins/manager.spec.ts (1)
libs/portal-framework-core/src/types/plugin.ts (1)
  • NamespacedId (18-18)
libs/portal-framework-core/src/util/framework-initializer.ts (3)
libs/portal-framework-core/src/api/builder.ts (1)
  • framework (12-18)
libs/portal-framework-core/src/api/framework.ts (2)
  • framework (31-36)
  • framework (37-39)
libs/portal-framework-core/src/plugins/manager.ts (2)
  • framework (23-25)
  • framework (27-32)
libs/portal-framework-core/src/components/WidgetArea.spec.tsx (2)
libs/portal-framework-core/src/api/framework.ts (1)
  • Framework (27-351)
libs/portal-framework-core/src/components/WidgetArea.tsx (1)
  • WidgetArea (12-49)
libs/portal-framework-core/src/types/plugin.ts (2)
libs/portal-framework-core/src/types/widget.ts (1)
  • PluginWidgets (30-33)
libs/portal-framework-core/src/index.ts (3)
  • NamespacedId (49-49)
  • PluginInitStatus (51-51)
  • PluginLoadStatus (52-52)
libs/portal-framework-core/src/components/WidgetArea.stories.tsx (2)
libs/portal-framework-core/src/api/framework.ts (2)
  • Framework (27-351)
  • meta (40-42)
libs/portal-framework-core/src/components/WidgetArea.tsx (1)
  • WidgetArea (12-49)
libs/portal-framework-core/src/util/portalMeta.spec.ts (2)
libs/portal-framework-core/src/types/portal.ts (1)
  • PortalMeta (11-16)
libs/portal-framework-core/src/util/portalMeta.ts (1)
  • getPluginMeta (62-72)
libs/portal-framework-core/src/api/framework.ts (6)
libs/portal-framework-core/src/types/portal.ts (1)
  • PortalMeta (11-16)
libs/portal-framework-core/src/plugins/manager.ts (9)
  • id (449-460)
  • id (462-467)
  • id (469-474)
  • id (476-489)
  • id (491-499)
  • id (501-509)
  • id (511-519)
  • id (521-526)
  • id (528-534)
libs/portal-framework-core/src/types/widget.ts (4)
  • WidgetAreaDefinition (5-20)
  • WidgetDefinition (35-51)
  • WidgetRegistrationWithComponent (71-73)
  • WidgetRegistration (53-69)
libs/portal-framework-core/src/utils/widget.ts (1)
  • sortWidgets (3-16)
libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (2)
  • createRemoteComponentLoader (43-93)
  • defaultRemoteOptions (108-111)
libs/portal-framework-core/src/env.ts (1)
  • env (4-38)
libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (1)
libs/portal-framework-core/src/plugins/manager.ts (1)
  • RemoteModule (16-20)
libs/portal-framework-core/src/util/portalMeta.ts (2)
libs/portal-framework-core/src/api/framework.ts (2)
  • portalUrl (43-48)
  • meta (40-42)
libs/portal-framework-core/src/types/portal.ts (1)
  • PortalMeta (11-16)
libs/portal-framework-core/src/components/WidgetArea.tsx (3)
libs/portal-framework-core/src/contexts/framework.tsx (1)
  • useFramework (147-153)
libs/portal-framework-core/src/utils/widget.ts (1)
  • sortWidgets (3-16)
libs/portal-framework-core/src/utils/grid.ts (1)
  • getGridStyles (15-27)
🪛 Biome (2.1.2)
libs/portal-framework-core/src/components/WidgetArea.tsx

[error] 45-45: Expected a statement but instead found ')'.

Expected a statement here.

(parse)


[error] 46-46: expected ) but instead found }

Remove }

(parse)


[error] 21-21: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🔇 Additional comments (8)
libs/portal-framework-core/src/util/portalMeta.spec.ts (1)

88-98: Memoization test is solid

Good use of the cache clear helper and precise verification of the single network call.

libs/portal-framework-core/src/util/framework-initializer.ts (1)

143-144: shouldInitialize simplification is correct

The optional chaining succinctly encodes “no framework or not initialized”.

libs/portal-framework-core/src/components/WidgetArea.spec.tsx (1)

36-42: Keep useFramework mock shape consistent with context

Your mock correctly returns { framework, getAppName, ... }, which matches the context shape. This will work once WidgetArea destructures { framework }.

libs/portal-framework-core/src/components/WidgetArea.stories.tsx (2)

31-41: Storybook meta/render setup looks good

Wrapping WidgetArea with MockFrameworkProvider and passing args aligns with the new API. Tags/title are placed correctly.


46-56: Props updated to id (not widgetAreaId) — good

Confirming the stories use id, matching the component’s props.

libs/portal-framework-core/src/api/framework.ts (3)

43-48: Defensive getter is good—ensures initialization before access.

Throwing when #portalUrl isn’t set prevents subtle runtime bugs from premature access.


116-122: Good: strict existence check for widget area.

Clear error message helps diagnose misconfigured areas early.


245-248: Public init flag is a nice touch.

Makes tests and bootstrap code cleaner by avoiding internal state peeks.

@pcfreak30 pcfreak30 force-pushed the libs/portal-plugin-dashboard branch from b31ae56 to 8957665 Compare August 22, 2025 02:08
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libs/portal-framework-core/src/util/dependencyGraph.ts (1)

22-28: Guard against unknown nodes in getters

new Set(this.#dependencies.get(node)) throws if node hasn’t been added. Public getters should return an empty set in that case. Same for getDependents.

   getDependencies(node: T): Set<T> {
-    return new Set(this.#dependencies.get(node));
+    const deps = this.#dependencies.get(node);
+    return deps ? new Set(deps) : new Set<T>();
   }

   getDependents(node: T): Set<T> {
-    return new Set(this.#reverseLookup.get(node));
+    const rev = this.#reverseLookup.get(node);
+    return rev ? new Set(rev) : new Set<T>();
   }
♻️ Duplicate comments (5)
libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (4)

13-13: Good: FallbackProps correctly imported and used with ErrorBoundary

The switch to using FallbackProps aligns with react-error-boundary’s expectations and fixes the earlier mismatch.


68-74: Good: Safe default export extraction in bridge path

Using module.default || module prevents failures for modules that don’t default-export a component. This addresses the earlier runtime risk.


113-122: Stop conflating module shape with component props; remove wrapper props from params

RemoteComponentParams<T, E> uses T both as the remote module shape and as the wrapped component’s props via props?: T. This collapses two distinct concerns and forces @ts-ignore later. Split module M from props and keep params strictly about loading/selection.

Apply this diff to RemoteComponentParams:

-export interface RemoteComponentParams<
-  T = Record<string, unknown>,
-  E extends keyof T = keyof T,
-> {
-  export?: E;
-  fallback: React.ComponentType<FallbackProps>;
-  loader: () => Promise<T>;
-  loading: React.ReactNode;
-  props?: T;
-}
+export interface RemoteComponentParams<
+  M = Record<string, unknown>,
+  E extends keyof M & string = any
+> {
+  export?: E;
+  fallback: React.ComponentType<FallbackProps>;
+  loader: () => Promise<M>;
+  loading: React.ReactNode;
+}

Follow-up (outside these lines): adjust createRemoteComponent/createLazyRemoteComponent generics from T to M to reflect “module” rather than “props”. This lets us infer the exported component’s props cleanly (see next comment).


124-130: Type RemoteComponentProps as the selected export’s actual props; remove index signature and wrapper

The index signature plus nested props?: T defeats type safety and necessitates @ts-ignore at use sites. Expose the real props from the selected export and accept them directly.

Apply this diff to redefine RemoteComponentProps:

-export interface RemoteComponentProps<T = Record<string, unknown>> {
-  [key: string]: unknown;
-  fallback?: React.ComponentType<FallbackProps>;
-  loading?: React.ReactNode;
-  props?: T;
-}
+export type RemoteComponentProps<
+  M,
+  E extends keyof M & string
+> = M[E] extends React.ComponentType<infer P>
+  ? P
+  : M[keyof M] extends React.ComponentType<infer P2>
+    ? P2
+    : Record<string, never>;

Then, update createRemoteComponent to pass props directly and drop the @ts-ignore (outside this hunk):

export function createRemoteComponent<
  M extends { [k in E]: React.ComponentType<any> } & { default: React.ComponentType<any> },
  E extends keyof M & string = "default"
>(info: RemoteComponentParams<M, E>) {
  const LazyComponent = createLazyRemoteComponent<M, E>(info);
  return forwardRef<HTMLDivElement, RemoteComponentProps<M, E>>((props, ref) => (
    <ErrorBoundary FallbackComponent={info.fallback}>
      <React.Suspense fallback={info.loading}>
        <LazyComponent {...(props as any)} ref={ref as any} />
      </React.Suspense>
    </ErrorBoundary>
  ));
}

Result: callers write instead of <RemoteComp props={{ foo: "bar" }} />, and type inference flows from the chosen export.

libs/portal-framework-core/src/types/plugin.ts (1)

31-32: Missing type import for PluginWidgets breaks Plugin.widgets typing

Plugin references PluginWidgets but it isn’t imported. Add a type-only relative import.

+import type { PluginWidgets } from "./widget";
🧹 Nitpick comments (23)
libs/portal-framework-core/src/util/portalMeta.spec.ts (2)

15-20: Add a focused test for the new "other-plugin" entry.

You already add other-plugin with meta.active: false. Add a quick assertion to exercise cross-plugin lookup as well.

Suggested test snippet (place within the getPluginMeta describe block):

it("should read boolean property from another plugin", () => {
  expect(getPluginMeta(mockMeta, "other-plugin", "active")).toBe(false);
});

152-155: Strengthen type-safety tests with negative compile-time checks.

Runtime assertions prove values, but generics are compile-time only. Add @ts-expect-error lines to ensure the generic shape is enforced by the compiler.

Suggested additions next to the existing assertions:

// Compile-time only checks – these should produce type errors.
// @ts-expect-error 'enabled' is not part of { version: string }
void getPluginMeta<{ version: string }>(mockMeta, "test-plugin")?.enabled;

// @ts-expect-error boolean does not have property 'value'
void getPluginMeta<boolean>(mockMeta, "test-plugin", "settings.enabled")?.value;

Also applies to: 158-162

libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (5)

48-49: Avoid ambiguous union return type with typed overloads keyed by strategy

Returning a union (() => Promise<BridgeResult>) | React.ComponentType forces callers to narrow manually and weakens type safety. Prefer overloads that bind the return type to config.strategy.

Add overloads above the implementation:

export function createRemoteComponentLoader(
  config: RemoteComponentConfig & { strategy: "bridge" },
  framework: Framework,
  options: RemoteComponentOptions,
): () => Promise<BridgeResult<any>>;

export function createRemoteComponentLoader(
  config: RemoteComponentConfig & { strategy?: "no-bridge" },
  framework: Framework,
  options: RemoteComponentOptions,
): React.ComponentType<RemoteComponentProps>;

This preserves the current runtime behavior while providing precise compile-time types for callers.


82-89: Good: Aligns default-extraction logic with bridge path; tweak error text

The fallback to module?.default ?? module is correct. Consider updating the error message (Line 85) to “did not export a valid React component” since you now accept either default or non-default shapes.

Proposed text tweak outside the changed line:

-            `Remote module ${pluginId}:${componentPath} did not export a default React component.`,
+            `Remote module ${pluginId}:${componentPath} did not export a valid React component.`,

135-135: Alias naming is fine; consider E’s default of "default" for ergonomics

type LazyRemoteComponentInfo<T, E extends keyof T> mirrors RemoteComponentParams. After the refactor above, defaulting E to "default" will better reflect common usage.

Suggested tweak outside this line:

type LazyRemoteComponentInfo<M, E extends keyof M & string = "default"> = RemoteComponentParams<M, E>;

137-146: Rename or remove local RemoteModule to avoid confusion with manager.ts RemoteModule

There’s an exported RemoteModule interface elsewhere (libs/portal-framework-core/src/plugins/manager.ts). This local RemoteModule has a different shape, is unused here, and may confuse maintainers.

Apply this diff to remove it:

-interface RemoteModule {
-  [key: string]: any; // Allow indexing with any string key
-  [key: symbol]: any; // Allow indexing with any symbol key
-  provider?: () => {
-    destroy: (info: { dom: any }) => void;
-    // Make provider optional if not always present
-    render: (info: RenderFnParams) => void;
-  };
-}
+// (removed) Local RemoteModule type was unused and conflicted in name with the exported type in plugins/manager.ts.

If needed elsewhere, reintroduce as LoadedRemoteProvider or FederatedProvider to disambiguate.


188-199: Optional: allow props-level overrides for fallback/loading or drop them from props entirely

You define fallback/loading on info and also in RemoteComponentProps, but only info.* is used. Either:

  • Let props override info for per-instance control, or
  • Remove these from props (preferred if you want a stable global config).

Minimal override example (outside this hunk):

return forwardRef<HTMLDivElement, RemoteComponentProps<M, E> & {
  fallback?: React.ComponentType<FallbackProps>;
  loading?: React.ReactNode;
}>((props, ref) => {
  const { fallback, loading, ...componentProps } = props;
  return (
    <ErrorBoundary FallbackComponent={fallback ?? info.fallback}>
      <React.Suspense fallback={loading ?? info.loading}>
        <LazyComponent {...(componentProps as any)} ref={ref as any} />
      </React.Suspense>
    </ErrorBoundary>
  );
});

Also note: after adopting the props typing refactor, the @ts-ignore lines can be removed.

libs/portal-framework-core/src/util/namespace.spec.ts (1)

26-29: Nit: test title grammar

“should namespaced unqualified IDs” → “should namespace unqualified IDs”. Keeps test descriptions clear.

libs/portal-plugin-dashboard/src/ui/components/Card.tsx (1)

49-51: Make bottom-aligned content opt-in

mt-auto forces the content block to the bottom of the card. This is great for dashboard tiles with fixed-height headers, but it will surprise consumers in non-grid contexts or when content should flow under the header. Gate this with a prop (e.g., contentStickBottom?: boolean).

Apply:

-      <UICardContent className={cn("space-y-4 mt-auto", contentClassName)}>
+      <UICardContent
+        className={cn("space-y-4", contentStickBottom && "mt-auto", contentClassName)}
+      >

Outside the shown range, add:

// In CardProps
contentStickBottom?: boolean;

// In defaults
contentStickBottom = false,
libs/portal-plugin-dashboard/src/widgetRegistrations.ts (2)

6-31: Reduce duplication: factor out the common grid config

All three areas repeat { columns: 12, gap: 16, rowHeight: "auto" }. Extract a const to avoid drift.

+const GRID = { columns: 12, gap: 16, rowHeight: "auto" } as const;

 export const widgetAreas: WidgetAreaDefinition[] = [
   {
-    grid: {
-      columns: 12,
-      gap: 16,
-      rowHeight: "auto",
-    },
+    grid: { ...GRID },
     id: "core:dashboard:header",
   },
   {
-    grid: {
-      columns: 12,
-      gap: 16,
-      rowHeight: "auto",
-    },
+    grid: { ...GRID },
     id: "core:dashboard:profile",
   },
   {
-    grid: {
-      columns: 12,
-      gap: 16,
-      rowHeight: "auto",
-    },
+    grid: { ...GRID },
     id: "core:dashboard:security",
   },
 ];

33-100: Consider setting explicit order and optional location.column for deterministic layout

The new sorter uses order and column position. Right now all widgets rely on auto-placement. If the header banner must always come first and span 12 columns, and profile widgets should follow a known order, add order (e.g., 0, 10, 20…) and position.location.column where needed.

Example:

   {
     areaId: "core:dashboard:header",
     componentName: "widgets/account/emailVerificationBanner",
     id: "core:dashboard:email-verification",
+    order: 0,
     position: {
+      location: { column: 1 },
       size: { height: 1, width: 12 },
     },
   },
libs/portal-framework-core/src/util/dependencyGraph.ts (2)

45-47: Avoid non-null assertion in traversal

Use a safe default to prevent accidental runtime errors if maps and sets ever get out of sync.

-        for (const dep of this.#dependencies.get(node)!) {
+        const deps = this.#dependencies.get(node);
+        for (const dep of deps ?? []) {
           visit(dep);
         }

55-58: Comment clarity

The code starts with nodes that have no incoming edges (i.e., no dependents in #reverseLookup). Reword the comment to avoid confusion.

-    // Start with nodes that have no dependents
+    // Start with nodes that have no incoming edges (no dependents in #reverseLookup)
libs/portal-framework-core/src/utils/grid.ts (2)

38-48: Clamp widget span/height to valid values

Defensive clamp prevents generating invalid CSS like span 0.

 export function getWidgetStyles(
   widget: {
     position: {
       location?: { column?: number };
       size: { width: number; height: number };
     };
     minHeight?: string | number;
     minWidth?: string | number;
   }
-): React.CSSProperties {
+): CSSProperties {
+  const width = Math.max(1, Math.floor(widget.position.size.width));
+  const height = Math.max(1, Math.floor(widget.position.size.height));
   return {
-    gridColumn: getGridColumnSpan(
-      widget.position.location?.column,
-      widget.position.size.width
-    ),
-    gridRow: `span ${widget.position.size.height}`,
+    gridColumn: getGridColumnSpan(widget.position.location?.column, width),
+    gridRow: `span ${height}`,
     minHeight: widget.minHeight,
     minWidth: widget.minWidth,
   };
 }

50-61: Clamp span in getGridColumnSpan and unify fallback

Ensures span is at least 1 and simplifies the ternary.

 export function getGridColumnSpan(
   column?: number,
   span?: number
 ): string {
-  return column !== undefined && span !== undefined
-    ? `${column} / span ${span}`
-    : `auto / span ${span || 1}`;
+  const s = Math.max(1, span ?? 1);
+  return column !== undefined ? `${column} / span ${s}` : `auto / span ${s}`;
 }
libs/portal-framework-core/src/types/plugin.ts (2)

3-8: Prefer type-only + relative imports; fix Node Module typing to avoid runtime cycles

  • Avoid monorepo-absolute paths for portability.
  • Use type-only imports to prevent runtime imports and circular deps.
  • The import * as Module pattern introduces a value import; extend the type instead.
-import type React from "react";
-import { Framework } from "libs/portal-framework-core/src/api/framework";
-import * as Module from "node:module";
-import { FrameworkFeature } from "../types/api";
-import { BaseCapability } from "./capabilities";
-import { RouteDefinition } from "./navigation";
+import type { ComponentType } from "react";
+import type { Framework } from "../api/framework";
+import type { Module as NodeModule } from "node:module";
+import type { FrameworkFeature } from "./api";
+import type { BaseCapability } from "./capabilities";
+import type { RouteDefinition } from "./navigation";
-export interface PluginModule extends Module {
+export interface PluginModule extends NodeModule {
   default: () => Plugin;
 }

Also applies to: 47-49


37-38: Use ComponentType directly; avoid default React import for types

Simplifies typing and avoids relying on default React export.

-export type PluginExports = Record<string, React.ComponentType>;
+export type PluginExports = Record<string, ComponentType>;
libs/portal-framework-core/src/types/navigation.ts (2)

26-29: Prefer ComponentType over React.FC for icon prop

ComponentType<NavigationItemIconProps> is more general and avoids FC-specific semantics. Also switch to a named type import from react.

-import type React from "react";
+import type { ComponentType } from "react";
-  icon?: React.FC<NavigationItemIconProps>;
+  icon?: ComponentType<NavigationItemIconProps>;

Also applies to: 1-1, 17-18


31-33: Avoid redundant re-declaration of caseSensitive

RouteDefinition extends Omit<RouteObject, "children">; caseSensitive already exists on RouteObject. Re-declaration is unnecessary.

-export interface RouteDefinition extends Omit<RouteObject, "children"> {
-  caseSensitive?: boolean;
+export interface RouteDefinition extends Omit<RouteObject, "children"> {
libs/portal-framework-core/src/api/framework.spec.ts (4)

26-26: Tidy mocks lifecycle: avoid double-restore and redundant resets

  • Using both resetAllMocks() and clearAllMocks() is redundant.
  • restoreAllMocks() already restores the spy; no need to call mockRestore() on the same spy.
   beforeEach(() => {
-    vi.resetAllMocks();
-    vi.clearAllMocks();
+    vi.clearAllMocks();
     mockPluginManager = createMockPluginManager();
     mockCapabilityManager = createMockCapabilityManager();
     framework = new Framework(
@@
-    consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
+    consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
   });
@@
   afterEach(() => {
-    vi.restoreAllMocks();
-    consoleWarnSpy.mockRestore(); // Restore the spy after each test
+    vi.restoreAllMocks();
   });

Also applies to: 29-32


54-97: Great coverage for auto-registration; consider adding layout/sorting assertions

Given the PR adds grid layout and sorting by order/column, add assertions that:

  • multiple widgets sort by order then by position.location.column
  • widgets respect column-span bounds and don’t overflow the area grid

I can draft an additional spec that seeds two widgets in the same area with different orders/columns and verifies getWidgetsForArea sorting and a failure path when width exceeds area columns. Want me to push that?


147-149: Align comments with mocks behavior in “initialize once” test

You mock pluginManager.initializePlugins to a resolved Map, so mockPluginInitialize is never invoked. Either:

  • keep expect(mockPluginInitialize).toHaveBeenCalledTimes(0) and update the comment to reflect that the mock bypasses the call, or
  • assert via pluginManager mock instead of the plugin’s initialize.
-    // The plugin's initialize method should be called as part of pluginManager.initializePlugins
-    // We don't directly assert mockPluginInitialize here as it's called internally by PluginManager
+    // pluginManager.initializePlugins is mocked, so the plugin's initialize is not invoked in this test.
+    // We assert initializePlugins calls count instead.

Also applies to: 160-167, 175-176, 184-185


186-190: Also assert exactly one re-initialization warning

You already match the message; assert call count for stronger guarantee.

     await framework.initialize(); // Third attempt
     expect(consoleWarnSpy).toHaveBeenCalledWith(
       expect.stringContaining("Framework for test-app already initialized."),
     );
+    expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b31ae56 and 8957665.

📒 Files selected for processing (42)
  • libs/portal-framework-core/src/api/builder.ts (3 hunks)
  • libs/portal-framework-core/src/api/framework.spec.ts (4 hunks)
  • libs/portal-framework-core/src/api/framework.ts (8 hunks)
  • libs/portal-framework-core/src/components/ErrorDisplay.stories.tsx (2 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundary.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.spec.tsx (2 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteLoading.spec.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteLoading.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.spec.tsx (4 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.tsx (1 hunks)
  • libs/portal-framework-core/src/contexts/framework.tsx (3 hunks)
  • libs/portal-framework-core/src/env.ts (1 hunks)
  • libs/portal-framework-core/src/index.ts (1 hunks)
  • libs/portal-framework-core/src/plugins/context-bridge.tsx (3 hunks)
  • libs/portal-framework-core/src/plugins/manager.spec.ts (1 hunks)
  • libs/portal-framework-core/src/plugins/manager.ts (3 hunks)
  • libs/portal-framework-core/src/plugins/remoteComponentLoader.spec.tsx (1 hunks)
  • libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (6 hunks)
  • libs/portal-framework-core/src/types/api.ts (1 hunks)
  • libs/portal-framework-core/src/types/capabilities.ts (1 hunks)
  • libs/portal-framework-core/src/types/navigation.ts (3 hunks)
  • libs/portal-framework-core/src/types/plugin.ts (2 hunks)
  • libs/portal-framework-core/src/types/portal.ts (1 hunks)
  • libs/portal-framework-core/src/types/widget.ts (1 hunks)
  • libs/portal-framework-core/src/util/dependencyGraph.spec.ts (4 hunks)
  • libs/portal-framework-core/src/util/dependencyGraph.ts (3 hunks)
  • libs/portal-framework-core/src/util/domain.spec.ts (1 hunks)
  • libs/portal-framework-core/src/util/framework-initializer.ts (1 hunks)
  • libs/portal-framework-core/src/util/namespace.spec.ts (2 hunks)
  • libs/portal-framework-core/src/util/portalMeta.spec.ts (5 hunks)
  • libs/portal-framework-core/src/util/portalMeta.ts (2 hunks)
  • libs/portal-framework-core/src/utils/grid.ts (1 hunks)
  • libs/portal-framework-core/src/utils/widget.ts (1 hunks)
  • libs/portal-framework-core/src/vite/index.ts (1 hunks)
  • libs/portal-plugin-dashboard/src/index.ts (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/components/Card.tsx (2 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/account.profile.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/account.security.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/dashboard.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/widgetRegistrations.ts (1 hunks)
✅ Files skipped from review due to trivial changes (3)
  • libs/portal-framework-core/src/util/domain.spec.ts
  • libs/portal-framework-core/src/plugins/manager.spec.ts
  • libs/portal-framework-core/src/components/RouteLoading.spec.tsx
🚧 Files skipped from review as they are similar to previous changes (28)
  • libs/portal-framework-core/src/utils/widget.ts
  • libs/portal-framework-core/src/plugins/remoteComponentLoader.spec.tsx
  • libs/portal-plugin-dashboard/src/ui/routes/dashboard.tsx
  • libs/portal-framework-core/src/types/api.ts
  • libs/portal-framework-core/src/components/RouteErrorBoundary.stories.tsx
  • libs/portal-plugin-dashboard/src/ui/routes/account.profile.tsx
  • libs/portal-framework-core/src/util/dependencyGraph.spec.ts
  • libs/portal-framework-core/src/components/ErrorDisplay.stories.tsx
  • libs/portal-framework-core/src/components/RouteLoading.stories.tsx
  • libs/portal-framework-core/src/components/WidgetArea.spec.tsx
  • libs/portal-framework-core/src/vite/index.ts
  • libs/portal-framework-core/src/types/widget.ts
  • libs/portal-framework-core/src/util/framework-initializer.ts
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.spec.tsx
  • libs/portal-plugin-dashboard/src/index.ts
  • libs/portal-framework-core/src/plugins/context-bridge.tsx
  • libs/portal-framework-core/src/types/portal.ts
  • libs/portal-framework-core/src/plugins/manager.ts
  • libs/portal-framework-core/src/api/framework.ts
  • libs/portal-framework-core/src/api/builder.ts
  • libs/portal-framework-core/src/components/WidgetArea.tsx
  • libs/portal-framework-core/src/env.ts
  • libs/portal-plugin-dashboard/src/ui/routes/account.security.tsx
  • libs/portal-framework-core/src/components/WidgetArea.stories.tsx
  • libs/portal-framework-core/src/contexts/framework.tsx
  • libs/portal-framework-core/src/util/portalMeta.ts
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.stories.tsx
  • libs/portal-framework-core/src/index.ts
🧰 Additional context used
🧬 Code graph analysis (7)
libs/portal-framework-core/src/utils/grid.ts (1)
libs/portal-framework-core/src/types/widget.ts (1)
  • DEFAULT_WIDGET_AREA_DEFINITION (22-28)
libs/portal-framework-core/src/types/capabilities.ts (1)
libs/portal-framework-core/src/index.ts (1)
  • CapabilityStatus (37-37)
libs/portal-plugin-dashboard/src/widgetRegistrations.ts (1)
libs/portal-framework-core/src/types/widget.ts (2)
  • WidgetAreaDefinition (5-20)
  • WidgetRegistration (53-69)
libs/portal-framework-core/src/types/navigation.ts (2)
libs/portal-framework-core/src/index.ts (2)
  • NamespacedId (49-49)
  • NavigationItemIconProps (45-45)
libs/portal-framework-core/src/types/plugin.ts (1)
  • NamespacedId (18-18)
libs/portal-framework-core/src/types/plugin.ts (1)
libs/portal-framework-core/src/types/widget.ts (1)
  • PluginWidgets (30-33)
libs/portal-framework-core/src/util/portalMeta.spec.ts (2)
libs/portal-framework-core/src/types/portal.ts (1)
  • PortalMeta (11-16)
libs/portal-framework-core/src/util/portalMeta.ts (1)
  • getPluginMeta (62-72)
libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (1)
libs/portal-framework-core/src/plugins/manager.ts (1)
  • RemoteModule (16-20)
🔇 Additional comments (19)
libs/portal-framework-core/src/types/capabilities.ts (2)

22-23: Reordering is safe; no functional or API change.

Declaring CapabilityStatus after BaseCapability is fine in TypeScript (types are available for reference regardless of declaration order). Public API and runtime remain unchanged.


18-19: Action Required: Verify ESLint’s no-use-before-define handling or hoist the CapabilityStatus alias

We didn’t find any @typescript-eslint/no-use-before-define settings in the project’s ESLint configs, and the type alias is declared after its first use. This can trigger lint errors if the rule is active without ignoring type references. To avoid noise:

• Hoist the alias above its first reference in
libs/portal-framework-core/src/types/capabilities.ts (currently lines 18–19 refer to it, and it’s declared at 22–23).

// Move this up above BaseCapability:
- export type CapabilityStatus = "active" | "error" | "inactive";
+ export type CapabilityStatus = "active" | "error" | "inactive";

  export type BaseCapability<TType extends string> = {
    readonly status: CapabilityStatus;
    readonly type: TType;
    // …
  };

• Or add to your ESLint config (e.g. root .eslintrc.json):

{
  "rules": {
    "@typescript-eslint/no-use-before-define": ["error", { "ignoreTypeReferences": true }]
  }
}

• Note: CapabilityStatus is correctly re-exported in
libs/portal-framework-core/src/index.ts (around line 37) — no change needed there.

libs/portal-framework-core/src/util/portalMeta.spec.ts (5)

5-9: Good: deterministic cache reset for memoized fetches.

Importing and using __test_clearCache keeps tests isolated and avoids cross-test flakiness from memoization.


26-31: Mock structure aligns with getPluginMeta contract.

The nested shape and version inside meta match the current implementation and are covered by subsequent tests.


129-133: Nice coverage of deep path resolution.

Validates dot-path traversal and guards against regressions in the reducer logic.


138-142: Good negative case for invalid nested path.

Covers the undefined path branch and optional chaining behavior.


173-175: Edge case covered: empty plugin meta.

Confirms getPluginMeta returns an empty object vs. undefined for present-but-empty metadata.

libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (2)

131-133: OK: RenderFnParams adds explicit dom handle

Explicit dom: HTMLElement in RenderFnParams improves clarity when integrating providers. No issues.


20-23: Manual verification required: align BridgeResult interface with upstream @module-federation/bridge-react v18

I wasn’t able to automatically locate the upstream type definitions in this environment—please verify locally that the BridgeResult you’ve declared here exactly matches the one published by @module-federation/bridge-react/v18. Any mismatch in parameters or return types (especially around destroy) can lead to unsafe casts at call sites.

• File under review:
libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (lines 20–23)
• Upstream definitions to compare against (your local install):
node_modules/@module-federation/bridge-react/v18/index.d.ts (or equivalent)

What to check:

  • destroy
    – Parameter shape ({ dom: HTMLElement; moduleName: string })
    – Return type (void vs. Promise<void>)
  • render
    – Parameter types (Record<string, unknown> & RenderParams)
    – Return type (Promise<void>)

If you find any discrepancies, adjust your BridgeResult signature here to match the official typings.

libs/portal-framework-core/src/util/namespace.spec.ts (1)

38-41: LGTM: updated parse expectation matches the new NamespacedId shape

The assertion now expects { name, namespace }. This aligns with the broader refactor. No issues.

libs/portal-plugin-dashboard/src/ui/components/Card.tsx (1)

35-39: No additional border utility needed in wrapper

Verified that the core Card component (aliased as UICard in portal-plugin-dashboard) already includes the border class in its base styles:

  • In libs/portal-framework-ui-core/src/components/ui/card.tsx, the default className includes "rounded-xl border bg-card/30 text-card-foreground shadow", so border-width is always applied.

Because UICard always renders with a border, the dashboard wrapper only needs to toggle the border color via border-border. No changes are required to add a redundant border utility.

libs/portal-plugin-dashboard/src/widgetRegistrations.ts (1)

58-60: Confirm Unique IDs for Widgets and Areas

I noticed in libs/portal-plugin-dashboard/src/widgetRegistrations.ts that you define an area and a widget sharing the same id:

  • In the widgetAreas array (lines 5–15):
    id: "core:dashboard:profile"

  • In the widgetRegistrations array (lines 58–60):
    id: "core:dashboard:profile"

If your framework treats area ids and widget ids in separate namespaces, this won’t cause a runtime issue. Please confirm whether the portal registry merges those keys or keeps them distinct. If they can collide, I recommend renaming the widget id to something more descriptive, for example:

-    componentName: "widgets/account/profile",
-    id: "core:dashboard:profile",
+    componentName: "widgets/account/profile",
+    id: "core:dashboard:profile-card",
     position: {
libs/portal-framework-core/src/types/plugin.ts (3)

15-16: FeatureStateStatus addition looks good

Union covers expected lifecycle states and matches usage in FeatureState.


45-46: PluginLoadStatus addition is consistent with init/load split

Clear separation between init and load phases. LGTM.


47-49: PluginModule is in use – keep it

PluginModule is referenced in multiple locations across the codebase—including getPluginInfo.ts, api/builder.ts, and plugins/manager.spec.ts—so it is not dead code and should be retained.

libs/portal-framework-core/src/types/navigation.ts (3)

5-6: Good fix: relative import for internal type

Switching to ./plugin improves package portability outside the monorepo.


21-24: parentId typed as NamespacedId is a good addition

Brings stronger linkage to the route tree. LGTM.


53-57: Inline docs for parentId are clear

Helpful guidance for nesting and namespacing. LGTM.

libs/portal-framework-core/src/api/framework.spec.ts (1)

82-86: Stubbing initialize paths is correct for isolate testing initialize()

Mocking both plugin and capability initialization to empty Maps keeps the test focused on widget registration. LGTM.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
libs/portal-framework-core/src/plugins/manager.ts (1)

420-426: Bug: retryFailedPlugins calls loadFeature with a pluginId (mismatched types/semantics)

loadFeature expects a feature id, not a plugin id. This path will always throw and never reinitialize failed plugins.

Apply this diff to reattempt plugin initialization instead of feature load:

   async retryFailedPlugins() {
     const failedPlugins = Array.from(this.#pluginStates.entries())
       .filter(
         ([_, state]) =>
           state.loadState === "failed" || state.initState === "failed",
       )
       .map(([id]) => id);

     for (const pluginId of failedPlugins) {
       try {
-        await this.loadFeature(pluginId);
+        const plugin = this.getOrActivatePlugin(pluginId);
+        if (!plugin?.initialize) continue;
+        this.#markPluginInitializing(pluginId);
+        await plugin.initialize(this.framework);
+        this.#markPluginInitialized(pluginId);
       } catch (error) {
         console.error(`Retry failed for plugin ${pluginId}:`, error);
+        this.#markPluginFailed(
+          pluginId,
+          error instanceof Error ? error : new Error(String(error)),
+          true,
+        );
       }
     }
   }

Alternatively, if ordering/dependency checks are required, consider rerunning await this.initializePlugins() and filtering the result for remaining failures.

libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (1)

20-23: Update BridgeResult.destroy to return Promise

The upstream createBridgeComponent implementation defines destroy() as returning a Promise<void>, not a plain void. Keeping our local interface in sync is critical to maintain assignability and avoid runtime/type errors. [1]

• File: libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx
• Lines: 20–23

Suggested change:

 export interface BridgeResult<T> {
-  destroy(info: { dom: HTMLElement; moduleName: string }): void; // Change Promise<void> to void
+  destroy(info: { dom: HTMLElement; moduleName: string }): Promise<void>; // align with upstream
   render(info: Record<string, unknown> & RenderParams): Promise<void>;
 }

[1]: https://module-federation.io/zh/practice/bridge/react-bridge.html?utm_source=chatgpt.com [destroy returns Promise]

♻️ Duplicate comments (7)
libs/portal-framework-core/src/types/api.ts (1)

16-24: No behavioral change; keep enforcing a concrete FeatureStatus at creation time

Reordering and the union literal order are harmless. Reminder that status is still required by FrameworkFeature; ensure all feature instances initialize it (e.g., "disabled") at construction to avoid transient undefined reads by consumers.

If not already done in this PR, run your usual grep to confirm every FrameworkFeature implementation sets status at creation.

libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (2)

68-69: Fix confirmed: safe default export extraction in bridge path

Switching to module.default || module prevents runtime errors when no default export exists.


82-83: Fix confirmed: safe default export extraction in default loader path

Using module?.default ?? module aligns both paths and avoids undefined component errors.

libs/portal-plugin-dashboard/src/ui/routes/dashboard.tsx (1)

11-12: LGTM: WidgetArea now uses id prop

Matches the framework refactor.

Run a quick sweep to ensure no remaining usages of the old widgetAreaId prop linger in tests/stories:

#!/bin/bash
rg -nP '\bwidgetAreaId\s*=' -g '!**/dist/**' -g '!**/build/**'
libs/portal-framework-core/src/components/WidgetArea.tsx (1)

19-21: Empty-state grid wrapper retained — previous issue resolved

Keeping the grid container even when empty preserves layout and drop/placeholder behavior. Thanks for addressing this.

libs/portal-framework-core/src/types/plugin.ts (1)

31-32: Missing import for PluginWidgets (breaks type-checking).

Plugin.widgets?: PluginWidgets is referenced but PluginWidgets is not imported in this file.

Apply this diff to fix:

@@
 import { RouteDefinition } from "./navigation";
+import type { PluginWidgets } from "./widget";
libs/portal-framework-core/src/api/framework.ts (1)

268-297: Fix WidgetDefinition construction: remove invalid fields and include required dimensions.

description, label, and title are not part of WidgetDefinition. minHeight and minWidth are required but currently omitted. Type mismatch likely causes TS errors and stores unintended fields.

Apply:

   registerWidget(widget: WidgetRegistrationWithComponent): void {
@@
     // Construct definition explicitly with only allowed fields
     const definition: WidgetDefinition = {
       areaId: widget.areaId,
-      component: createRemoteComponentLoader(
+      component: createRemoteComponentLoader(
         {
           componentPath: widget.componentName,
           pluginId: widget.pluginId,
         },
         this,
         defaultRemoteOptions,
-      ),
-      description: widget.description,
+      ) as WidgetDefinition["component"],
       id: widget.id,
-      label: widget.label,
+      minHeight: widget.minHeight,
+      minWidth: widget.minWidth,
       order: widget.order,
       position: widget.position,
-      title: widget.title,
     };

If you prefer not to cast, widen WidgetDefinition["component"] to match the loader’s return type.

🧹 Nitpick comments (20)
libs/portal-framework-core/src/components/RouteErrorBoundary.stories.tsx (1)

32-36: Story name vs. behavior mismatch (minor).

WithChildren intentionally throws via shouldThrow={true}, so it showcases the error state rather than a neutral “with children” scenario. Consider renaming to WithChildrenError or switching this to shouldThrow={false} to make it distinct from WithRouterError.

libs/portal-framework-core/src/util/dependencyGraph.ts (3)

6-12: Guard against self-dependencies

Currently addDependency(node, node) will introduce a trivial cycle. Add a quick check to fail early.

   addDependency(from: T, to: T): void {
+    if (from === to) {
+      throw new Error(`Self-dependency detected for node ${String(from)}`);
+    }
     this.addNode(from);
     this.addNode(to);

     this.#dependencies.get(from)!.add(to);
     this.#reverseLookup.get(to)!.add(from);
   }

56-58: Root selection should be “no dependencies” (in-degree) rather than “no dependents” (out-degree)

Using nodes with zero dependents as roots is unconventional and can produce a reversed “priority” view. It still yields a valid topo order thanks to DFS, but starting from nodes with zero dependencies is clearer and avoids surprising orders.

-  // Start with nodes that have no dependents
-  const rootNodes = Array.from(this.#nodes).filter(
-    (node) => this.#reverseLookup.get(node)!.size === 0,
-  );
+  // Start with nodes that have no dependencies (in-degree == 0)
+  const rootNodes = Array.from(this.#nodes).filter(
+    (node) => this.#dependencies.get(node)!.size === 0,
+  );

If the semantic of addDependency(from, to) is “from depends on to” (current implementation), this change better matches common expectations.


22-28: Protect getters from unknown nodes

new Set(this.#dependencies.get(node)) will throw if node was never added. Return an empty set when missing to make the API safer.

Consider changing both getters:

getDependencies(node: T): Set<T> {
  return new Set(this.#dependencies.get(node) ?? []);
}
getDependents(node: T): Set<T> {
  return new Set(this.#reverseLookup.get(node) ?? []);
}
libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (3)

113-129: Tighten generics: separate module shape from component props; remove need for @ts-ignore later

Current types conflate remote module shape and the wrapped component’s props, forcing the nested props and @ts-ignore. Split module and export generics, and infer the component props.

Apply this diff:

-export interface RemoteComponentParams<
-  T = Record<string, unknown>,
-  E extends keyof T = keyof T,
-> {
-  export?: E;
-  fallback: React.ComponentType<FallbackProps>;
-  loader: () => Promise<T>;
-  loading: React.ReactNode;
-  props?: T;
-}
+export interface RemoteComponentParams<
+  M = Record<string, unknown>,
+  E extends keyof M & string = any
+> {
+  export?: E;
+  fallback: React.ComponentType<FallbackProps>;
+  loader: () => Promise<M>;
+  loading: React.ReactNode;
+}
 
-export interface RemoteComponentProps<T = Record<string, unknown>> {
-  [key: string]: unknown;
-  fallback?: React.ComponentType<FallbackProps>;
-  loading?: React.ReactNode;
-  props?: T;
-}
+type InferComponentProps<M, E extends keyof M & string> =
+  M[E] extends React.ComponentType<infer P>
+    ? P
+    : M[keyof M] extends React.ComponentType<infer P2>
+      ? P2
+      : Record<string, never>;
+
+export type RemoteComponentProps<
+  M = Record<string, unknown>,
+  E extends keyof M & string = any
+> = {
+  // Back-compat shim: keep nested 'props' while allowing direct props usage.
+  // Prefer passing props directly; 'props' will be removed in a future cleanup.
+  props?: InferComponentProps<M, E>;
+  fallback?: React.ComponentType<FallbackProps>;
+  loading?: React.ReactNode;
+} & Partial<InferComponentProps<M, E>>;

Follow-up (separate change): update createRemoteComponent’s generics to thread M, E into RemoteComponentProps<M, E> and remove the @ts-ignore spreads.


137-145: Loosen any index signatures; prefer unknown and narrow locally

The open-ended [key: string]: any / [key: symbol]: any harms type safety and masks mistakes. Use unknown and narrow where needed.

-interface RemoteModule {
-  [key: string]: any; // Allow indexing with any string key
-  [key: symbol]: any; // Allow indexing with any symbol key
+interface RemoteModule {
+  [key: string]: unknown;
+  [key: symbol]: unknown;
   provider?: () => {
     destroy: (info: { dom: any }) => void;
     // Make provider optional if not always present
     render: (info: RenderFnParams) => void;
   };
 }

31-37: Optional: type ErrorComponent with FallbackProps for consistency

Align RemoteComponentOptions.ErrorComponent to React.ComponentType<FallbackProps> to match react-error-boundary.

For example:

export interface RemoteComponentOptions {
  ErrorComponent: React.ComponentType<FallbackProps>;
  LoadingComponent: React.ComponentType;
}
libs/portal-framework-core/src/api/builder.ts (1)

59-66: Avoid non-null assertion on loadRemote result; validate module and default export

Relying on ! may turn a recoverable condition into a cryptic runtime error. Validate mod and that mod.default is a callable factory; throw a targeted error to aid debugging.

Apply:

-      try {
-        const mod = (await loadRemote(moduleId))!;
-        const { id: pluginId } = getPluginInfo(mod);
-
-        this.#plugins.registerRemoteModule(moduleId, remoteEntry, pluginId);
-        this.#plugins.registerFactory(pluginId, mod.default);
-        this.#plugins.enableAndActivatePlugin(pluginId);
-      } catch (error) {
+      try {
+        const mod = await loadRemote(moduleId);
+        if (!mod) {
+          throw new Error(`Remote module ${moduleId} returned no exports`);
+        }
+        const { id: pluginId } = getPluginInfo(mod);
+        if (typeof mod.default !== "function") {
+          throw new Error(
+            `Remote module ${moduleId} default export is not a plugin factory`,
+          );
+        }
+        this.#plugins.registerRemoteModule(moduleId, remoteEntry, pluginId);
+        this.#plugins.registerFactory(pluginId, mod.default);
+        this.#plugins.enableAndActivatePlugin(pluginId);
+      } catch (error) {
         console.error(`Failed to register remote module ${moduleId}:`, error);
         throw error;
       }
libs/portal-framework-core/src/utils/widget.ts (1)

3-16: Pure, deterministic sort with sensible fallbacks — nice

Cloning before sort prevents accidental mutation; order → column → id is a solid priority chain. Consider numeric-aware ID comparison to keep widget-2 before widget-10.

Apply:

-import type { WidgetDefinition } from "../types/widget";
+import type { WidgetDefinition } from "../types/widget";
+const idCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });

 export function sortWidgets(widgets: WidgetDefinition[]): WidgetDefinition[] {
   return [...widgets].sort((a, b) => {
     // Prefer explicit order if specified
     const orderA = a.order ?? Number.MAX_SAFE_INTEGER;
     const orderB = b.order ?? Number.MAX_SAFE_INTEGER;
     
     // Fall back to column position with safe defaults
     const colA = a.position.location?.column ?? Number.MAX_SAFE_INTEGER;
     const colB = b.position.location?.column ?? Number.MAX_SAFE_INTEGER;
     
     // Final fallback to ID comparison
-    return orderA - orderB || colA - colB || a.id.localeCompare(b.id);
+    return orderA - orderB || colA - colB || idCollator.compare(a.id, b.id);
   });
 }

Optionally add unit tests to lock this behavior (ties by order/column resolve by ID). I can draft them if useful.

libs/portal-framework-core/src/api/framework.spec.ts (3)

26-32: Avoid double restore of console.warn spy

vi.restoreAllMocks() already restores spies; the explicit consoleWarnSpy.mockRestore() is redundant.

Apply:

 afterEach(() => {
   vi.restoreAllMocks();
-  consoleWarnSpy.mockRestore(); // Restore the spy after each test
 });

54-97: Make assertions resilient to defaults applied by Framework

If getWidgetArea fills defaults (e.g., gap/rowHeight), strict equality may become brittle. Prefer partial matching for area and widget.

Apply:

-    // Verify widget area was registered
-    expect(framework.getWidgetArea("dashboard")).toEqual({
-      grid: { columns: 12 },
-      id: "dashboard",
-    });
+    // Verify widget area was registered
+    expect(framework.getWidgetArea("dashboard")).toEqual(
+      expect.objectContaining({
+        id: "dashboard",
+        grid: expect.objectContaining({ columns: 12 }),
+      }),
+    );

     // Verify widget was registered
-    const widgets = framework.getWidgetsForArea("dashboard");
-    expect(widgets).toHaveLength(1);
-    expect(widgets[0].id).toBe("test-widget");
+    const widgets = framework.getWidgetsForArea("dashboard");
+    expect(widgets).toHaveLength(1);
+    expect(widgets[0]).toEqual(
+      expect.objectContaining({
+        id: "test-widget",
+        areaId: "dashboard",
+      }),
+    );

147-167: Align comment with assertion or drop the assertion on initialize calls

The comment says we don’t assert the plugin’s initialize, yet there’s an assertion expecting zero calls. This is confusing and may be fragile.

Apply:

-    // The plugin's initialize method should be called as part of pluginManager.initializePlugins
-    // We don't directly assert mockPluginInitialize here as it's called internally by PluginManager
+    // The plugin's initialize method is called internally by PluginManager; we don't assert it here.
@@
-    // Verify the plugin's initialize method was NOT called again
-    expect(mockPluginInitialize).toHaveBeenCalledTimes(0); // PluginManager.initializePlugins calls this
+    // Verify only managers were not re-invoked

Also applies to: 184-185

libs/portal-framework-core/src/contexts/framework.tsx (1)

123-136: Consider safe fallbacks instead of throwing when framework not yet ready

Throwing here can crash UI paths that render before initialization completes. Optionally return sensible fallbacks and rely on isLoading for gating UI.

Apply:

-    getWidgetArea: (id: string) => {
-      if (!state.framework) {
-        throw new Error("Framework not initialized");
-      }
-      return state.framework.getWidgetArea(id);
-    },
-    getWidgetsForArea: (id: string) => {
-      if (!state.framework) {
-        throw new Error("Framework not initialized");
-      }
-      return state.framework.getWidgetsForArea(id);
-    },
+    getWidgetArea: (id: string) => {
+      return state.framework ? state.framework.getWidgetArea(id) : { id, grid: { columns: 12 } };
+    },
+    getWidgetsForArea: (id: string) => {
+      return state.framework ? state.framework.getWidgetsForArea(id) : [];
+    },

If throwing is intentional for developer ergonomics, feel free to ignore.

libs/portal-framework-core/src/components/WidgetArea.tsx (1)

24-30: Optional: strengthen list keys for area-local uniqueness

If id isn’t globally unique, consider ${widget.areaId}:${widget.id} as the key to avoid collisions when rendering multiple areas on the same page.

Apply:

-              key={widget.id}
+              key={`${widget.areaId}:${widget.id}`}
libs/portal-framework-core/src/types/plugin.ts (2)

37-37: Be explicit about component props for PluginExports.

Use an explicit props type to avoid implicit-any and make intent clear.

-export type PluginExports = Record<string, React.ComponentType>;
+export type PluginExports = Record<string, React.ComponentType<unknown>>;

4-4: Avoid extending Node’s Module here; declare the minimal shape instead.

Extending node:module’s Module type is unnecessary and brittle. We only need the default export contract.

-import * as Module from "node:module";
@@
-export interface PluginModule extends Module {
-  default: () => Plugin;
-}
+export interface PluginModule {
+  default: () => Plugin;
+}

Also applies to: 47-49

libs/portal-framework-core/src/api/framework.ts (4)

93-97: Remove non-null assertion; return type already allows undefined.

getCapability currently asserts non-null with ! while the signature returns T | undefined. Drop the assertion to reflect reality.

-  async getCapability<T extends BaseCapability>(
-    id: string,
-  ): Promise<T | undefined> {
-    return (await this.#capabilities.get(id))!;
-  }
+  async getCapability<T extends BaseCapability>(id: string): Promise<T | undefined> {
+    return this.#capabilities.get(id);
+  }

299-301: Guard duplicate widget areas.

Currently, re-registering an area silently overwrites it. Add a guard to warn or throw to catch configuration mistakes early.

   registerWidgetArea(area: WidgetAreaDefinition): void {
-    this.widgetAreas.set(area.id, area);
+    if (this.widgetAreas.has(area.id)) {
+      console.warn(`Overwriting existing widget area: ${area.id}`);
+    }
+    this.widgetAreas.set(area.id, area);
   }

306-317: Drop redundant property spread in registerWidgetsFromPlugin.

componentName is already part of widget; re-adding it is redundant.

-      this.registerWidget({
-        ...widget,
-        componentName: widget.componentName,
-        pluginId,
-      });
+      this.registerWidget({ ...widget, pluginId });

331-350: Meta fallback: consider validating/normalizing the URL once.

The fallback path sets #portalUrl but leaves #meta as null. If downstream code relies on meta.domain, consider storing a minimal meta or normalizing #portalUrl with a single helper (protocol + no trailing slash).

Happy to provide a small normalizeUrl(url: string): string helper and thread it through here if you want.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b31ae56 and 8957665.

📒 Files selected for processing (42)
  • libs/portal-framework-core/src/api/builder.ts (3 hunks)
  • libs/portal-framework-core/src/api/framework.spec.ts (4 hunks)
  • libs/portal-framework-core/src/api/framework.ts (8 hunks)
  • libs/portal-framework-core/src/components/ErrorDisplay.stories.tsx (2 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundary.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.spec.tsx (2 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteLoading.spec.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteLoading.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.spec.tsx (4 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.tsx (1 hunks)
  • libs/portal-framework-core/src/contexts/framework.tsx (3 hunks)
  • libs/portal-framework-core/src/env.ts (1 hunks)
  • libs/portal-framework-core/src/index.ts (1 hunks)
  • libs/portal-framework-core/src/plugins/context-bridge.tsx (3 hunks)
  • libs/portal-framework-core/src/plugins/manager.spec.ts (1 hunks)
  • libs/portal-framework-core/src/plugins/manager.ts (3 hunks)
  • libs/portal-framework-core/src/plugins/remoteComponentLoader.spec.tsx (1 hunks)
  • libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (6 hunks)
  • libs/portal-framework-core/src/types/api.ts (1 hunks)
  • libs/portal-framework-core/src/types/capabilities.ts (1 hunks)
  • libs/portal-framework-core/src/types/navigation.ts (3 hunks)
  • libs/portal-framework-core/src/types/plugin.ts (2 hunks)
  • libs/portal-framework-core/src/types/portal.ts (1 hunks)
  • libs/portal-framework-core/src/types/widget.ts (1 hunks)
  • libs/portal-framework-core/src/util/dependencyGraph.spec.ts (4 hunks)
  • libs/portal-framework-core/src/util/dependencyGraph.ts (3 hunks)
  • libs/portal-framework-core/src/util/domain.spec.ts (1 hunks)
  • libs/portal-framework-core/src/util/framework-initializer.ts (1 hunks)
  • libs/portal-framework-core/src/util/namespace.spec.ts (2 hunks)
  • libs/portal-framework-core/src/util/portalMeta.spec.ts (5 hunks)
  • libs/portal-framework-core/src/util/portalMeta.ts (2 hunks)
  • libs/portal-framework-core/src/utils/grid.ts (1 hunks)
  • libs/portal-framework-core/src/utils/widget.ts (1 hunks)
  • libs/portal-framework-core/src/vite/index.ts (1 hunks)
  • libs/portal-plugin-dashboard/src/index.ts (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/components/Card.tsx (2 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/account.profile.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/account.security.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/dashboard.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/widgetRegistrations.ts (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • libs/portal-framework-core/src/components/RouteLoading.spec.tsx
  • libs/portal-framework-core/src/util/namespace.spec.ts
🚧 Files skipped from review as they are similar to previous changes (26)
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.spec.tsx
  • libs/portal-framework-core/src/plugins/remoteComponentLoader.spec.tsx
  • libs/portal-framework-core/src/util/domain.spec.ts
  • libs/portal-plugin-dashboard/src/ui/routes/account.profile.tsx
  • libs/portal-framework-core/src/util/framework-initializer.ts
  • libs/portal-plugin-dashboard/src/widgetRegistrations.ts
  • libs/portal-plugin-dashboard/src/ui/components/Card.tsx
  • libs/portal-framework-core/src/types/capabilities.ts
  • libs/portal-framework-core/src/plugins/manager.spec.ts
  • libs/portal-plugin-dashboard/src/index.ts
  • libs/portal-framework-core/src/vite/index.ts
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.stories.tsx
  • libs/portal-framework-core/src/components/ErrorDisplay.stories.tsx
  • libs/portal-framework-core/src/types/widget.ts
  • libs/portal-framework-core/src/env.ts
  • libs/portal-plugin-dashboard/src/ui/routes/account.security.tsx
  • libs/portal-framework-core/src/utils/grid.ts
  • libs/portal-framework-core/src/util/dependencyGraph.spec.ts
  • libs/portal-framework-core/src/types/portal.ts
  • libs/portal-framework-core/src/plugins/context-bridge.tsx
  • libs/portal-framework-core/src/util/portalMeta.ts
  • libs/portal-framework-core/src/types/navigation.ts
  • libs/portal-framework-core/src/components/RouteLoading.stories.tsx
  • libs/portal-framework-core/src/components/WidgetArea.spec.tsx
  • libs/portal-framework-core/src/components/WidgetArea.stories.tsx
  • libs/portal-framework-core/src/util/portalMeta.spec.ts
🧰 Additional context used
🧬 Code graph analysis (5)
libs/portal-framework-core/src/utils/widget.ts (1)
libs/portal-framework-core/src/types/widget.ts (1)
  • WidgetDefinition (35-51)
libs/portal-plugin-dashboard/src/ui/routes/dashboard.tsx (1)
libs/portal-framework-core/src/index.ts (1)
  • WidgetArea (7-7)
libs/portal-framework-core/src/contexts/framework.tsx (1)
libs/portal-framework-core/src/types/widget.ts (2)
  • WidgetAreaDefinition (5-20)
  • WidgetDefinition (35-51)
libs/portal-framework-core/src/types/api.ts (1)
libs/portal-framework-core/src/types/plugin.ts (1)
  • NamespacedId (18-18)
libs/portal-framework-core/src/types/plugin.ts (2)
libs/portal-framework-core/src/types/widget.ts (1)
  • PluginWidgets (30-33)
libs/portal-framework-core/src/index.ts (3)
  • NamespacedId (49-49)
  • PluginInitStatus (51-51)
  • PluginLoadStatus (52-52)
🔇 Additional comments (14)
libs/portal-framework-core/src/components/RouteErrorBoundary.stories.tsx (1)

5-9: Meta property reordering is safe — all stories include a title
Storybook ignores object key order; moving title after tags is non-functional and has no impact on CSF or autodocs. Verification across all .stories.tsx files found no missing title properties.

libs/portal-framework-core/src/plugins/manager.ts (3)

110-113: Method relocation is fine

Moving getFeatureState nearer to getFeature improves discoverability without behavior change.


321-325: Good: loadFeature now resolves to the initialized feature instance

Chaining .then(() => feature) ensures callers receive the concrete FrameworkFeature. Caching the promise before awaiting also dedupes concurrent loads.


477-480: Formatting-only change

No action needed.

libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (2)

48-50: Return type union reorder is fine

No behavior change; keeps the types readable.


188-199: Good: ErrorBoundary wiring now passes FallbackComponent directly

This aligns the prop with react-error-boundary expectations and the updated RemoteComponentParams.fallback type.

libs/portal-plugin-dashboard/src/ui/routes/dashboard.tsx (1)

11-12: Resolved: core:dashboard:header area is registered

I’ve verified that the core:dashboard:header area is already defined and registered in widgetRegistrations.ts:

  • Line 13: { id: "core:dashboard:header", … }
  • Line 35: { areaId: "core:dashboard:header", … }

No further action needed here.

libs/portal-framework-core/src/api/builder.ts (1)

22-22: Type re-declaration is fine

Switching #operations to (() => Promise<void>)[] is equivalent and clearer. No action needed.

libs/portal-framework-core/src/contexts/framework.tsx (1)

29-34: New context APIs are coherent and discoverable

Adding getAppName, getWidgetArea, and getWidgetsForArea on the context sharpens the consumer API. Nice.

libs/portal-framework-core/src/index.ts (2)

68-70: All references updated: no stale getPortalManifests calls found.

The grep search for the old getPortalManifests identifier returned no matches, confirming that all callers now use getPortalPluginManifests. No further changes are needed.


57-61: All clear: no remaining WidgetRegistration imports from ./types/plugin

Verification with ripgrep returned no matches, confirming that the public re-export aligns consumers with the new surface and fully decouples from ./types/plugin.

libs/portal-framework-core/src/api/framework.ts (3)

116-122: LGTM: Strict existence check for widget area.

Clear error messaging if an area is missing improves debuggability.


124-132: LGTM: Centralized sorting via sortWidgets.

Good move replacing ad hoc sort; avoids the previous colStart pitfall and keeps ordering logic consistent.


148-162: Please confirm the contents of defaultRemoteOptions and the body of createRemoteComponentLoader in remoteComponentLoader.tsx—specifically whether framework.portalUrl is referenced or captured at load time—so we can verify that the loader defers portalUrl access to render/invocation time.

@pcfreak30 pcfreak30 force-pushed the libs/portal-plugin-dashboard branch from 8957665 to 30839ea Compare August 22, 2025 02:57
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libs/portal-framework-core/src/util/framework-initializer.ts (1)

115-133: Don’t return an asserted framework! on failures; ensure it’s populated or rethrow.

In the catch block, framework may be undefined if the error occurred before retrieving it. Returning { framework: framework! } risks propagating undefined at runtime under a non-optional type.

Safer pattern: attempt to obtain the framework from the builder; if that fails, rethrow the original error to preserve the contract.

   } catch (err) {
     // Handle unexpected system-level errors
     // Ensure builder is defined before returning
     if (!builder) {
       // If builder creation failed, we can't return a builder
       throw err; // Re-throw if builder is null
     }
-    return {
-      builder: builder,
-      errors: [
-        {
-          category: "system",
-          error: err instanceof Error ? err : new Error(String(err)),
-          id: "system-initialization",
-        },
-      ],
-      framework: framework!, // framework might be undefined if error happened before builder.framework
-      success: false,
-    };
+    // Try to recover a Framework instance; if not possible, rethrow to avoid returning invalid shape.
+    let safeFramework = framework;
+    if (!safeFramework) {
+      try {
+        safeFramework = await builder.framework;
+      } catch {
+        throw err;
+      }
+    }
+    return {
+      builder,
+      errors: [
+        {
+          category: "system",
+          error: err instanceof Error ? err : new Error(String(err)),
+          id: "system-initialization",
+        },
+      ],
+      framework: safeFramework,
+      success: false,
+    };
   }
♻️ Duplicate comments (7)
libs/portal-framework-core/src/components/WidgetArea.stories.tsx (1)

48-49: Prop rename to id is correctly applied in stories

Matches the new WidgetArea signature. This addresses the earlier feedback.

Also applies to: 54-55

libs/portal-framework-core/src/utils/grid.ts (2)

1-1: Resolved: explicit CSSProperties import.

Past suggestion to avoid React.CSSProperties is implemented. Good.


22-22: Resolved: return types use the imported CSSProperties.

Return signatures switched from React.CSSProperties to CSSProperties. Matches prior guidance.

libs/portal-framework-core/src/types/plugin.ts (1)

31-32: Missing import for PluginWidgets breaks type checking.

Plugin references PluginWidgets but it isn’t imported.

 import { RouteDefinition } from "./navigation";
+import type { PluginWidgets } from "./widget";
libs/portal-framework-core/src/api/framework.ts (1)

268-297: Construct a clean WidgetDefinition; remove invalid fields and include required dimensions.

Spreading description, label, and title into WidgetDefinition (which typically does not include these) will cause TS excess-property errors and leaks unintended fields at runtime. Also ensure minHeight/minWidth are included if required by the type.

   registerWidget(widget: WidgetRegistrationWithComponent): void {
     if (this.widgets.has(widget.id)) {
       throw new Error(`Widget with id ${widget.id} already registered`);
     }

     if (!this.widgetAreas.has(widget.areaId)) {
       throw new Error(`Widget area ${widget.areaId} not registered`);
     }

-    // Construct definition explicitly with only allowed fields
-    const definition: WidgetDefinition = {
-      areaId: widget.areaId,
-      component: createRemoteComponentLoader(
-        {
-          componentPath: widget.componentName,
-          pluginId: widget.pluginId,
-        },
-        this,
-        defaultRemoteOptions,
-      ),
-      description: widget.description,
-      id: widget.id,
-      label: widget.label,
-      order: widget.order,
-      position: widget.position,
-      title: widget.title,
-    };
+    // Construct definition explicitly with only allowed fields
+    const { componentName, pluginId, ...rest } = widget;
+    const definition: WidgetDefinition = {
+      ...rest, // areaId, id, order, position, minHeight, minWidth, etc.
+      component: createRemoteComponentLoader(
+        { componentPath: componentName, pluginId },
+        this,
+        defaultRemoteOptions,
+      ) as React.ComponentType, // ensure assignability to WidgetDefinition['component']
+    };

     this.widgets.set(widget.id, definition);
   }

If WidgetDefinition.component already matches the loader’s return type, you can remove the cast.

libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (2)

113-122: Split module shape from component props; remove wrapper props from params

T conflates the remote module shape with the component props and necessitates downstream // @ts-ignore. Model the module (M) separately and drop the wrapper props from params.

Apply this diff:

-export interface RemoteComponentParams<
-  T = Record<string, unknown>,
-  E extends keyof T = keyof T,
-> {
-  export?: E;
-  fallback: React.ComponentType<FallbackProps>;
-  loader: () => Promise<T>;
-  loading: React.ReactNode;
-  props?: T;
-}
+export interface RemoteComponentParams<
+  M = Record<string, unknown>,
+  E extends keyof M & string = "default"
+> {
+  export?: E;
+  fallback: React.ComponentType<FallbackProps>;
+  loader: () => Promise<M>;
+  loading: React.ReactNode;
+}

124-129: Expose the actual remote component props; drop index signature and wrapper

RemoteComponentProps should represent the props of the selected export. The index signature and nested props lead to unsafe escapes and //@ts-ignore sites.

Apply this diff:

-export interface RemoteComponentProps<T = Record<string, unknown>> {
-  [key: string]: unknown;
-  fallback?: React.ComponentType<FallbackProps>;
-  loading?: React.ReactNode;
-  props?: T;
-}
+export type RemoteComponentProps<
+  M,
+  E extends keyof M & string
+> = M[E] extends React.ComponentType<infer P>
+  ? P
+  : M[keyof M] extends React.ComponentType<infer P2>
+    ? P2
+    : Record<string, never>;
🧹 Nitpick comments (23)
libs/portal-framework-core/src/components/RouteErrorBoundary.stories.tsx (2)

5-9: Prefer satisfies Meta<...> for stronger typing and excess-prop checks.

Using satisfies preserves the inferred type of meta while ensuring it conforms to Meta<typeof RouteErrorBoundary>, catching accidental extra fields without widening.

-const meta: Meta<typeof RouteErrorBoundary> = {
+const meta = {
   component: RouteErrorBoundary,
   tags: ["autodocs"],
   title: "Components/RouteErrorBoundary",
-};
+} satisfies Meta<typeof RouteErrorBoundary>;

21-29: Remove redundant args in WithRouterError.

render fully controls the story output; the children: undefined args add noise without effect.

 export const WithRouterError: Story = {
-  args: {
-    children: undefined,
-  },
   render: () => (
     <RouteErrorBoundary>
       <ErrorThrowingComponent shouldThrow={true} />
     </RouteErrorBoundary>
   ),
 };
libs/portal-framework-core/src/components/WidgetArea.stories.tsx (2)

9-9: Remove unused top-level MockWidget

This duplicate definition isn’t used after switching to component fields in getWidgetsForArea.

-const MockWidget = () => <div className="p-4 border rounded">Mock Widget</div>;

5-5: Make Framework a type-only import

It’s used purely for casting; type-only import avoids pulling runtime code into the story bundle.

-import { Framework } from "../api/framework";
+import type { Framework } from "../api/framework";
libs/portal-plugin-dashboard/src/ui/routes/account.security.tsx (2)

4-14: Rename component to reflect route (consistency/readability).

The file is account.security.tsx, but the component is named AccountProfile. Rename to AccountSecurity for clarity in debugging and stack traces.

-export default function AccountProfile() {
+export default function AccountSecurity() {

11-11: Use self-closing JSX for void content.

Nit: Prefer <WidgetArea id="core:dashboard:security" /> for brevity.

-      <WidgetArea id={"core:dashboard:security"}></WidgetArea>
+      <WidgetArea id="core:dashboard:security" />
libs/portal-framework-core/src/utils/grid.ts (5)

51-53: Guard against invalid column counts (0 or negatives).

Defensive clamp avoids emitting invalid CSS like repeat(0, ...). If upstream validation already enforces columns >= 1, consider this a no-op suggestion.

-export function getGridTemplateColumns(columns: number): string {
-  return `repeat(${columns}, minmax(0, 1fr))`;
-}
+export function getGridTemplateColumns(columns: number): string {
+  const safe = Math.max(1, Math.floor(columns));
+  return `repeat(${safe}, minmax(0, 1fr))`;
+}

55-62: Normalize column and span values to valid CSS grid lines.

Prevents accidental column=0 or span<=0 producing invalid declarations. If inputs are already validated elsewhere, treat this as optional hardening.

-export function getGridColumnSpan(
-  column?: number,
-  span?: number
-): string {
-  return column !== undefined && span !== undefined
-    ? `${column} / span ${span}`
-    : `auto / span ${span || 1}`;
-}
+export function getGridColumnSpan(
+  column?: number,
+  span?: number
+): string {
+  const safeSpan = Math.max(1, Math.floor(span ?? 1));
+  if (column !== undefined) {
+    const safeColumn = Math.max(1, Math.floor(column));
+    return `${safeColumn} / span ${safeSpan}`;
+  }
+  return `auto / span ${safeSpan}`;
+}

16-22: Type the area argument using framework types to avoid drift.

Leverage WidgetAreaDefinition instead of an inline shape to keep utils aligned with the source of truth.

-export function getGridStyles(area: {
-  grid: {
-    columns: number;
-    gap?: number;
-    rowHeight?: "auto" | number;
-  };
-}): CSSProperties {
+// import type { WidgetAreaDefinition } from "../types/widget";
+export function getGridStyles(area: Pick<WidgetAreaDefinition, "grid">): CSSProperties {

30-49: Optional: omit undefined style keys for cleaner objects.

Current code relies on React ignoring undefined. If you want to strip undefineds (useful when styles are serialized), build the object conditionally.

 export function getWidgetStyles(
   widget: {
@@
 ): CSSProperties {
-  return {
-    gridColumn: getGridColumnSpan(
-      widget.position.location?.column,
-      widget.position.size.width
-    ),
-    gridRow: `span ${widget.position.size.height}`,
-    minHeight: widget.minHeight,
-    minWidth: widget.minWidth,
-  };
+  const styles: CSSProperties = {
+    gridColumn: getGridColumnSpan(
+      widget.position.location?.column,
+      widget.position.size.width
+    ),
+    gridRow: `span ${widget.position.size.height}`,
+  };
+  if (widget.minHeight !== undefined) styles.minHeight = widget.minHeight;
+  if (widget.minWidth !== undefined) styles.minWidth = widget.minWidth;
+  return styles;
 }

4-62: Add focused unit tests for grid helpers.

Recommend tests to lock behavior:

  • getGridAutoRows: numeric, "auto", undefined.
  • getGridGap: numeric, 0, undefined.
  • getGridTemplateColumns: columns=1, 3, and 0 (if you adopt clamping).
  • getGridColumnSpan: with/without column, span omitted, span=0.
  • getWidgetStyles: minHeight/minWidth present/absent.

I can generate a spec file if helpful.

libs/portal-framework-core/src/util/framework-initializer.ts (1)

143-144: Init guard simplification is correct; consider clarifying unused builder param.

The new return !framework?.isInitialized(); matches the intended semantics. Since builder is unused, either mark it intentionally unused (underscore) for lint clarity or drop it in a future breaking change.

Apply this non-breaking tweak to silence unused-param linters:

-export function shouldInitialize(
-  builder?: Builder | null,
-  framework?: Framework | null,
-): boolean {
+export function shouldInitialize(
+  _builder?: Builder | null, // kept for backward-compatibility
+  framework?: Framework | null,
+): boolean {
   // We need to initialize if we don't have a framework instance,
   // or if the existing framework instance is not initialized (using the public getter).
   return !framework?.isInitialized();
 }
libs/portal-framework-core/src/api/framework.spec.ts (2)

29-32: Avoid double-restore of spies.

vi.restoreAllMocks() already restores consoleWarnSpy. Calling mockRestore() again is redundant and may be brittle across runner versions.

   afterEach(() => {
     vi.restoreAllMocks();
-    consoleWarnSpy.mockRestore(); // Restore the spy after each test
   });

54-70: Test setup covers the new widgets surface; consider asserting sort stability.

Given the framework now sorts widgets by explicit order and then column, add a second widget with different order and position.location.column to assert deterministic ordering. This will catch regressions in sortWidgets.

libs/portal-framework-core/src/types/plugin.ts (1)

3-3: Import Framework as a type and prefer a relative path to avoid runtime cycles.

Using a value import can create circular runtime deps. Switch to a type-only, relative import.

-import { Framework } from "libs/portal-framework-core/src/api/framework";
+import type { Framework } from "../api/framework";
libs/portal-framework-core/src/api/framework.ts (4)

93-97: Avoid non-null assertion in getCapability.

#capabilities.get(id) may be undefined; the ! hides that. Let the declared Promise<T | undefined> propagate naturally.

-    return (await this.#capabilities.get(id))!;
+    return await this.#capabilities.get(id);

299-301: Guard against accidental area overrides.

Silently overwriting areas makes debugging hard. Warn or throw on duplicates.

   registerWidgetArea(area: WidgetAreaDefinition): void {
-    this.widgetAreas.set(area.id, area);
+    if (this.widgetAreas.has(area.id)) {
+      console.warn(`Overwriting widget area registration: ${area.id}`);
+    }
+    this.widgetAreas.set(area.id, area);
   }

306-317: Drop redundant property spread.

componentName: widget.componentName is already included via ...widget.

   registerWidgetsFromPlugin(
     pluginId: NamespacedId,
     widgets: WidgetRegistration[],
   ): void {
     widgets.forEach((widget) => {
       this.registerWidget({
         ...widget,
-        componentName: widget.componentName,
         pluginId,
       });
     });
   }

331-349: Portal URL derivation is resilient; consider normalizing domain inputs.

If meta.domain can include trailing slashes or protocols inconsistently, normalizing with new URL(...) (or a small helper) avoids double slashes and mixed protocol issues. Optional improvement.

libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (4)

13-13: Correct use of FallbackProps with ErrorBoundary

Importing FallbackProps and wiring it through ErrorBoundary resolves the previous type mismatch.

For consistency, consider aligning RemoteComponentOptions to FallbackProps as well:

export interface RemoteComponentOptions {
  ErrorComponent: React.ComponentType<FallbackProps>;
  LoadingComponent: React.ComponentType;
}

48-49: Union return type impairs inference; prefer overloads keyed by strategy

The return type union forces consumers to branch manually and hurts IntelliSense. Overloads tied to strategy provide precise types without casts.

Add overloads just above the implementation:

export function createRemoteComponentLoader(
  config: RemoteComponentConfig & { strategy: "bridge" },
  framework: Framework,
  options: RemoteComponentOptions,
): () => Promise<BridgeResult<any>>;

export function createRemoteComponentLoader(
  config: RemoteComponentConfig & { strategy?: "no-bridge" },
  framework: Framework,
  options: RemoteComponentOptions,
): React.ComponentType<RemoteComponentProps<any, any>>;

Then remove the explicit union return type from the implementation signature so it’s governed by the overloads.


135-135: Unused generic E in alias; incorporate it or remove

E is declared but not used. Tie it to the params and provide a default for better DX.

Apply this diff:

-type LazyRemoteComponentInfo<T, E extends keyof T> = RemoteComponentParams<T>;
+type LazyRemoteComponentInfo<
+  M,
+  E extends keyof M & string = "default"
+> = RemoteComponentParams<M, E>;

137-146: Local interface rename and signature alignment

We’ve confirmed that the only declaration and use of RemoteModule in remoteComponentLoader.tsx is within that file (no external references), and that manager.ts exports a separate RemoteModule type. Renaming the local interface is safe and will reduce confusion. We’ve also verified that BridgeResult.destroy expects both a dom: HTMLElement and a moduleName: string, whereas the local provider only accepts dom.

• No occurrences of the local RemoteModule outside remoteComponentLoader.tsx.
manager.ts exports its own RemoteModule (with entry, moduleId, pluginId)—no linkage to the loader’s interface.
BridgeResult.destroy signature is (info: { dom: HTMLElement; moduleName: string }), so the provider’s destroy should accept the same.
• Index signatures can be tightened from any to unknown.

Suggested diff (optional refactor):

- interface RemoteModule {
-   [key: string]: any; // Allow indexing with any string key
-   [key: symbol]: any; // Allow indexing with any symbol key
-   provider?: () => {
-     destroy: (info: { dom: any }) => void;
-     // Make provider optional if not always present
-     render: (info: RenderFnParams) => void;
-   };
- }
+ interface RemoteProviderModule {
+   [key: string]: unknown;
+   [key: symbol]: unknown;
+   provider?: () => {
+     destroy: (info: { dom: HTMLElement; moduleName: string }) => void;
+     render: (info: RenderFnParams) => void;
+   };
+ }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8957665 and 30839ea.

📒 Files selected for processing (42)
  • libs/portal-framework-core/src/api/builder.ts (3 hunks)
  • libs/portal-framework-core/src/api/framework.spec.ts (4 hunks)
  • libs/portal-framework-core/src/api/framework.ts (8 hunks)
  • libs/portal-framework-core/src/components/ErrorDisplay.stories.tsx (2 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundary.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.spec.tsx (2 hunks)
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteLoading.spec.tsx (1 hunks)
  • libs/portal-framework-core/src/components/RouteLoading.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.spec.tsx (4 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.stories.tsx (1 hunks)
  • libs/portal-framework-core/src/components/WidgetArea.tsx (1 hunks)
  • libs/portal-framework-core/src/contexts/framework.tsx (3 hunks)
  • libs/portal-framework-core/src/env.ts (1 hunks)
  • libs/portal-framework-core/src/index.ts (1 hunks)
  • libs/portal-framework-core/src/plugins/context-bridge.tsx (3 hunks)
  • libs/portal-framework-core/src/plugins/manager.spec.ts (1 hunks)
  • libs/portal-framework-core/src/plugins/manager.ts (3 hunks)
  • libs/portal-framework-core/src/plugins/remoteComponentLoader.spec.tsx (1 hunks)
  • libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (6 hunks)
  • libs/portal-framework-core/src/types/api.ts (1 hunks)
  • libs/portal-framework-core/src/types/capabilities.ts (1 hunks)
  • libs/portal-framework-core/src/types/navigation.ts (3 hunks)
  • libs/portal-framework-core/src/types/plugin.ts (2 hunks)
  • libs/portal-framework-core/src/types/portal.ts (1 hunks)
  • libs/portal-framework-core/src/types/widget.ts (1 hunks)
  • libs/portal-framework-core/src/util/dependencyGraph.spec.ts (4 hunks)
  • libs/portal-framework-core/src/util/dependencyGraph.ts (3 hunks)
  • libs/portal-framework-core/src/util/domain.spec.ts (1 hunks)
  • libs/portal-framework-core/src/util/framework-initializer.ts (1 hunks)
  • libs/portal-framework-core/src/util/namespace.spec.ts (2 hunks)
  • libs/portal-framework-core/src/util/portalMeta.spec.ts (5 hunks)
  • libs/portal-framework-core/src/util/portalMeta.ts (2 hunks)
  • libs/portal-framework-core/src/utils/grid.ts (1 hunks)
  • libs/portal-framework-core/src/utils/widget.ts (1 hunks)
  • libs/portal-framework-core/src/vite/index.ts (1 hunks)
  • libs/portal-plugin-dashboard/src/index.ts (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/components/Card.tsx (3 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/account.profile.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/account.security.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/ui/routes/dashboard.tsx (1 hunks)
  • libs/portal-plugin-dashboard/src/widgetRegistrations.ts (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.spec.tsx
  • libs/portal-framework-core/src/components/RouteLoading.spec.tsx
🚧 Files skipped from review as they are similar to previous changes (31)
  • libs/portal-framework-core/src/plugins/remoteComponentLoader.spec.tsx
  • libs/portal-framework-core/src/util/dependencyGraph.spec.ts
  • libs/portal-plugin-dashboard/src/ui/routes/account.profile.tsx
  • libs/portal-framework-core/src/util/domain.spec.ts
  • libs/portal-framework-core/src/env.ts
  • libs/portal-framework-core/src/util/namespace.spec.ts
  • libs/portal-framework-core/src/vite/index.ts
  • libs/portal-framework-core/src/types/capabilities.ts
  • libs/portal-framework-core/src/types/widget.ts
  • libs/portal-framework-core/src/plugins/context-bridge.tsx
  • libs/portal-framework-core/src/utils/widget.ts
  • libs/portal-framework-core/src/util/dependencyGraph.ts
  • libs/portal-framework-core/src/plugins/manager.spec.ts
  • libs/portal-framework-core/src/api/builder.ts
  • libs/portal-framework-core/src/components/WidgetArea.spec.tsx
  • libs/portal-plugin-dashboard/src/ui/components/Card.tsx
  • libs/portal-framework-core/src/types/portal.ts
  • libs/portal-framework-core/src/components/WidgetArea.tsx
  • libs/portal-plugin-dashboard/src/index.ts
  • libs/portal-framework-core/src/types/api.ts
  • libs/portal-framework-core/src/components/ErrorDisplay.stories.tsx
  • libs/portal-framework-core/src/components/RouteErrorBoundaryFallback.stories.tsx
  • libs/portal-framework-core/src/components/RouteLoading.stories.tsx
  • libs/portal-framework-core/src/plugins/manager.ts
  • libs/portal-framework-core/src/util/portalMeta.spec.ts
  • libs/portal-plugin-dashboard/src/ui/routes/dashboard.tsx
  • libs/portal-framework-core/src/contexts/framework.tsx
  • libs/portal-framework-core/src/types/navigation.ts
  • libs/portal-plugin-dashboard/src/widgetRegistrations.ts
  • libs/portal-framework-core/src/index.ts
  • libs/portal-framework-core/src/util/portalMeta.ts
🧰 Additional context used
🧬 Code graph analysis (5)
libs/portal-framework-core/src/utils/grid.ts (1)
libs/portal-framework-core/src/types/widget.ts (1)
  • DEFAULT_WIDGET_AREA_DEFINITION (22-28)
libs/portal-framework-core/src/util/framework-initializer.ts (1)
libs/portal-framework-core/src/plugins/manager.ts (2)
  • framework (23-25)
  • framework (27-32)
libs/portal-framework-core/src/types/plugin.ts (1)
libs/portal-framework-core/src/types/widget.ts (1)
  • PluginWidgets (30-33)
libs/portal-plugin-dashboard/src/ui/routes/account.security.tsx (1)
libs/portal-framework-core/src/index.ts (1)
  • WidgetArea (7-7)
libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (1)
libs/portal-framework-core/src/plugins/manager.ts (1)
  • RemoteModule (16-20)
🔇 Additional comments (13)
libs/portal-framework-core/src/components/RouteErrorBoundary.stories.tsx (1)

8-8: Reordering title below tags is fine and keeps Storybook meta consistent.

No functional impact; aligns with the PR-wide style update.

libs/portal-framework-core/src/components/WidgetArea.stories.tsx (2)

2-2: No-op whitespace change

No action needed.


39-41: Story meta updates look good

tags/title placement is fine and compatible with Storybook autodocs.

libs/portal-plugin-dashboard/src/ui/routes/account.security.tsx (1)

11-11: core:dashboard:security widget area registration confirmed

The area ID "core:dashboard:security" is defined and registered in libs/portal-plugin-dashboard/src/widgetRegistrations.ts, and the framework automatically registers all plugin widget areas on startup. No further action is needed here.

• Definition found at libs/portal-plugin-dashboard/src/widgetRegistrations.ts:29 (id: "core:dashboard:security")
• Widgets targeting this area at lines 79 and 90 of the same file
• Registration hook in libs/portal-framework-core/src/api/framework.ts:153 ensures all areas from plugin.widgets.areas are registered on initialization

libs/portal-framework-core/src/util/framework-initializer.ts (1)

48-66: No changes needed: getPortalPluginManifests returns a string array
The helper in libs/portal-framework-core/src/util/pluginManifest.ts declares

const manifests: string[] = [];

and returns that array, so calling .map on its result is correct and won’t throw at runtime.

Likely an incorrect or invalid review comment.

libs/portal-framework-core/src/api/framework.spec.ts (2)

26-27: LGTM: centralizing the console.warn spy improves consistency.


187-190: LGTM: re-initialization warning expectation matches the new message.

libs/portal-framework-core/src/api/framework.ts (2)

116-122: Strict existence check for widget areas is good.

Throwing on missing areas surfaces configuration issues early.


124-132: Good: delegates ordering to sortWidgets.

This replaces fragile ad-hoc sorting and aligns with the new typed grid semantics.

libs/portal-framework-core/src/plugins/remoteComponentLoader.tsx (4)

68-76: Bridge path now safely handles absence of default export

Switching to module.default || module prevents runtime failures when the remote doesn’t have a default export. Good fix.


82-89: Default-loader path consistently handles non-default exports

Using module?.default ?? module mirrors the bridge path and avoids extraction errors. Looks good.


131-134: RenderFnParams: adding dom: HTMLElement is appropriate

This clarifies the host container for providers.


183-201: Nested-props migration not needed & any-cast defeats type safety

Upon inspection, there are no JSX call sites in this repo using the nested props={{…}} pattern—so migrating consumer code is unnecessary. The two remaining // @ts-ignore lines (in remoteComponentLoader.tsx at lines 191 and 194) should be addressed by tightening the RemoteComponentProps definition and forwarding the ref, rather than flattening everything to any, which would defeat the very type-safety this PR aims to introduce.

• No occurrences of props={{…}} in .tsx files—nested-props API is isolated to the loader itself
• Two // @ts-ignore in remoteComponentLoader.tsx—refine RemoteComponentProps (e.g. make inner props required or default to {}) and apply proper type annotations on <LazyComponent /> instead of casting to any
• The suggested diff replaces generics with any and removes the conditional, undermining type guarantees and should be rejected
• Consider either attaching the forwarded ref to a wrapper element or dropping it if unused, to avoid a misleading API

Likely an incorrect or invalid review comment.

…dget system with improved area management and grid layout

- Introduced new widget types and interfaces for better type safety
- Implemented proper grid layout management with column/row spanning
- Added widget area definitions and registration methods
- Updated WidgetArea component to use new grid system
- Improved error handling and validation for widget registration
- Added sorting by order and column position
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant