You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by vi...@apache.org on 2026/03/02 21:51:36 UTC

(superset) branch master updated: feat(extensions): code-first frontend contributions (#38346)

This is an automated email from the ASF dual-hosted git repository.

villebro pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new a74d32ab44f feat(extensions): code-first frontend contributions (#38346)
a74d32ab44f is described below

commit a74d32ab44fd2562fc904c26bb4f0147736970da
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Mon Mar 2 18:51:29 2026 -0300

    feat(extensions): code-first frontend contributions (#38346)
---
 docs/developer_docs/extensions/architecture.md     |   5 +-
 .../extensions/contribution-types.md               | 135 +++--
 docs/developer_docs/extensions/development.md      |  83 ++-
 docs/developer_docs/extensions/quick-start.md      |  63 +--
 .../src/superset_core/extensions/types.py          | 100 +---
 .../src/superset_extensions_cli/cli.py             |   8 +-
 .../templates/extension.json.j2                    |  14 -
 .../templates/frontend/src/index.tsx.j2            |  17 +-
 .../templates/frontend/webpack.config.js.j2        |   3 +-
 superset-extensions-cli/tests/test_cli_build.py    |  31 +-
 superset-extensions-cli/tests/test_cli_init.py     |  17 +-
 superset-extensions-cli/tests/test_templates.py    |  19 +-
 .../packages/superset-core/src/api/commands.ts     |  49 +-
 .../superset-core/src/api/contributions.ts         |  92 +---
 .../packages/superset-core/src/api/core.ts         |  74 +--
 .../packages/superset-core/src/api/editors.ts      |  55 +-
 .../packages/superset-core/src/api/index.ts        |   2 +
 .../packages/superset-core/src/api/menus.ts        | 102 ++++
 .../packages/superset-core/src/api/views.ts        |  88 ++++
 superset-frontend/spec/helpers/testing-library.tsx |   5 +-
 .../SqlLab/components/AppLayout/AppLayout.test.tsx |  79 +--
 .../src/SqlLab/components/AppLayout/index.tsx      |   9 +-
 .../src/SqlLab/components/SouthPane/index.tsx      |  20 +-
 .../SqlLab/components/StatusBar/StatusBar.test.tsx |  14 +-
 .../src/SqlLab/components/StatusBar/index.tsx      |   9 +-
 .../src/components/PanelToolbar/index.tsx          |  40 +-
 .../ViewListExtension/ViewListExtension.test.tsx   | 228 +++-----
 .../src/components/ViewListExtension/index.tsx     |  24 +-
 superset-frontend/src/core/commands/index.ts       |  42 +-
 superset-frontend/src/core/editors/EditorHost.tsx  |   8 +-
 .../src/core/editors/EditorProviders.test.ts       |  85 ++-
 .../src/core/editors/EditorProviders.ts            |  20 +-
 superset-frontend/src/core/editors/index.ts        |  36 +-
 superset-frontend/src/core/extensions/index.ts     |   6 +-
 superset-frontend/src/core/index.ts                |  14 +-
 superset-frontend/src/core/menus/index.test.ts     | 124 +++++
 superset-frontend/src/core/menus/index.ts          |  83 +++
 superset-frontend/src/core/models.ts               |   4 -
 superset-frontend/src/core/views/index.test.ts     | 112 ++++
 superset-frontend/src/core/views/index.ts          |  83 +++
 .../src/extensions/ExtensionsContext.test.tsx      | 150 ------
 .../src/extensions/ExtensionsContext.tsx           |  93 ----
 .../src/extensions/ExtensionsContextUtils.test.ts  |  74 ---
 .../src/extensions/ExtensionsContextUtils.ts       |  32 --
 .../src/extensions/ExtensionsList.test.tsx         |  17 -
 .../src/extensions/ExtensionsList.tsx              |  29 --
 .../src/extensions/ExtensionsLoader.test.ts        | 113 ++++
 .../src/extensions/ExtensionsLoader.ts             | 161 ++++++
 .../src/extensions/ExtensionsManager.test.ts       | 572 ---------------------
 .../src/extensions/ExtensionsManager.ts            | 355 -------------
 .../src/extensions/ExtensionsStartup.test.tsx      |  72 +--
 .../src/extensions/ExtensionsStartup.tsx           |  51 +-
 superset-frontend/src/views/App.tsx                |  45 +-
 .../src/views/RootContextProviders.tsx             |  17 +-
 superset/extensions/utils.py                       |   5 +-
 tests/unit_tests/extensions/test_types.py          |  59 +--
 56 files changed, 1437 insertions(+), 2410 deletions(-)

diff --git a/docs/developer_docs/extensions/architecture.md b/docs/developer_docs/extensions/architecture.md
index b13091bfa3d..568f8feff47 100644
--- a/docs/developer_docs/extensions/architecture.md
+++ b/docs/developer_docs/extensions/architecture.md
@@ -39,7 +39,7 @@ Superset's core should remain minimal, with many features delegated to extension
 
 ### 2. Explicit Contribution Points
 
-All extension points are clearly defined and documented. Extension authors know exactly where and how they can interact with the host system. Each extension declares its capabilities in a metadata file, enabling the host to:
+All extension points are clearly defined and documented. Extension authors know exactly where and how they can interact with the host system. Backend contributions are declared in `extension.json`. Frontend contributions are registered directly in code at module load time, giving the host clear visibility into what each extension provides:
 - Manage the extension lifecycle
 - Provide a consistent user experience
 - Validate extension compatibility
@@ -122,7 +122,6 @@ The Superset host application serves as the runtime environment for extensions:
 
 The extensions table contains:
 - Extension name, version, and author
-- Contributed features and exposed modules
 - Metadata and configuration
 - Built frontend and/or backend code
 
@@ -173,7 +172,7 @@ new ModuleFederationPlugin({
 
 This configuration does several important things:
 
-**`exposes`** - Declares which modules are available to the host application. The extension makes `./index` available as its entry point.
+**`exposes`** - Declares which modules are available to the host application. Superset always loads extensions by requesting the `./index` module from the remote container — this is a fixed convention, not a configurable value. Extensions must expose exactly `'./index': './src/index.tsx'` and place all API registrations (views, commands, menus, editors, event listeners) in that file. The module is executed as a side effect when the extension loads, so any call to `views.registerView`, `c [...]
 
 **`externals` and `externalsType`** - Tell Webpack that when the extension imports `@apache-superset/core`, it should use `window.superset` at runtime instead of bundling its own copy. This ensures extensions use the host's implementation of shared packages.
 
diff --git a/docs/developer_docs/extensions/contribution-types.md b/docs/developer_docs/extensions/contribution-types.md
index f802bd609cb..e6fddef7abd 100644
--- a/docs/developer_docs/extensions/contribution-types.md
+++ b/docs/developer_docs/extensions/contribution-types.md
@@ -28,100 +28,83 @@ To facilitate the development of extensions, we define a set of well-defined con
 
 ## Frontend
 
-Frontend contribution types allow extensions to extend Superset's user interface with new views, commands, and menu items.
+Frontend contribution types allow extensions to extend Superset's user interface with new views, commands, and menu items. Frontend contributions are registered directly in code from your extension's `index.tsx` entry point — they do not need to be declared in `extension.json`.
 
 ### Views
 
-Extensions can add new views or panels to the host application, such as custom SQL Lab panels, dashboards, or other UI components. Each view is registered with a unique ID and can be activated or deactivated as needed. Contribution areas are uniquely identified (e.g., `sqllab.panels` for SQL Lab panels), enabling seamless integration into specific parts of the application.
+Extensions can add new views or panels to the host application, such as custom SQL Lab panels, dashboards, or other UI components. Contribution areas are uniquely identified (e.g., `sqllab.panels` for SQL Lab panels), enabling seamless integration into specific parts of the application.
 
-```json
-"frontend": {
-  "contributions": {
-    "views": {
-      "sqllab": {
-        "panels": [
-          {
-            "id": "my_extension.main",
-            "name": "My Panel Name"
-          }
-        ]
-      }
-    }
-  }
-}
+```tsx
+import React from 'react';
+import { views } from '@apache-superset/core';
+import MyPanel from './MyPanel';
+
+views.registerView(
+  { id: 'my-extension.main', name: 'My Panel Name' },
+  'sqllab.panels',
+  () => <MyPanel />,
+);
 ```
 
 ### Commands
 
-Extensions can define custom commands that can be executed within the host application, such as context-aware actions or menu options. Each command can specify properties like a unique command identifier, an icon, a title, and a description. These commands can be invoked by users through menus, keyboard shortcuts, or other UI elements, enabling extensions to add rich, interactive functionality to Superset.
-
-```json
-"frontend": {
-  "contributions": {
-    "commands": [
-      {
-        "command": "my_extension.copy_query",
-        "icon": "CopyOutlined",
-        "title": "Copy Query",
-        "description": "Copy the current query to clipboard"
-      }
-    ]
-  }
-}
+Extensions can define custom commands that can be executed within the host application, such as context-aware actions or menu options. Each command specifies a unique identifier, a title, an optional icon, and a description. Commands can be invoked by users through menus, keyboard shortcuts, or other UI elements.
+
+```typescript
+import { commands } from '@apache-superset/core';
+
+commands.registerCommand(
+  {
+    id: 'my-extension.copy-query',
+    title: 'Copy Query',
+    icon: 'CopyOutlined',
+    description: 'Copy the current query to clipboard',
+  },
+  () => {
+    // Command implementation
+  },
+);
 ```
 
 ### Menus
 
-Extensions can contribute new menu items or context menus to the host application, providing users with additional actions and options. Each menu item can specify properties such as the target view, the command to execute, its placement (primary, secondary, or context), and conditions for when it should be displayed. Menu contribution areas are uniquely identified (e.g., `sqllab.editor` for the SQL Lab editor), allowing extensions to seamlessly integrate their functionality into specific [...]
+Extensions can contribute new menu items or context menus to the host application, providing users with additional actions and options. Each menu item specifies the target area, the command to execute, and its placement (primary, secondary, or context). Menu contribution areas are uniquely identified (e.g., `sqllab.editor` for the SQL Lab editor).
 
-```json
-"frontend": {
-  "contributions": {
-    "menus": {
-      "sqllab": {
-        "editor": {
-          "primary": [
-            {
-              "view": "builtin.editor",
-              "command": "my_extension.copy_query"
-            }
-          ],
-          "secondary": [
-            {
-              "view": "builtin.editor",
-              "command": "my_extension.prettify"
-            }
-          ],
-          "context": [
-            {
-              "view": "builtin.editor",
-              "command": "my_extension.clear"
-            }
-          ]
-        }
-      }
-    }
-  }
-}
+```typescript
+import { menus } from '@apache-superset/core';
+
+menus.addMenuItem('sqllab.editor', {
+  placement: 'primary',
+  command: 'my-extension.copy-query',
+});
+
+menus.addMenuItem('sqllab.editor', {
+  placement: 'secondary',
+  command: 'my-extension.prettify',
+});
+
+menus.addMenuItem('sqllab.editor', {
+  placement: 'context',
+  command: 'my-extension.clear',
+});
 ```
 
 ### Editors
 
-Extensions can replace Superset's default text editors with custom implementations. This enables enhanced editing experiences using alternative editor frameworks like Monaco, CodeMirror, or custom solutions. When an extension registers an editor for a language, it replaces the default Ace editor in all locations that use that language (SQL Lab, Dashboard Properties, CSS editors, etc.).
+Extensions can replace Superset's default text editors with custom implementations. This enables enhanced editing experiences using alternative editor frameworks like Monaco, CodeMirror, or custom solutions. When an extension registers an editor for a language, it replaces the default editor in all locations that use that language (SQL Lab, Dashboard Properties, CSS editors, etc.).
 
-```json
-"frontend": {
-  "contributions": {
-    "editors": [
-      {
-        "id": "my_extension.monaco_sql",
-        "name": "Monaco SQL Editor",
-        "languages": ["sql"],
-        "description": "Monaco-based SQL editor with IntelliSense"
-      }
-    ]
-  }
-}
+```typescript
+import { editors } from '@apache-superset/core';
+import MonacoSQLEditor from './MonacoSQLEditor';
+
+editors.registerEditor(
+  {
+    id: 'my-extension.monaco-sql',
+    name: 'Monaco SQL Editor',
+    languages: ['sql'],
+  },
+  MonacoSQLEditor,
+);
 ```
 
 See [Editors Extension Point](./extension-points/editors) for implementation details.
diff --git a/docs/developer_docs/extensions/development.md b/docs/developer_docs/extensions/development.md
index a927e44962e..57810965810 100644
--- a/docs/developer_docs/extensions/development.md
+++ b/docs/developer_docs/extensions/development.md
@@ -91,39 +91,24 @@ The `README.md` file provides documentation and instructions for using the exten
 
 ## Extension Metadata
 
-The `extension.json` file contains all metadata necessary for the host application to understand and manage the extension:
+The `extension.json` file contains the metadata necessary for the host application to identify and load the extension. Backend contributions (entry points and files) are declared here. Frontend contributions are registered directly in code from `frontend/src/index.tsx`.
 
 ```json
 {
-  "id": "dataset-references",
-  "name": "Dataset References",
+  "publisher": "my-org",
+  "name": "dataset-references",
+  "displayName": "Dataset References",
   "version": "1.0.0",
-  "frontend": {
-    "contributions": {
-      "views": {
-        "sqllab": {
-          "panels": [
-            {
-              "id": "dataset-references.main",
-              "name": "Dataset References"
-            }
-          ]
-        }
-      }
-    },
-    "moduleFederation": {
-      "exposes": ["./index"],
-      "name": "datasetReferences"
-    }
-  },
+  "license": "Apache-2.0",
   "backend": {
     "entryPoints": ["superset_extensions.dataset_references.entrypoint"],
     "files": ["backend/src/superset_extensions/dataset_references/**/*.py"]
-  }
+  },
+  "permissions": []
 }
 ```
 
-The `contributions` section declares how the extension extends Superset's functionality through views, commands, menus, and other contribution types. The `backend` section specifies entry points and files to include in the bundle.
+The `backend` section specifies Python entry points to load eagerly when the extension starts, and glob patterns for source files to include in the bundle.
 
 ## Interacting with the Host
 
@@ -154,34 +139,38 @@ export const onDidQueryStop: Event<QueryContext>;
 The following code demonstrates more examples of the existing frontend APIs:
 
 ```typescript
-import { core, commands, sqlLab, authentication, Button } from '@apache-superset/core';
+import React from 'react';
+import { views, commands, sqlLab, authentication, Button } from '@apache-superset/core';
 import MyPanel from './MyPanel';
 
-export function activate(context) {
-  // Register a new panel (view) in SQL Lab and use shared UI components in your extension's React code
-  const panelDisposable = core.registerView('my_extension.panel', <MyPanel><Button/></MyPanel>);
-
-  // Register a custom command
-  const commandDisposable = commands.registerCommand(
-    'my_extension.copy_query',
-    () => {
-      // Command logic here
-    },
-  );
-
-  // Listen for query run events in SQL Lab
-  const eventDisposable = sqlLab.onDidQueryRun(queryContext => {
-    console.log('Query started on database:', queryContext.tab.databaseId);
-  });
+// Register a new panel (view) in SQL Lab and use shared UI components in your extension's React code
+views.registerView(
+  { id: 'my-extension.panel', name: 'My Panel' },
+  'sqllab.panels',
+  () => <MyPanel><Button /></MyPanel>,
+);
+
+// Register a custom command
+commands.registerCommand(
+  {
+    id: 'my-extension.copy-query',
+    title: 'Copy Query',
+    description: 'Copy the current query to clipboard',
+  },
+  () => {
+    // Command logic here
+  },
+);
 
-  // Access a CSRF token for secure API requests
-  authentication.getCSRFToken().then(token => {
-    // Use token as needed
-  });
+// Listen for query run events in SQL Lab
+sqlLab.onDidQueryRun(queryContext => {
+  console.log('Query started on database:', queryContext.tab.databaseId);
+});
 
-  // Add all disposables for automatic cleanup on deactivation
-  context.subscriptions.push(panelDisposable, commandDisposable, eventDisposable);
-}
+// Access a CSRF token for secure API requests
+authentication.getCSRFToken().then(token => {
+  // Use token as needed
+});
 ```
 
 ### Backend APIs
diff --git a/docs/developer_docs/extensions/quick-start.md b/docs/developer_docs/extensions/quick-start.md
index f14e0be23b3..a3ac8390bdf 100644
--- a/docs/developer_docs/extensions/quick-start.md
+++ b/docs/developer_docs/extensions/quick-start.md
@@ -95,7 +95,7 @@ my-org.hello-world/
 
 ## Step 3: Configure Extension Metadata
 
-The generated `extension.json` contains basic metadata. Update it to register your panel in SQL Lab:
+The generated `extension.json` contains the extension's metadata. It is used to identify the extension and declare its backend entry points. Frontend contributions are registered directly in code (see Step 5).
 
 ```json
 {
@@ -104,24 +104,6 @@ The generated `extension.json` contains basic metadata. Update it to register yo
   "displayName": "Hello World",
   "version": "0.1.0",
   "license": "Apache-2.0",
-  "frontend": {
-    "contributions": {
-      "views": {
-        "sqllab": {
-          "panels": [
-            {
-              "id": "my-org.hello-world.main",
-              "name": "Hello World"
-            }
-          ]
-        }
-      }
-    },
-    "moduleFederation": {
-      "exposes": ["./index"],
-      "name": "myOrg_helloWorld"
-    }
-  },
   "backend": {
     "entryPoints": ["superset_extensions.my_org.hello_world.entrypoint"],
     "files": ["backend/src/superset_extensions/my_org/hello_world/**/*.py"]
@@ -130,15 +112,13 @@ The generated `extension.json` contains basic metadata. Update it to register yo
 }
 ```
 
-**Note**: The `moduleFederation.name` uses collision-safe naming (`myOrg_helloWorld`), and backend entry points use the full nested Python namespace (`superset_extensions.my_org.hello_world`).
-
 **Key fields:**
 
 - `publisher`: Organizational namespace for the extension
 - `name`: Technical identifier (kebab-case)
 - `displayName`: Human-readable name shown to users
-- `frontend.contributions.views.sqllab.panels`: Registers your panel in SQL Lab
-- `backend.entryPoints`: Python modules to load eagerly when extension starts
+- `backend.entryPoints`: Python modules to load eagerly when the extension starts
+- `backend.files`: Glob patterns for Python source files to include in the bundle
 
 ## Step 4: Create Backend API
 
@@ -247,7 +227,9 @@ The `@apache-superset/core` package must be listed in both `peerDependencies` (t
 
 **`frontend/webpack.config.js`**
 
-The webpack configuration requires specific settings for Module Federation. Key settings include `externalsType: "window"` and `externals` to map `@apache-superset/core` to `window.superset` at runtime, `import: false` for shared modules to use the host's React instead of bundling a separate copy, and `remoteEntry.[contenthash].js` for cache busting:
+The webpack configuration requires specific settings for Module Federation. Key settings include `externalsType: "window"` and `externals` to map `@apache-superset/core` to `window.superset` at runtime, `import: false` for shared modules to use the host's React instead of bundling a separate copy, and `remoteEntry.[contenthash].js` for cache busting.
+
+**Convention**: Superset always loads extensions by requesting the `./index` module from the Module Federation container. The `exposes` entry must be exactly `'./index': './src/index.tsx'` — do not rename or add additional entries. All API registrations must be reachable from that file. See [Architecture](./architecture#module-federation) for a full explanation.
 
 ```javascript
 const path = require("path");
@@ -413,29 +395,26 @@ export default HelloWorldPanel;
 
 **Update `frontend/src/index.tsx`**
 
-Replace the generated code with the extension entry point:
+This file is the single entry point Superset loads from every extension. All registrations — views, commands, menus, editors, event listeners — must be made here (or imported and executed from here). Replace the generated code with:
 
 ```tsx
 import React from 'react';
-import { core } from '@apache-superset/core';
+import { views } from '@apache-superset/core';
 import HelloWorldPanel from './HelloWorldPanel';
 
-export const activate = (context: core.ExtensionContext) => {
-  context.disposables.push(
-    core.registerViewProvider('my-org.hello-world.main', () => <HelloWorldPanel />),
-  );
-};
-
-export const deactivate = () => {};
+views.registerView(
+  { id: 'my-org.hello-world.main', name: 'Hello World' },
+  'sqllab.panels',
+  () => <HelloWorldPanel />,
+);
 ```
 
 **Key patterns:**
 
-- `activate` function is called when the extension loads
-- `core.registerViewProvider` registers the component with ID `my-org.hello-world.main` (matching `extension.json`)
-- `authentication.getCSRFToken()` retrieves the CSRF token for API calls
+- `views.registerView` is called at module load time — no `activate`/`deactivate` lifecycle needed
+- The first argument is a `{ id, name }` descriptor; the second is the contribution area (e.g., `sqllab.panels`); the third is a factory returning the React component
+- `authentication.getCSRFToken()` retrieves the CSRF token for API calls (used inside components)
 - Fetch calls to `/extensions/{publisher}/{name}/{endpoint}` reach your backend API
-- `context.disposables.push()` ensures proper cleanup
 
 ## Step 6: Install Dependencies
 
@@ -513,15 +492,15 @@ Superset will extract and validate the extension metadata, load the assets, regi
 
 Here's what happens when your extension loads:
 
-1. **Superset starts**: Reads `extension.json` and loads backend entrypoint
+1. **Superset starts**: Reads `extension.json` and loads the backend entrypoint
 2. **Backend registration**: `entrypoint.py` registers your API via `rest_api.add_extension_api()`
 3. **Frontend loads**: When SQL Lab opens, Superset fetches the remote entry file
-4. **Module Federation**: Webpack loads your extension code and resolves `@apache-superset/core` to `window.superset`
-5. **Activation**: `activate()` is called, registering your view provider
+4. **Module Federation**: Webpack loads your extension module and resolves `@apache-superset/core` to `window.superset`
+5. **Registration**: The module executes at load time, calling `views.registerView` to register your panel
 6. **Rendering**: When the user opens your panel, React renders `<HelloWorldPanel />`
-7. **API call**: Component fetches data from `/extensions/my-org/hello-world/message`
+7. **API call**: The component fetches data from `/extensions/my-org/hello-world/message`
 8. **Backend response**: Your Flask API returns the hello world message
-9. **Display**: Component shows the message to the user
+9. **Display**: The component shows the message to the user
 
 ## Next Steps
 
diff --git a/superset-core/src/superset_core/extensions/types.py b/superset-core/src/superset_core/extensions/types.py
index 49f3eefe749..fc24576063b 100644
--- a/superset-core/src/superset_core/extensions/types.py
+++ b/superset-core/src/superset_core/extensions/types.py
@@ -25,8 +25,6 @@ Two distinct schemas:
 
 from __future__ import annotations
 
-from typing import Any
-
 from pydantic import BaseModel, Field  # noqa: I001
 
 from superset_core.extensions.constants import (
@@ -36,75 +34,6 @@ from superset_core.extensions.constants import (
     VERSION_PATTERN,
 )
 
-# =============================================================================
-# Shared components
-# =============================================================================
-
-
-class ModuleFederationConfig(BaseModel):
-    """Configuration for Webpack Module Federation."""
-
-    name: str | None = Field(
-        default=None,
-        description="Module Federation container name "
-        "(must be valid JavaScript identifier)",
-    )
-    exposes: list[str] = Field(
-        default_factory=list,
-        description="Modules exposed by this extension",
-    )
-    filename: str = Field(
-        default="remoteEntry.js",
-        description="Remote entry filename",
-    )
-    shared: dict[str, Any] = Field(
-        default_factory=dict,
-        description="Shared dependencies configuration",
-    )
-    remotes: dict[str, str] = Field(
-        default_factory=dict,
-        description="Remote module references",
-    )
-
-
-class ContributionConfig(BaseModel):
-    """Configuration for frontend UI contributions.
-
-    Views and menus use a nested structure: type -> scope -> location -> contributions.
-
-    Example:
-        {
-            "views": {
-                "sqllab": {
-                    "panels": [{"id": "my-ext.panel", "name": "My Panel"}],
-                    "leftSidebar": [{"id": "my-ext.sidebar", "name": "Sidebar"}]
-                }
-            },
-            "menus": {
-                "sqllab": {
-                    "editor": {"primary": [...], "secondary": [...]}
-                }
-            }
-        }
-    """
-
-    commands: list[dict[str, Any]] = Field(
-        default_factory=list,
-        description="Command contributions",
-    )
-    views: dict[str, dict[str, list[dict[str, Any]]]] = Field(
-        default_factory=dict,
-        description="View contributions by scope and location",
-    )
-    menus: dict[str, dict[str, Any]] = Field(
-        default_factory=dict,
-        description="Menu contributions by scope and location",
-    )
-    editors: list[dict[str, Any]] = Field(
-        default_factory=list,
-        description="Editor contributions",
-    )
-
 
 class BaseExtension(BaseModel):
     """Base fields shared by ExtensionConfig and Manifest."""
@@ -155,19 +84,6 @@ class BaseExtension(BaseModel):
 # =============================================================================
 
 
-class ExtensionConfigFrontend(BaseModel):
-    """Frontend section in extension.json."""
-
-    contributions: ContributionConfig = Field(
-        default_factory=ContributionConfig,
-        description="UI contribution points",
-    )
-    moduleFederation: ModuleFederationConfig = Field(  # noqa: N815
-        default_factory=ModuleFederationConfig,
-        description="Module Federation configuration",
-    )
-
-
 class ExtensionConfigBackend(BaseModel):
     """Backend section in extension.json."""
 
@@ -188,10 +104,6 @@ class ExtensionConfig(BaseExtension):
     This file is authored by developers to define extension metadata.
     """
 
-    frontend: ExtensionConfigFrontend | None = Field(
-        default=None,
-        description="Frontend configuration",
-    )
     backend: ExtensionConfigBackend | None = Field(
         default=None,
         description="Backend configuration",
@@ -206,18 +118,14 @@ class ExtensionConfig(BaseExtension):
 class ManifestFrontend(BaseModel):
     """Frontend section in manifest.json."""
 
-    contributions: ContributionConfig = Field(
-        default_factory=ContributionConfig,
-        description="UI contribution points",
-    )
-    moduleFederation: ModuleFederationConfig = Field(  # noqa: N815
-        default_factory=ModuleFederationConfig,
-        description="Module Federation configuration",
-    )
     remoteEntry: str = Field(  # noqa: N815
         ...,
         description="Path to the built remote entry file",
     )
+    moduleFederationName: str = Field(  # noqa: N815
+        ...,
+        description="Webpack Module Federation container name (maps to window[name])",
+    )
 
 
 class ManifestBackend(BaseModel):
diff --git a/superset-extensions-cli/src/superset_extensions_cli/cli.py b/superset-extensions-cli/src/superset_extensions_cli/cli.py
index b26a0eb87d3..13c9d3f60a8 100644
--- a/superset-extensions-cli/src/superset_extensions_cli/cli.py
+++ b/superset-extensions-cli/src/superset_extensions_cli/cli.py
@@ -42,6 +42,7 @@ from superset_extensions_cli.exceptions import ExtensionNameError
 from superset_extensions_cli.types import ExtensionNames
 from superset_extensions_cli.utils import (
     generate_extension_names,
+    get_module_federation_name,
     kebab_to_snake_case,
     read_json,
     read_toml,
@@ -152,11 +153,12 @@ def build_manifest(cwd: Path, remote_entry: str | None) -> Manifest:
     composite_id = f"{extension.publisher}.{extension.name}"
 
     frontend: ManifestFrontend | None = None
-    if extension.frontend and remote_entry:
+    if remote_entry:
         frontend = ManifestFrontend(
-            contributions=extension.frontend.contributions,
-            moduleFederation=extension.frontend.moduleFederation,
             remoteEntry=remote_entry,
+            moduleFederationName=get_module_federation_name(
+                extension.publisher, extension.name
+            ),
         )
 
     backend: ManifestBackend | None = None
diff --git a/superset-extensions-cli/src/superset_extensions_cli/templates/extension.json.j2 b/superset-extensions-cli/src/superset_extensions_cli/templates/extension.json.j2
index 08410b6d6d6..dc750a22545 100644
--- a/superset-extensions-cli/src/superset_extensions_cli/templates/extension.json.j2
+++ b/superset-extensions-cli/src/superset_extensions_cli/templates/extension.json.j2
@@ -4,20 +4,6 @@
   "displayName": "{{ display_name }}",
   "version": "{{ version }}",
   "license": "{{ license }}",
-  {% if include_frontend -%}
-  "frontend": {
-    "contributions": {
-      "commands": [],
-      "views": {},
-      "menus": {},
-      "editors": []
-    },
-    "moduleFederation": {
-      "name": "{{ mf_name }}",
-      "exposes": ["./index"]
-    }
-  },
-  {% endif -%}
   {% if include_backend -%}
   "backend": {
     "entryPoints": ["{{ backend_entry }}"],
diff --git a/superset-extensions-cli/src/superset_extensions_cli/templates/frontend/src/index.tsx.j2 b/superset-extensions-cli/src/superset_extensions_cli/templates/frontend/src/index.tsx.j2
index b9a6266216d..582ba518519 100644
--- a/superset-extensions-cli/src/superset_extensions_cli/templates/frontend/src/index.tsx.j2
+++ b/superset-extensions-cli/src/superset_extensions_cli/templates/frontend/src/index.tsx.j2
@@ -1,13 +1,8 @@
 import React from "react";
-import { core } from "@apache-superset/core";
+import { views } from "@apache-superset/core";
 
-export const activate = (context: core.ExtensionContext) => {
-  context.disposables.push(
-    core.registerViewProvider("{{ id }}.example", () => <p>{{ name }}</p>)
-  );
-  console.log("{{ name }} extension activated");
-};
-
-export const deactivate = () => {
-  console.log("{{ name }} extension deactivated");
-};
+views.registerView(
+  { id: "{{ id }}.example", name: "{{ display_name }}" },
+  "sqllab.panels",
+  () => <p>{{ display_name }}</p>,
+);
diff --git a/superset-extensions-cli/src/superset_extensions_cli/templates/frontend/webpack.config.js.j2 b/superset-extensions-cli/src/superset_extensions_cli/templates/frontend/webpack.config.js.j2
index b3bf5139899..93fc61459fb 100644
--- a/superset-extensions-cli/src/superset_extensions_cli/templates/frontend/webpack.config.js.j2
+++ b/superset-extensions-cli/src/superset_extensions_cli/templates/frontend/webpack.config.js.j2
@@ -1,6 +1,7 @@
 const path = require("path");
 const { ModuleFederationPlugin } = require("webpack").container;
 const packageConfig = require("./package");
+const extensionConfig = require("../extension.json");
 
 module.exports = (env, argv) => {
   const isProd = argv.mode === "production";
@@ -19,7 +20,7 @@ module.exports = (env, argv) => {
       filename: isProd ? undefined : "[name].[contenthash].js",
       chunkFilename: "[name].[contenthash].js",
       path: path.resolve(__dirname, "dist"),
-      publicPath: `/api/v1/extensions/{{ publisher }}/{{ name }}/`,
+      publicPath: `/api/v1/extensions/${extensionConfig.publisher}/${extensionConfig.name}/`,
     },
     resolve: {
       extensions: [".ts", ".tsx", ".js", ".jsx"],
diff --git a/superset-extensions-cli/tests/test_cli_build.py b/superset-extensions-cli/tests/test_cli_build.py
index 94116e76b5a..2e95ff4f3c1 100644
--- a/superset-extensions-cli/tests/test_cli_build.py
+++ b/superset-extensions-cli/tests/test_cli_build.py
@@ -59,20 +59,6 @@ def extension_with_build_structure():
             "permissions": [],
         }
 
-        if include_frontend:
-            extension_json["frontend"] = {
-                "contributions": {
-                    "commands": [],
-                    "views": {},
-                    "menus": {},
-                    "editors": [],
-                },
-                "moduleFederation": {
-                    "exposes": ["./index"],
-                    "name": "testOrg_testExtension",
-                },
-            }
-
         if include_backend:
             extension_json["backend"] = {
                 "entryPoints": [
@@ -249,18 +235,6 @@ def test_build_manifest_creates_correct_manifest_structure(isolated_filesystem):
         "version": "1.0.0",
         "permissions": ["read_data"],
         "dependencies": ["some_dep"],
-        "frontend": {
-            "contributions": {
-                "commands": [{"id": "test_command", "title": "Test"}],
-                "views": {},
-                "menus": {},
-                "editors": [],
-            },
-            "moduleFederation": {
-                "exposes": ["./index"],
-                "name": "testOrg_testExtension",
-            },
-        },
         "backend": {
             "entryPoints": ["superset_extensions.test_org.test_extension.entrypoint"]
         },
@@ -281,11 +255,8 @@ def test_build_manifest_creates_correct_manifest_structure(isolated_filesystem):
 
     # Verify frontend section
     assert manifest.frontend is not None
-    assert manifest.frontend.contributions.commands == [
-        {"id": "test_command", "title": "Test"}
-    ]
-    assert manifest.frontend.moduleFederation.exposes == ["./index"]
     assert manifest.frontend.remoteEntry == "remoteEntry.abc123.js"
+    assert manifest.frontend.moduleFederationName == "testOrg_testExtension"
 
     # Verify backend section
     assert manifest.backend is not None
diff --git a/superset-extensions-cli/tests/test_cli_init.py b/superset-extensions-cli/tests/test_cli_init.py
index 7d9d5f284bd..ce9fa963b6a 100644
--- a/superset-extensions-cli/tests/test_cli_init.py
+++ b/superset-extensions-cli/tests/test_cli_init.py
@@ -223,21 +223,8 @@ def test_extension_json_content_is_correct(
     # Load and verify more complex nested structures
     content = load_json_file(extension_json_path)
 
-    # Verify frontend section exists and has correct structure
-    assert "frontend" in content
-    frontend = content["frontend"]
-    assert "contributions" in frontend
-    assert "moduleFederation" in frontend
-    assert frontend["contributions"] == {
-        "commands": [],
-        "views": {},
-        "menus": {},
-        "editors": [],
-    }
-    assert frontend["moduleFederation"] == {
-        "exposes": ["./index"],
-        "name": "testOrg_testExtension",
-    }
+    # Verify frontend section is not present (contributions are code-first)
+    assert "frontend" not in content
 
     # Verify backend section exists and has correct structure
     assert "backend" in content
diff --git a/superset-extensions-cli/tests/test_templates.py b/superset-extensions-cli/tests/test_templates.py
index 29858e494a3..80dee4fae09 100644
--- a/superset-extensions-cli/tests/test_templates.py
+++ b/superset-extensions-cli/tests/test_templates.py
@@ -78,21 +78,8 @@ def test_extension_json_template_renders_with_both_frontend_and_backend(
     assert parsed["license"] == "Apache-2.0"
     assert parsed["permissions"] == []
 
-    # Verify frontend section exists
-    assert "frontend" in parsed
-    frontend = parsed["frontend"]
-    assert "contributions" in frontend
-    assert "moduleFederation" in frontend
-    assert frontend["contributions"] == {
-        "commands": [],
-        "views": {},
-        "menus": {},
-        "editors": [],
-    }
-    assert frontend["moduleFederation"] == {
-        "exposes": ["./index"],
-        "name": "testOrg_testExtension",
-    }
+    # Verify frontend section is not present (contributions are code-first)
+    assert "frontend" not in parsed
 
     # Verify backend section exists
     assert "backend" in parsed
@@ -109,7 +96,7 @@ def test_extension_json_template_renders_with_both_frontend_and_backend(
 @pytest.mark.parametrize(
     "include_frontend,include_backend,expected_sections",
     [
-        (True, False, ["frontend"]),
+        (True, False, []),
         (False, True, ["backend"]),
         (False, False, []),
     ],
diff --git a/superset-frontend/packages/superset-core/src/api/commands.ts b/superset-frontend/packages/superset-core/src/api/commands.ts
index 523f55238aa..f00aa3a4812 100644
--- a/superset-frontend/packages/superset-core/src/api/commands.ts
+++ b/superset-frontend/packages/superset-core/src/api/commands.ts
@@ -28,19 +28,40 @@
 import { Disposable } from './core';
 
 /**
- * Registers a command that can be invoked via a keyboard shortcut,
- * a menu item, an action, or directly.
+ * Describes a command that can be contributed to the application.
+ */
+export interface Command {
+  /** The unique identifier for the command. */
+  id: string;
+  /** The display title of the command. */
+  title: string;
+  /** The icon associated with the command. */
+  icon?: string;
+  /** A description of what the command does. */
+  description: string;
+}
+
+/**
+ * Registers a command with its handler as a module-level side effect.
  *
  * Registering a command with an existing command identifier twice
- * will cause an error.
+ * will cause a warning and overwrite the existing command.
  *
- * @param command A unique identifier for the command.
+ * @param command The command descriptor.
  * @param callback A command handler function.
  * @param thisArg The `this` context used when invoking the handler function.
  * @returns Disposable which unregisters this command on disposal.
+ *
+ * @example
+ * ```typescript
+ * commands.registerCommand(
+ *   { id: 'sqllab_parquet.export', title: 'Export to Parquet', icon: 'FileOutlined', description: 'Export results to Parquet format' },
+ *   async () => { exportToParquet(); },
+ * );
+ * ```
  */
 export declare function registerCommand(
-  command: string,
+  command: Command,
   callback: (...args: any[]) => any,
   thisArg?: any,
 ): Disposable;
@@ -59,12 +80,16 @@ export declare function executeCommand<T = unknown>(
 ): Promise<T>;
 
 /**
- * Retrieve the list of all available commands. Commands starting with an underscore are
- * treated as internal commands.
+ * Retrieve all registered commands.
+ *
+ * @returns An array of Command objects for all registered commands.
+ */
+export declare function getCommands(): Command[];
+
+/**
+ * Retrieve a specific command.
  *
- * @param filterInternal Set `true` to not see internal commands (starting with an underscore)
- * @returns Promise that resolves to a list of command ids.
+ * @param id The command identifier to look up.
+ * @returns The Command or undefined if not found.
  */
-export declare function getCommands(
-  filterInternal?: boolean,
-): Promise<string[]>;
+export declare function getCommand(id: string): Command | undefined;
diff --git a/superset-frontend/packages/superset-core/src/api/contributions.ts b/superset-frontend/packages/superset-core/src/api/contributions.ts
index 21b9c67fee3..6aec1bd0122 100644
--- a/superset-frontend/packages/superset-core/src/api/contributions.ts
+++ b/superset-frontend/packages/superset-core/src/api/contributions.ts
@@ -18,80 +18,18 @@
  */
 
 /**
- * @fileoverview Contributions API for Superset extension UI integration.
+ * @fileoverview Manifest schema for Superset extension contributions.
  *
- * This module defines the interfaces and types for extension contributions to the
- * Superset user interface. Extensions use these contribution types to register
- * commands, menu items, and custom views that integrate seamlessly with the
- * Superset platform. The contribution system allows extensions to extend the
- * application's functionality while maintaining a consistent user experience.
+ * This module defines the aggregate interfaces used by the extension.json
+ * manifest and the `superset-extensions` build command. Individual metadata
+ * types are defined in their respective namespace modules (commands, views,
+ * menus, editors) and re-exported here for the manifest schema.
  */
 
-/**
- * Describes a command that can be contributed to the application.
- */
-export interface CommandContribution {
-  /** The unique identifier for the command. */
-  command: string;
-  /** The icon associated with the command. */
-  icon: string;
-  /** The display title of the command. */
-  title: string;
-  /** A description of what the command does. */
-  description: string;
-}
-
-/**
- * Represents a menu item that links a view to a command.
- */
-export interface MenuItem {
-  /** The identifier of the view associated with this menu item. */
-  view: string;
-  /** The command to execute when this menu item is selected. */
-  command: string;
-}
-
-/**
- * Defines the structure of menu contributions, allowing for primary, secondary, and context menus.
- */
-export interface MenuContribution {
-  /** Items to appear in the primary menu. */
-  primary?: MenuItem[];
-  /** Items to appear in the secondary menu. */
-  secondary?: MenuItem[];
-  /** Items to appear in the context menu. */
-  context?: MenuItem[];
-}
-
-/**
- * Represents a contributed view in the application.
- */
-export interface ViewContribution {
-  /** The unique identifier for the view. */
-  id: string;
-  /** The display name of the view. */
-  name: string;
-}
-
-/**
- * Describes an editor that can be contributed to the application.
- * Extensions declare editor contributions in their extension.json manifest.
- */
-export interface EditorContribution {
-  /** Unique identifier for the editor (e.g., "acme.monaco-sql") */
-  id: string;
-  /** Display name of the editor */
-  name: string;
-  /** Languages this editor supports */
-  languages: EditorLanguage[];
-  /** Optional description of the editor */
-  description?: string;
-}
-
-/**
- * Supported editor languages.
- */
-export type EditorLanguage = 'sql' | 'json' | 'yaml' | 'markdown' | 'css';
+import { Command } from './commands';
+import { View } from './views';
+import { Menu } from './menus';
+import { Editor } from './editors';
 
 /**
  * Valid locations within SQL Lab.
@@ -116,7 +54,7 @@ export type SqlLabLocation =
  * }
  */
 export interface ViewContributions {
-  sqllab?: Partial<Record<SqlLabLocation, ViewContribution[]>>;
+  sqllab?: Partial<Record<SqlLabLocation, View[]>>;
 }
 
 /**
@@ -129,19 +67,19 @@ export interface ViewContributions {
  * }
  */
 export interface MenuContributions {
-  sqllab?: Partial<Record<SqlLabLocation, MenuContribution>>;
+  sqllab?: Partial<Record<SqlLabLocation, Menu>>;
 }
 
 /**
  * Aggregates all contributions (commands, menus, views, and editors) provided by an extension or module.
  */
 export interface Contributions {
-  /** List of command contributions. */
-  commands: CommandContribution[];
+  /** List of commands. */
+  commands: Command[];
   /** Nested mapping of menu contributions by scope and location. */
   menus: MenuContributions;
   /** Nested mapping of view contributions by scope and location. */
   views: ViewContributions;
-  /** List of editor contributions. */
-  editors?: EditorContribution[];
+  /** List of editors. */
+  editors?: Editor[];
 }
diff --git a/superset-frontend/packages/superset-core/src/api/core.ts b/superset-frontend/packages/superset-core/src/api/core.ts
index 03c5f7c358d..cc399cf6275 100644
--- a/superset-frontend/packages/superset-core/src/api/core.ts
+++ b/superset-frontend/packages/superset-core/src/api/core.ts
@@ -25,9 +25,6 @@
  * resource management, and extension lifecycle definitions.
  */
 
-import { ReactElement } from 'react';
-import { Contributions } from './contributions';
-
 /**
  * Represents a database column with its name and data type.
  */
@@ -217,22 +214,15 @@ export declare interface Event<T> {
 }
 
 /**
- * Represents a Superset extension with its metadata and lifecycle methods.
- * Extensions are modular components that can extend Superset's functionality.
+ * Represents a Superset extension with its metadata.
+ * Extensions are modular components that can extend Superset's functionality
+ * by registering commands, views, menus, and editors as module-level side effects.
  */
 export interface Extension {
-  /** Function called when the extension is activated */
-  activate: Function;
-  /** UI contributions provided by this extension */
-  contributions: Contributions;
-  /** Function called when the extension is deactivated */
-  deactivate: Function;
   /** List of other extensions that this extension depends on */
   dependencies: string[];
   /** Human-readable description of the extension */
   description: string;
-  /** List of modules exposed by this extension for use by other extensions */
-  exposedModules: string[];
   /** List of other extensions that this extension depends on */
   extensionDependencies: string[];
   /** Unique identifier for the extension */
@@ -244,61 +234,3 @@ export interface Extension {
   /** Version of the extension */
   version: string;
 }
-
-/**
- * Context object provided to extensions during activation.
- * Contains utilities and resources that extensions can use during their lifecycle.
- */
-export interface ExtensionContext {
-  /**
-   * Array of disposable objects that will be automatically disposed when the extension is deactivated.
-   * Extensions should add any resources that need cleanup to this array.
-   *
-   * @example
-   * ```typescript
-   * export function activate(context: ExtensionContext) {
-   *   // Register an event listener
-   *   const disposable = onSomeEvent(() => { ... });
-   *
-   *   // Add to context so it's cleaned up automatically
-   *   context.disposables.push(disposable);
-   * }
-   * ```
-   */
-  disposables: Disposable[];
-
-  /**
-   * @todo We might want to add more properties to this interface in the future like
-   * storage, configuration, logging, etc. For now, it serves as a placeholder
-   * to allow for future extensibility without breaking existing extensions.
-   */
-}
-
-/**
- * Registers a view provider that can render custom React components in Superset.
- * View providers allow extensions to contribute custom UI components that can be
- * displayed in various parts of the Superset interface.
- *
- * @param id Unique identifier for the view provider. This ID is used to reference
- *   the view provider from other parts of the system.
- * @param viewProvider Function that returns a React element to be rendered.
- *   This function will be called whenever the view needs to be displayed.
- * @returns A Disposable object that can be used to unregister the view provider.
- *
- * @example
- * ```typescript
- * const disposable = registerViewProvider('my-extension.custom-view', () => (
- *   <div>
- *     <h1>My Custom View</h1>
- *     <p>This is a custom component from my extension.</p>
- *   </div>
- * ));
- *
- * // Later, unregister the view provider
- * disposable.dispose();
- * ```
- */
-export declare const registerViewProvider: (
-  id: string,
-  viewProvider: () => ReactElement,
-) => Disposable;
diff --git a/superset-frontend/packages/superset-core/src/api/editors.ts b/superset-frontend/packages/superset-core/src/api/editors.ts
index cc57dde3073..334f28a49e1 100644
--- a/superset-frontend/packages/superset-core/src/api/editors.ts
+++ b/superset-frontend/packages/superset-core/src/api/editors.ts
@@ -34,12 +34,27 @@
  */
 
 import { ForwardRefExoticComponent, RefAttributes } from 'react';
-import { EditorContribution, EditorLanguage } from './contributions';
 import { Disposable, Event } from './core';
 import type { SupersetTheme } from '../ui';
 
-// Re-export contribution types for convenience
-export type { EditorContribution, EditorLanguage };
+/**
+ * Supported editor languages.
+ */
+export type EditorLanguage = 'sql' | 'json' | 'yaml' | 'markdown' | 'css';
+
+/**
+ * Describes an editor that can be contributed to the application.
+ */
+export interface Editor {
+  /** Unique identifier for the editor (e.g., "acme.monaco-sql") */
+  id: string;
+  /** Display name of the editor */
+  name: string;
+  /** Languages this editor supports */
+  languages: EditorLanguage[];
+  /** Optional description of the editor */
+  description?: string;
+}
 
 /**
  * Represents a position in the editor (line and column).
@@ -468,11 +483,11 @@ export type EditorComponent = ForwardRefExoticComponent<
 >;
 
 /**
- * A registered editor provider with its contribution metadata and component.
+ * A registered editor provider with its descriptor and component.
  */
 export interface EditorProvider {
-  /** The editor contribution metadata */
-  contribution: EditorContribution;
+  /** The editor descriptor */
+  editor: Editor;
   /** The React component implementing the editor */
   component: EditorComponent;
 }
@@ -489,33 +504,29 @@ export interface EditorProviderRegisteredEvent {
  * Event fired when an editor provider is unregistered.
  */
 export interface EditorProviderUnregisteredEvent {
-  /** The contribution that was unregistered */
-  contribution: EditorContribution;
+  /** The descriptor of the editor that was unregistered */
+  editor: Editor;
 }
 
 /**
- * Register an editor provider for specific languages.
- * When an extension registers an editor, it replaces the default for those languages.
+ * Registers a custom editor provider as a module-level side effect.
  *
- * The contribution metadata (name, languages, description) is read from the
- * extension's manifest (extension.json), so only the contribution ID and
- * component are needed at registration time.
+ * When an extension registers an editor, it replaces the default for those languages.
  *
- * @param id The editor contribution ID declared in extension.json
- * @param component The React component implementing EditorProps
- * @returns A Disposable to unregister the provider
+ * @param editor The editor descriptor including id, name, and languages.
+ * @param component The React component implementing the editor.
+ * @returns Disposable which unregisters this editor provider on disposal.
  *
  * @example
  * ```typescript
- * const disposable = registerEditorProvider(
- *   'acme.monaco-sql',
- *   MonacoSQLEditor
+ * editors.registerEditor(
+ *   { id: 'editors_bundle.monaco_sql', name: 'Monaco SQL Editor', languages: ['sql'] },
+ *   MonacoSQLEditor,
  * );
- * context.disposables.push(disposable);
  * ```
  */
-export declare function registerEditorProvider(
-  id: string,
+export declare function registerEditor(
+  editor: Editor,
   component: EditorComponent,
 ): Disposable;
 
diff --git a/superset-frontend/packages/superset-core/src/api/index.ts b/superset-frontend/packages/superset-core/src/api/index.ts
index d0df8513d16..0fbe91a7bf3 100644
--- a/superset-frontend/packages/superset-core/src/api/index.ts
+++ b/superset-frontend/packages/superset-core/src/api/index.ts
@@ -39,4 +39,6 @@ export * as contributions from './contributions';
 export * as core from './core';
 export * as editors from './editors';
 export * as extensions from './extensions';
+export * as menus from './menus';
 export * as sqlLab from './sqlLab';
+export * as views from './views';
diff --git a/superset-frontend/packages/superset-core/src/api/menus.ts b/superset-frontend/packages/superset-core/src/api/menus.ts
new file mode 100644
index 00000000000..8580af6ae2b
--- /dev/null
+++ b/superset-frontend/packages/superset-core/src/api/menus.ts
@@ -0,0 +1,102 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * @fileoverview Menus registration API for Superset extensions.
+ *
+ * This module provides functions for registering menu items at specific
+ * locations in the Superset UI. Menu items link commands to views and
+ * are organized into primary, secondary, and context menu groups.
+ *
+ * @example
+ * ```typescript
+ * import { menus } from '@apache-superset/core';
+ *
+ * menus.registerMenuItem({
+ *   command: 'sqllab_parquet.export',
+ *   view: 'builtin.editor',
+ *   location: 'sqllab.editor',
+ *   group: 'secondary',
+ * });
+ * ```
+ */
+
+import { Disposable } from './core';
+
+/**
+ * Represents a menu item that links a view to a command.
+ */
+export interface MenuItem {
+  /** The identifier of the view associated with this menu item. */
+  view: string;
+  /** The command to execute when this menu item is selected. */
+  command: string;
+}
+
+/**
+ * Defines the structure of registered menu items, organized into primary, secondary, and context menus.
+ */
+export interface Menu {
+  /** Items to appear in the primary menu. */
+  primary?: MenuItem[];
+  /** Items to appear in the secondary menu. */
+  secondary?: MenuItem[];
+  /** Items to appear in the context menu. */
+  context?: MenuItem[];
+}
+
+/**
+ * Registers a menu item at a specific UI location.
+ *
+ * @param item The menu item linking a view to a command.
+ * @param location The location where this menu item should appear (e.g. "sqllab.editor").
+ * @param group The menu group to place this item in.
+ * @returns A Disposable that unregisters the menu item when disposed.
+ *
+ * @example
+ * ```typescript
+ * menus.registerMenuItem(
+ *   { command: 'sqllab_parquet.export', view: 'builtin.editor' },
+ *   'sqllab.editor',
+ *   'secondary',
+ * );
+ * ```
+ */
+export declare function registerMenuItem(
+  item: MenuItem,
+  location: string,
+  group: 'primary' | 'secondary' | 'context',
+): Disposable;
+
+/**
+ * Retrieves all menu items registered at a specific location,
+ * grouped by primary, secondary, and context.
+ *
+ * @param location The location to retrieve registered menu items for (e.g. "sqllab.editor").
+ * @returns A Menu object, or undefined if none are registered.
+ *
+ * @example
+ * ```typescript
+ * const editorMenus = menus.getMenu('sqllab.editor');
+ * if (editorMenus?.secondary) {
+ *   // render secondary menu items
+ * }
+ * ```
+ */
+export declare function getMenu(location: string): Menu | undefined;
diff --git a/superset-frontend/packages/superset-core/src/api/views.ts b/superset-frontend/packages/superset-core/src/api/views.ts
new file mode 100644
index 00000000000..26c3a0eb4e2
--- /dev/null
+++ b/superset-frontend/packages/superset-core/src/api/views.ts
@@ -0,0 +1,88 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * @fileoverview Views registration API for Superset extensions.
+ *
+ * This module provides functions for registering custom React views
+ * at specific locations in the Superset UI. Views are registered as
+ * module-level side effects at import time.
+ *
+ * @example
+ * ```typescript
+ * import { views } from '@apache-superset/core';
+ *
+ * views.registerView(
+ *   { id: 'my_ext.result_stats', name: 'Result Stats', location: 'sqllab.panels' },
+ *   () => <ResultStatsPanel />,
+ * );
+ * ```
+ */
+
+import { ReactElement } from 'react';
+import { Disposable } from './core';
+
+/**
+ * Represents a contributed view in the application.
+ */
+export interface View {
+  /** The unique identifier for the view. */
+  id: string;
+  /** The display name of the view. */
+  name: string;
+}
+
+/**
+ * Registers a custom view at a specific UI location.
+ *
+ * The view provider function is called when the UI renders the location,
+ * and should return a React element to display.
+ *
+ * @param view The view descriptor (id and name).
+ * @param location The location where this view should appear (e.g. "sqllab.panels").
+ * @param provider A function that returns the React element to render.
+ * @returns A Disposable that unregisters the view when disposed.
+ *
+ * @example
+ * ```typescript
+ * views.registerView(
+ *   { id: 'my_ext.result_stats', name: 'Result Stats' },
+ *   'sqllab.panels',
+ *   () => <ResultStatsPanel />,
+ * );
+ * ```
+ */
+export declare function registerView(
+  view: View,
+  location: string,
+  provider: () => ReactElement,
+): Disposable;
+
+/**
+ * Retrieves all views registered at a specific location.
+ *
+ * @param location The location to retrieve registered views for (e.g. "sqllab.panels").
+ * @returns An array of View objects, or undefined if none are registered.
+ *
+ * @example
+ * ```typescript
+ * const panelViews = views.getViews('sqllab.panels');
+ * ```
+ */
+export declare function getViews(location: string): View[] | undefined;
diff --git a/superset-frontend/spec/helpers/testing-library.tsx b/superset-frontend/spec/helpers/testing-library.tsx
index 21abd6a44d0..e753a3ac037 100644
--- a/superset-frontend/spec/helpers/testing-library.tsx
+++ b/superset-frontend/spec/helpers/testing-library.tsx
@@ -43,7 +43,6 @@ import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
 import { configureStore, Store } from '@reduxjs/toolkit';
 import { api } from 'src/hooks/apiResources/queryApi';
 import userEvent from '@testing-library/user-event';
-import { ExtensionsProvider } from 'src/extensions/ExtensionsContext';
 
 type Options = Omit<RenderOptions, 'queries'> & {
   useRedux?: boolean;
@@ -86,9 +85,7 @@ export function createWrapper(options?: Options) {
 
   return ({ children }: { children?: ReactNode }) => {
     let result = (
-      <ThemeProvider theme={supersetTheme}>
-        <ExtensionsProvider>{children}</ExtensionsProvider>
-      </ThemeProvider>
+      <ThemeProvider theme={supersetTheme}>{children}</ThemeProvider>
     );
 
     if (useTheme) {
diff --git a/superset-frontend/src/SqlLab/components/AppLayout/AppLayout.test.tsx b/superset-frontend/src/SqlLab/components/AppLayout/AppLayout.test.tsx
index f7e1e3ae8e9..88ee6941765 100644
--- a/superset-frontend/src/SqlLab/components/AppLayout/AppLayout.test.tsx
+++ b/superset-frontend/src/SqlLab/components/AppLayout/AppLayout.test.tsx
@@ -16,11 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import React from 'react';
 import { render, userEvent, waitFor } from 'spec/helpers/testing-library';
 import { initialState } from 'src/SqlLab/fixtures';
 import useStoredSidebarWidth from 'src/components/ResizableSidebar/useStoredSidebarWidth';
-import type { contributions, core } from '@apache-superset/core';
-import ExtensionsManager from 'src/extensions/ExtensionsManager';
+import { views } from 'src/core';
+import { ViewLocations } from 'src/SqlLab/contributions';
 import AppLayout from './index';
 
 jest.mock('src/components/ResizableSidebar/useStoredSidebarWidth');
@@ -57,63 +58,9 @@ const defaultProps = {
   children: <div>Child</div>,
 };
 
-function createMockView(
-  id: string,
-  overrides: Partial<contributions.ViewContribution> = {},
-): contributions.ViewContribution {
-  return {
-    id,
-    name: `${id} View`,
-    ...overrides,
-  };
-}
-
-function setupActivatedExtension(
-  manager: ExtensionsManager,
-  extension: core.Extension,
-) {
-  const context = { disposables: [] };
-  (manager as any).contextIndex.set(extension.id, context);
-  (manager as any).extensionContributions.set(extension.id, {
-    commands: extension.contributions.commands,
-    menus: extension.contributions.menus,
-    views: extension.contributions.views,
-  });
-}
-
-async function createActivatedExtension(
-  manager: ExtensionsManager,
-  extensionOptions: Partial<core.Extension> = {},
-): Promise<core.Extension> {
-  const mockExtension: core.Extension = {
-    id: 'test-extension',
-    name: 'Test Extension',
-    description: 'A test extension',
-    version: '1.0.0',
-    dependencies: [],
-    remoteEntry: '',
-    exposedModules: [],
-    extensionDependencies: [],
-    contributions: {
-      commands: [],
-      menus: {},
-      views: {},
-    },
-    activate: jest.fn(),
-    deactivate: jest.fn(),
-    ...extensionOptions,
-  };
-
-  await manager.initializeExtension(mockExtension);
-  setupActivatedExtension(manager, mockExtension);
-
-  return mockExtension;
-}
-
 beforeEach(() => {
   jest.clearAllMocks();
   (useStoredSidebarWidth as jest.Mock).mockReturnValue([250, jest.fn()]);
-  (ExtensionsManager as any).instance = undefined;
 });
 
 test('renders two panels', () => {
@@ -147,20 +94,12 @@ test('calls setWidth on sidebar resize when not hidden', async () => {
   await waitFor(() => expect(setWidth).toHaveBeenCalled());
 });
 
-test('renders right sidebar when RIGHT_SIDEBAR_VIEW_ID view is contributed', async () => {
-  const manager = ExtensionsManager.getInstance();
-  const viewId = 'test-right-sidebar-view';
-  await createActivatedExtension(manager, {
-    contributions: {
-      commands: [],
-      menus: {},
-      views: {
-        sqllab: {
-          rightSidebar: [createMockView(viewId)],
-        },
-      },
-    },
-  });
+test('renders right sidebar when view is contributed at rightSidebar location', () => {
+  views.registerView(
+    { id: 'test-right-sidebar-view', name: 'Test Right Sidebar View' },
+    ViewLocations.sqllab.rightSidebar,
+    () => React.createElement('div', null, 'Right Sidebar Content'),
+  );
 
   const { getByText, getAllByTestId } = render(
     <AppLayout {...defaultProps} />,
diff --git a/superset-frontend/src/SqlLab/components/AppLayout/index.tsx b/superset-frontend/src/SqlLab/components/AppLayout/index.tsx
index 06486382749..3c5b9406286 100644
--- a/superset-frontend/src/SqlLab/components/AppLayout/index.tsx
+++ b/superset-frontend/src/SqlLab/components/AppLayout/index.tsx
@@ -22,7 +22,7 @@ import type { SqlLabRootState } from 'src/SqlLab/types';
 import { css, styled } from '@apache-superset/core';
 import { useComponentDidUpdate } from '@superset-ui/core';
 import { Grid } from '@superset-ui/core/components';
-import ExtensionsManager from 'src/extensions/ExtensionsManager';
+import { views } from 'src/core';
 import { Splitter } from 'src/components/Splitter';
 import useEffectEvent from 'src/hooks/useEffectEvent';
 import useStoredSidebarWidth from 'src/components/ResizableSidebar/useStoredSidebarWidth';
@@ -96,10 +96,7 @@ const AppLayout: React.FC = ({ children }) => {
       setRightWidth(possibleRightWidth);
     }
   };
-  const contributions =
-    ExtensionsManager.getInstance().getViewContributions(
-      ViewLocations.sqllab.rightSidebar,
-    ) || [];
+  const viewItems = views.getViews(ViewLocations.sqllab.rightSidebar) || [];
 
   return (
     <StyledContainer>
@@ -128,7 +125,7 @@ const AppLayout: React.FC = ({ children }) => {
           </StyledSidebar>
         </Splitter.Panel>
         <Splitter.Panel className="sqllab-body">{children}</Splitter.Panel>
-        {contributions.length > 0 && (
+        {viewItems.length > 0 && (
           <Splitter.Panel
             collapsible={{
               start: true,
diff --git a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
index 74c5ee05fd7..da5a33ad35b 100644
--- a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
@@ -30,8 +30,8 @@ import { Icons } from '@superset-ui/core/components/Icons';
 import { SqlLabRootState } from 'src/SqlLab/types';
 import { ViewLocations } from 'src/SqlLab/contributions';
 import PanelToolbar from 'src/components/PanelToolbar';
-import { useExtensionsContext } from 'src/extensions/ExtensionsContext';
-import ExtensionsManager from 'src/extensions/ExtensionsManager';
+import { views } from 'src/core';
+import { resolveView } from 'src/core/views';
 import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
 import useLogAction from 'src/logger/useLogAction';
 import { LOG_ACTIONS_SQLLAB_SWITCH_SOUTH_PANE_TAB } from 'src/logger/LogUtils';
@@ -104,11 +104,7 @@ const SouthPane = ({
   const editorId = tabViewId ?? id;
   const theme = useTheme();
   const dispatch = useDispatch();
-  const contributions =
-    ExtensionsManager.getInstance().getViewContributions(
-      ViewLocations.sqllab.panels,
-    ) || [];
-  const { getView } = useExtensionsContext();
+  const viewItems = views.getViews(ViewLocations.sqllab.panels) || [];
   const { offline, tables } = useSelector(
     ({ sqlLab: { offline, tables } }: SqlLabRootState) => ({
       offline,
@@ -212,9 +208,9 @@ const SouthPane = ({
         />
       ),
     })),
-    ...contributions.map(contribution => ({
-      key: contribution.id,
-      label: contribution.name,
+    ...viewItems.map(view => ({
+      key: view.id,
+      label: view.name,
       children: (
         <div
           css={css`
@@ -223,8 +219,8 @@ const SouthPane = ({
             }
           `}
         >
-          <PanelToolbar viewId={contribution.id} />
-          {getView(contribution.id)}
+          <PanelToolbar viewId={view.id} />
+          {resolveView(view.id)}
         </div>
       ),
       forceRender: true,
diff --git a/superset-frontend/src/SqlLab/components/StatusBar/StatusBar.test.tsx b/superset-frontend/src/SqlLab/components/StatusBar/StatusBar.test.tsx
index 3cf0ea3fb31..60c0642e64a 100644
--- a/superset-frontend/src/SqlLab/components/StatusBar/StatusBar.test.tsx
+++ b/superset-frontend/src/SqlLab/components/StatusBar/StatusBar.test.tsx
@@ -19,14 +19,12 @@
 import { render, screen } from 'spec/helpers/testing-library';
 import StatusBar from 'src/SqlLab/components/StatusBar';
 
-jest.mock('src/extensions/ExtensionsManager', () => {
-  const getInstance = jest.fn().mockReturnValue({
-    getViewContributions: jest
-      .fn()
-      .mockReturnValue([{ id: 'test-status-bar' }]),
-  });
-  return { getInstance };
-});
+jest.mock('src/core/views', () => ({
+  views: {
+    getViews: jest.fn().mockReturnValue([{ id: 'test-status-bar' }]),
+    registerView: jest.fn(),
+  },
+}));
 
 jest.mock('src/components/ViewListExtension', () => ({
   __esModule: true,
diff --git a/superset-frontend/src/SqlLab/components/StatusBar/index.tsx b/superset-frontend/src/SqlLab/components/StatusBar/index.tsx
index edaa9e8be5a..878fdb07cf1 100644
--- a/superset-frontend/src/SqlLab/components/StatusBar/index.tsx
+++ b/superset-frontend/src/SqlLab/components/StatusBar/index.tsx
@@ -19,7 +19,7 @@
 import { styled } from '@apache-superset/core';
 import { Flex } from '@superset-ui/core/components';
 import ViewListExtension from 'src/components/ViewListExtension';
-import ExtensionsManager from 'src/extensions/ExtensionsManager';
+import { views } from 'src/core';
 import { SQL_EDITOR_STATUSBAR_HEIGHT } from 'src/SqlLab/constants';
 import { ViewLocations } from 'src/SqlLab/contributions';
 
@@ -38,14 +38,11 @@ const Container = styled(Flex)`
 `;
 
 const StatusBar = () => {
-  const statusBarContributions =
-    ExtensionsManager.getInstance().getViewContributions(
-      ViewLocations.sqllab.statusBar,
-    ) || [];
+  const statusBarViews = views.getViews(ViewLocations.sqllab.statusBar) || [];
 
   return (
     <>
-      {statusBarContributions.length > 0 && (
+      {statusBarViews.length > 0 && (
         <Container align="center" justify="space-between">
           <ViewListExtension viewId={ViewLocations.sqllab.statusBar} />
         </Container>
diff --git a/superset-frontend/src/components/PanelToolbar/index.tsx b/superset-frontend/src/components/PanelToolbar/index.tsx
index 18144ae253a..16aa8930176 100644
--- a/superset-frontend/src/components/PanelToolbar/index.tsx
+++ b/superset-frontend/src/components/PanelToolbar/index.tsx
@@ -21,8 +21,7 @@ import { css, useTheme } from '@apache-superset/core/ui';
 import { Button, Divider, Dropdown } from '@superset-ui/core/components';
 import { Menu, MenuItemType } from '@superset-ui/core/components/Menu';
 import { Icons } from '@superset-ui/core/components/Icons';
-import { commands } from 'src/core';
-import ExtensionsManager from 'src/extensions/ExtensionsManager';
+import { commands, menus } from 'src/core';
 
 export interface PanelToolbarProps {
   viewId: string;
@@ -36,20 +35,16 @@ const PanelToolbar = ({
   defaultSecondaryActions,
 }: PanelToolbarProps) => {
   const theme = useTheme();
-  const contributions =
-    ExtensionsManager.getInstance().getMenuContributions(viewId);
+  const menu = menus.getMenu(viewId);
 
-  const primaryContributions = contributions?.primary || [];
-  const secondaryContributions = contributions?.secondary || [];
+  const primaryItems = menu?.primary || [];
+  const secondaryItems = menu?.secondary || [];
 
   const extensionPrimaryActions = useMemo(
     () =>
-      primaryContributions
-        .map(contribution => {
-          const command =
-            ExtensionsManager.getInstance().getCommandContribution(
-              contribution.command,
-            )!;
+      primaryItems
+        .map(item => {
+          const command = commands.getCommand(item.command)!;
           if (!command?.icon) {
             return null;
           }
@@ -60,8 +55,8 @@ const PanelToolbar = ({
 
           return (
             <Button
-              key={contribution.view}
-              onClick={() => commands.executeCommand(command?.command)}
+              key={item.view}
+              onClick={() => commands.executeCommand(command?.id)}
               tooltip={command?.description ?? command?.title}
               icon={<Icon iconSize="m" />}
               buttonSize="small"
@@ -72,30 +67,27 @@ const PanelToolbar = ({
           );
         })
         .filter(Boolean),
-    [primaryContributions],
+    [primaryItems],
   );
 
   const secondaryActions = useMemo(
     () =>
-      secondaryContributions
-        .map(contribution => {
-          const command =
-            ExtensionsManager.getInstance().getCommandContribution(
-              contribution.command,
-            )!;
+      secondaryItems
+        .map(item => {
+          const command = commands.getCommand(item.command)!;
           if (!command) {
             return null;
           }
           return {
-            key: command.command,
+            key: command.id,
             label: command.title,
             title: command.description,
-            onClick: () => commands.executeCommand(command.command),
+            onClick: () => commands.executeCommand(command.id),
           } as MenuItemType;
         })
         .filter(Boolean)
         .concat(defaultSecondaryActions || []),
-    [secondaryContributions, defaultSecondaryActions],
+    [secondaryItems, defaultSecondaryActions],
   );
 
   const hasPrimaryActions =
diff --git a/superset-frontend/src/components/ViewListExtension/ViewListExtension.test.tsx b/superset-frontend/src/components/ViewListExtension/ViewListExtension.test.tsx
index 5d0b7d360f5..77fda1e2dfd 100644
--- a/superset-frontend/src/components/ViewListExtension/ViewListExtension.test.tsx
+++ b/superset-frontend/src/components/ViewListExtension/ViewListExtension.test.tsx
@@ -16,189 +16,121 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ReactElement } from 'react';
+import React from 'react';
 import { render, screen } from 'spec/helpers/testing-library';
-import type { contributions, core } from '@apache-superset/core';
-import ExtensionsManager from 'src/extensions/ExtensionsManager';
-import { ExtensionsProvider } from 'src/extensions/ExtensionsContext';
+import { views } from 'src/core';
 import ViewListExtension from '.';
 
-function createMockView(
-  id: string,
-  overrides: Partial<contributions.ViewContribution> = {},
-): contributions.ViewContribution {
-  return {
-    id,
-    name: `${id} View`,
-    ...overrides,
-  };
-}
-
-function createMockExtension(
-  options: Partial<core.Extension> & {
-    views?: Record<string, Record<string, contributions.ViewContribution[]>>;
-  } = {},
-): core.Extension {
-  const {
-    id = 'test-extension',
-    name = 'Test Extension',
-    views = {},
-  } = options;
-
-  return {
-    id,
-    name,
-    description: 'A test extension',
-    version: '1.0.0',
-    dependencies: [],
-    remoteEntry: '',
-    exposedModules: [],
-    extensionDependencies: [],
-    contributions: {
-      commands: [],
-      menus: {},
-      views,
-    },
-    activate: jest.fn(),
-    deactivate: jest.fn(),
-  };
-}
-
-function setupActivatedExtension(
-  manager: ExtensionsManager,
-  extension: core.Extension,
-) {
-  const context = { disposables: [] };
-  (manager as any).contextIndex.set(extension.id, context);
-  (manager as any).extensionContributions.set(extension.id, {
-    commands: extension.contributions.commands,
-    menus: extension.contributions.menus,
-    views: extension.contributions.views,
-  });
-}
-
-async function createActivatedExtension(
-  manager: ExtensionsManager,
-  extensionOptions: Parameters<typeof createMockExtension>[0] = {},
-): Promise<core.Extension> {
-  const mockExtension = createMockExtension(extensionOptions);
-  await manager.initializeExtension(mockExtension);
-  setupActivatedExtension(manager, mockExtension);
-  return mockExtension;
-}
-
 const TEST_VIEW_ID = 'test.view';
 
-const renderWithExtensionsProvider = (ui: ReactElement) =>
-  render(ui, { wrapper: ExtensionsProvider as any });
-
-beforeEach(() => {
-  (ExtensionsManager as any).instance = undefined;
-});
+const disposables: Array<{ dispose: () => void }> = [];
 
 afterEach(() => {
-  (ExtensionsManager as any).instance = undefined;
+  disposables.forEach(d => d.dispose());
+  disposables.length = 0;
 });
 
 test('renders nothing when no view contributions exist', () => {
-  const { container } = renderWithExtensionsProvider(
-    <ViewListExtension viewId={TEST_VIEW_ID} />,
-  );
+  const { container } = render(<ViewListExtension viewId={TEST_VIEW_ID} />);
 
   expect(container.firstChild?.childNodes.length ?? 0).toBe(0);
 });
 
-test('renders placeholder for unregistered view provider', async () => {
-  const manager = ExtensionsManager.getInstance();
-
-  await createActivatedExtension(manager, {
-    views: {
-      test: {
-        view: [createMockView('test-view-1')],
-      },
-    },
-  });
+test('renders provider content for registered view', () => {
+  const provider = () =>
+    React.createElement('div', null, 'My Extension Content');
+  disposables.push(
+    views.registerView(
+      { id: 'test-view-1', name: 'test-view-1 View' },
+      TEST_VIEW_ID,
+      provider,
+    ),
+  );
 
-  renderWithExtensionsProvider(<ViewListExtension viewId={TEST_VIEW_ID} />);
+  render(<ViewListExtension viewId={TEST_VIEW_ID} />);
 
-  expect(screen.getByText(/test-view-1/)).toBeInTheDocument();
+  expect(screen.getByText('My Extension Content')).toBeInTheDocument();
 });
 
-test('renders multiple view placeholders for multiple contributions', async () => {
-  const manager = ExtensionsManager.getInstance();
-
-  await createActivatedExtension(manager, {
-    views: {
-      test: {
-        view: [createMockView('test-view-1'), createMockView('test-view-2')],
-      },
-    },
-  });
+test('renders content for multiple registered views', () => {
+  const provider1 = () => React.createElement('div', null, 'Content One');
+  const provider2 = () => React.createElement('div', null, 'Content Two');
+
+  disposables.push(
+    views.registerView(
+      { id: 'test-view-1', name: 'test-view-1 View' },
+      TEST_VIEW_ID,
+      provider1,
+    ),
+    views.registerView(
+      { id: 'test-view-2', name: 'test-view-2 View' },
+      TEST_VIEW_ID,
+      provider2,
+    ),
+  );
 
-  renderWithExtensionsProvider(<ViewListExtension viewId={TEST_VIEW_ID} />);
+  render(<ViewListExtension viewId={TEST_VIEW_ID} />);
 
-  expect(screen.getByText(/test-view-1/)).toBeInTheDocument();
-  expect(screen.getByText(/test-view-2/)).toBeInTheDocument();
+  expect(screen.getByText('Content One')).toBeInTheDocument();
+  expect(screen.getByText('Content Two')).toBeInTheDocument();
 });
 
 test('renders nothing for viewId with no matching contributions', () => {
-  const { container } = renderWithExtensionsProvider(
-    <ViewListExtension viewId="nonexistent.view" />,
-  );
+  const { container } = render(<ViewListExtension viewId="nonexistent.view" />);
 
   expect(container.firstChild?.childNodes.length ?? 0).toBe(0);
 });
 
-test('handles multiple extensions with views for same viewId', async () => {
-  const manager = ExtensionsManager.getInstance();
-
-  await createActivatedExtension(manager, {
-    id: 'extension-1',
-    views: {
-      test: {
-        view: [createMockView('ext1-view')],
-      },
-    },
-  });
-
-  await createActivatedExtension(manager, {
-    id: 'extension-2',
-    views: {
-      test: {
-        view: [createMockView('ext2-view')],
-      },
-    },
-  });
-
-  renderWithExtensionsProvider(<ViewListExtension viewId={TEST_VIEW_ID} />);
-
-  expect(screen.getByText(/ext1-view/)).toBeInTheDocument();
-  expect(screen.getByText(/ext2-view/)).toBeInTheDocument();
+test('handles multiple views registered at the same location', () => {
+  const provider1 = () => React.createElement('div', null, 'Ext1 Content');
+  const provider2 = () => React.createElement('div', null, 'Ext2 Content');
+
+  disposables.push(
+    views.registerView(
+      { id: 'ext1-view', name: 'ext1-view View' },
+      TEST_VIEW_ID,
+      provider1,
+    ),
+    views.registerView(
+      { id: 'ext2-view', name: 'ext2-view View' },
+      TEST_VIEW_ID,
+      provider2,
+    ),
+  );
+
+  render(<ViewListExtension viewId={TEST_VIEW_ID} />);
+
+  expect(screen.getByText('Ext1 Content')).toBeInTheDocument();
+  expect(screen.getByText('Ext2 Content')).toBeInTheDocument();
 });
 
-test('renders views for different viewIds independently', async () => {
-  const manager = ExtensionsManager.getInstance();
+test('renders views for different viewIds independently', () => {
   const VIEW_ID_A = 'view.a';
   const VIEW_ID_B = 'view.b';
 
-  await createActivatedExtension(manager, {
-    views: {
-      view: {
-        a: [createMockView('view-a-component')],
-        b: [createMockView('view-b-component')],
-      },
-    },
-  });
-
-  const { rerender } = renderWithExtensionsProvider(
-    <ViewListExtension viewId={VIEW_ID_A} />,
+  const providerA = () => React.createElement('div', null, 'View A Content');
+  const providerB = () => React.createElement('div', null, 'View B Content');
+
+  disposables.push(
+    views.registerView(
+      { id: 'view-a-component', name: 'view-a-component View' },
+      VIEW_ID_A,
+      providerA,
+    ),
+    views.registerView(
+      { id: 'view-b-component', name: 'view-b-component View' },
+      VIEW_ID_B,
+      providerB,
+    ),
   );
 
-  expect(screen.getByText(/view-a-component/)).toBeInTheDocument();
-  expect(screen.queryByText(/view-b-component/)).not.toBeInTheDocument();
+  const { rerender } = render(<ViewListExtension viewId={VIEW_ID_A} />);
+
+  expect(screen.getByText('View A Content')).toBeInTheDocument();
+  expect(screen.queryByText('View B Content')).not.toBeInTheDocument();
 
   rerender(<ViewListExtension viewId={VIEW_ID_B} />);
 
-  expect(screen.getByText(/view-b-component/)).toBeInTheDocument();
-  expect(screen.queryByText(/view-a-component/)).not.toBeInTheDocument();
+  expect(screen.getByText('View B Content')).toBeInTheDocument();
+  expect(screen.queryByText('View A Content')).not.toBeInTheDocument();
 });
diff --git a/superset-frontend/src/components/ViewListExtension/index.tsx b/superset-frontend/src/components/ViewListExtension/index.tsx
index a7f52093567..42292f87337 100644
--- a/superset-frontend/src/components/ViewListExtension/index.tsx
+++ b/superset-frontend/src/components/ViewListExtension/index.tsx
@@ -16,29 +16,25 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import ExtensionsManager from 'src/extensions/ExtensionsManager';
-import { useExtensionsContext } from 'src/extensions/ExtensionsContext';
+import React from 'react';
+import { ensureIsArray } from '@superset-ui/core';
+import { views } from 'src/core';
+import { resolveView } from 'src/core/views';
 
 export interface ViewListExtensionProps {
   viewId: string;
 }
 
 const ViewListExtension = ({ viewId }: ViewListExtensionProps) => {
-  const maybeContributions =
-    ExtensionsManager.getInstance().getViewContributions(viewId);
-  const contributions = Array.isArray(maybeContributions)
-    ? maybeContributions
-    : [];
-  const { getView } = useExtensionsContext();
+  const viewItems = ensureIsArray(views.getViews(viewId));
 
   return (
     <>
-      {contributions
-        .filter(
-          contribution =>
-            contribution && typeof contribution.id !== 'undefined',
-        )
-        .map(contribution => getView(contribution.id))}
+      {viewItems
+        .filter(view => view && typeof view.id !== 'undefined')
+        .map(view => (
+          <React.Fragment key={view.id}>{resolveView(view.id)}</React.Fragment>
+        ))}
     </>
   );
 };
diff --git a/superset-frontend/src/core/commands/index.ts b/superset-frontend/src/core/commands/index.ts
index 7eccc80b405..1eac191f112 100644
--- a/superset-frontend/src/core/commands/index.ts
+++ b/superset-frontend/src/core/commands/index.ts
@@ -20,22 +20,32 @@ import { logging } from '@apache-superset/core';
 import { commands as commandsApi } from '@apache-superset/core';
 import { Disposable } from '../models';
 
+type Command = commandsApi.Command;
+
+const commandsMap: Map<string, Command> = new Map();
+
 const commandRegistry: Map<string, (...args: any[]) => any> = new Map();
 
 const registerCommand: typeof commandsApi.registerCommand = (
-  command,
-  callback,
-  thisArg,
-) => {
-  if (commandRegistry.has(command)) {
+  command: Command,
+  callback: (...args: any[]) => any,
+  thisArg?: any,
+): Disposable => {
+  const { id } = command;
+
+  if (commandRegistry.has(id)) {
     logging.warn(
-      `Command "${command}" is already registered. Overwriting the existing command.`,
+      `Command "${id}" is already registered. Overwriting the existing command.`,
     );
   }
+
+  commandsMap.set(id, command);
   const boundCallback = thisArg ? callback.bind(thisArg) : callback;
-  commandRegistry.set(command, boundCallback);
+  commandRegistry.set(id, boundCallback);
+
   return new Disposable(() => {
-    commandRegistry.delete(command);
+    commandsMap.delete(id);
+    commandRegistry.delete(id);
   });
 };
 
@@ -50,15 +60,21 @@ const executeCommand: typeof commandsApi.executeCommand = async <T>(
   return callback(...args) as T;
 };
 
-const getCommands: typeof commandsApi.getCommands = filterInternal => {
-  const commands = Array.from(commandRegistry.keys());
-  return Promise.resolve(
-    filterInternal ? commands.filter(cmd => !cmd.startsWith('_')) : commands,
-  );
+const getCommands: typeof commandsApi.getCommands = (): Command[] =>
+  Array.from(commandsMap.values());
+
+const getCommand: typeof commandsApi.getCommand = (
+  id: string,
+): Command | undefined => commandsMap.get(id);
+
+export const resetContributions = (): void => {
+  commandsMap.clear();
+  commandRegistry.clear();
 };
 
 export const commands: typeof commandsApi = {
   registerCommand,
   executeCommand,
   getCommands,
+  getCommand,
 };
diff --git a/superset-frontend/src/core/editors/EditorHost.tsx b/superset-frontend/src/core/editors/EditorHost.tsx
index a6caa4743b1..210d2241b19 100644
--- a/superset-frontend/src/core/editors/EditorHost.tsx
+++ b/superset-frontend/src/core/editors/EditorHost.tsx
@@ -27,12 +27,12 @@
  */
 
 import { useState, useEffect, forwardRef } from 'react';
-import type { editors, contributions } from '@apache-superset/core';
+import type { editors } from '@apache-superset/core';
 import { useTheme } from '@apache-superset/core/ui';
 import EditorProviders from './EditorProviders';
 import AceEditorProvider from './AceEditorProvider';
 
-type EditorLanguage = contributions.EditorLanguage;
+type EditorLanguage = editors.EditorLanguage;
 type EditorProps = editors.EditorProps;
 type EditorHandle = editors.EditorHandle;
 
@@ -61,13 +61,13 @@ const useEditorProvider = (language: EditorLanguage) => {
 
     // Subscribe to provider changes
     const registerDisposable = manager.onDidRegister(event => {
-      if (event.provider.contribution.languages.includes(language)) {
+      if (event.provider.editor.languages.includes(language)) {
         updateProvider();
       }
     });
 
     const unregisterDisposable = manager.onDidUnregister(event => {
-      if (event.contribution.languages.includes(language)) {
+      if (event.editor.languages.includes(language)) {
         updateProvider();
       }
     });
diff --git a/superset-frontend/src/core/editors/EditorProviders.test.ts b/superset-frontend/src/core/editors/EditorProviders.test.ts
index 74abb6088cf..b67fdcd874c 100644
--- a/superset-frontend/src/core/editors/EditorProviders.test.ts
+++ b/superset-frontend/src/core/editors/EditorProviders.test.ts
@@ -16,19 +16,17 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import type { editors, contributions } from '@apache-superset/core';
+import type { editors } from '@apache-superset/core';
 import EditorProviders from './EditorProviders';
 
-type EditorLanguage = contributions.EditorLanguage;
-type EditorContribution = editors.EditorContribution;
+type EditorLanguage = editors.EditorLanguage;
+type Editor = editors.Editor;
 type EditorComponent = editors.EditorComponent;
 
 /**
- * Creates a mock editor contribution for testing.
+ * Creates a mock editor for testing.
  */
-function createMockEditorContribution(
-  overrides: Partial<EditorContribution> = {},
-): EditorContribution {
+function createMockEditor(overrides: Partial<Editor> = {}): Editor {
   return {
     id: 'test.mock-editor',
     name: 'Mock Editor',
@@ -61,37 +59,37 @@ test('creates singleton instance', () => {
 
 test('registers and retrieves a provider', () => {
   const manager = EditorProviders.getInstance();
-  const contribution = createMockEditorContribution();
+  const editor = createMockEditor();
   const component = createMockEditorComponent();
 
-  manager.registerProvider(contribution, component);
+  manager.registerProvider(editor, component);
 
   const provider = manager.getProvider('sql');
   expect(provider).toBeDefined();
-  expect(provider?.contribution).toEqual(contribution);
+  expect(provider?.editor).toEqual(editor);
   expect(provider?.component).toBe(component);
 });
 
 test('hasProvider returns true when provider is registered', () => {
   const manager = EditorProviders.getInstance();
-  const contribution = createMockEditorContribution();
+  const editor = createMockEditor();
   const component = createMockEditorComponent();
 
   expect(manager.hasProvider('sql')).toBe(false);
 
-  manager.registerProvider(contribution, component);
+  manager.registerProvider(editor, component);
 
   expect(manager.hasProvider('sql')).toBe(true);
 });
 
 test('hasProvider returns false for unregistered languages', () => {
   const manager = EditorProviders.getInstance();
-  const contribution = createMockEditorContribution({
+  const editor = createMockEditor({
     languages: ['sql'],
   });
   const component = createMockEditorComponent();
 
-  manager.registerProvider(contribution, component);
+  manager.registerProvider(editor, component);
 
   expect(manager.hasProvider('sql')).toBe(true);
   expect(manager.hasProvider('json')).toBe(false);
@@ -110,17 +108,17 @@ test('getAllProviders returns all registered providers', () => {
 
   expect(manager.getAllProviders()).toHaveLength(0);
 
-  const contribution1 = createMockEditorContribution({
+  const editor1 = createMockEditor({
     id: 'editor-1',
     languages: ['sql'],
   });
-  const contribution2 = createMockEditorContribution({
+  const editor2 = createMockEditor({
     id: 'editor-2',
     languages: ['json'],
   });
 
-  manager.registerProvider(contribution1, createMockEditorComponent());
-  manager.registerProvider(contribution2, createMockEditorComponent());
+  manager.registerProvider(editor1, createMockEditorComponent());
+  manager.registerProvider(editor2, createMockEditorComponent());
 
   const providers = manager.getAllProviders();
   expect(providers).toHaveLength(2);
@@ -128,10 +126,10 @@ test('getAllProviders returns all registered providers', () => {
 
 test('unregisters provider when disposable is disposed', () => {
   const manager = EditorProviders.getInstance();
-  const contribution = createMockEditorContribution();
+  const editor = createMockEditor();
   const component = createMockEditorComponent();
 
-  const disposable = manager.registerProvider(contribution, component);
+  const disposable = manager.registerProvider(editor, component);
 
   expect(manager.hasProvider('sql')).toBe(true);
 
@@ -143,12 +141,12 @@ test('unregisters provider when disposable is disposed', () => {
 
 test('supports multiple languages per provider', () => {
   const manager = EditorProviders.getInstance();
-  const contribution = createMockEditorContribution({
+  const editor = createMockEditor({
     languages: ['sql', 'json', 'yaml'],
   });
   const component = createMockEditorComponent();
 
-  manager.registerProvider(contribution, component);
+  manager.registerProvider(editor, component);
 
   expect(manager.hasProvider('sql')).toBe(true);
   expect(manager.hasProvider('json')).toBe(true);
@@ -168,15 +166,15 @@ test('warns when registering duplicate provider id', () => {
   const manager = EditorProviders.getInstance();
   const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
 
-  const contribution = createMockEditorContribution({
+  const editor = createMockEditor({
     id: 'duplicate-editor',
   });
 
-  manager.registerProvider(contribution, createMockEditorComponent());
+  manager.registerProvider(editor, createMockEditorComponent());
 
   // Try to register with same ID
   const disposable = manager.registerProvider(
-    { ...contribution },
+    { ...editor },
     createMockEditorComponent(),
   );
 
@@ -199,15 +197,15 @@ test('fires onDidRegister event when provider is registered', () => {
 
   manager.onDidRegister(listener);
 
-  const contribution = createMockEditorContribution();
+  const editor = createMockEditor();
   const component = createMockEditorComponent();
 
-  manager.registerProvider(contribution, component);
+  manager.registerProvider(editor, component);
 
   expect(listener).toHaveBeenCalledTimes(1);
   expect(listener).toHaveBeenCalledWith({
     provider: {
-      contribution,
+      editor,
       component,
     },
   });
@@ -219,9 +217,9 @@ test('fires onDidUnregister event when provider is unregistered', () => {
 
   manager.onDidUnregister(listener);
 
-  const contribution = createMockEditorContribution();
+  const editor = createMockEditor();
   const disposable = manager.registerProvider(
-    contribution,
+    editor,
     createMockEditorComponent(),
   );
 
@@ -229,7 +227,7 @@ test('fires onDidUnregister event when provider is unregistered', () => {
 
   expect(listener).toHaveBeenCalledTimes(1);
   expect(listener).toHaveBeenCalledWith({
-    contribution,
+    editor,
   });
 });
 
@@ -241,7 +239,7 @@ test('event listeners can be disposed', () => {
 
   // Register first provider - listener should be called
   manager.registerProvider(
-    createMockEditorContribution({ id: 'editor-1' }),
+    createMockEditor({ id: 'editor-1' }),
     createMockEditorComponent(),
   );
 
@@ -252,7 +250,7 @@ test('event listeners can be disposed', () => {
 
   // Register second provider - listener should not be called
   manager.registerProvider(
-    createMockEditorContribution({ id: 'editor-2', languages: ['json'] }),
+    createMockEditor({ id: 'editor-2', languages: ['json'] }),
     createMockEditorComponent(),
   );
 
@@ -271,10 +269,7 @@ test('handles errors in event listeners gracefully', () => {
   manager.onDidRegister(errorListener);
   manager.onDidRegister(successListener);
 
-  manager.registerProvider(
-    createMockEditorContribution(),
-    createMockEditorComponent(),
-  );
+  manager.registerProvider(createMockEditor(), createMockEditorComponent());
 
   // Both listeners should have been called
   expect(errorListener).toHaveBeenCalledTimes(1);
@@ -293,11 +288,11 @@ test('reset clears all providers and language mappings', () => {
   const manager = EditorProviders.getInstance();
 
   manager.registerProvider(
-    createMockEditorContribution({ id: 'editor-1', languages: ['sql'] }),
+    createMockEditor({ id: 'editor-1', languages: ['sql'] }),
     createMockEditorComponent(),
   );
   manager.registerProvider(
-    createMockEditorContribution({ id: 'editor-2', languages: ['json'] }),
+    createMockEditor({ id: 'editor-2', languages: ['json'] }),
     createMockEditorComponent(),
   );
 
@@ -315,24 +310,24 @@ test('reset clears all providers and language mappings', () => {
 test('later registration replaces language mapping', () => {
   const manager = EditorProviders.getInstance();
 
-  const contribution1 = createMockEditorContribution({
+  const editor1 = createMockEditor({
     id: 'editor-1',
     name: 'Editor 1',
     languages: ['sql'],
   });
-  const contribution2 = createMockEditorContribution({
+  const editor2 = createMockEditor({
     id: 'editor-2',
     name: 'Editor 2',
     languages: ['sql'],
   });
 
-  manager.registerProvider(contribution1, createMockEditorComponent());
-  manager.registerProvider(contribution2, createMockEditorComponent());
+  manager.registerProvider(editor1, createMockEditorComponent());
+  manager.registerProvider(editor2, createMockEditorComponent());
 
   // The second registration should replace the first for the 'sql' language
   const provider = manager.getProvider('sql');
-  expect(provider?.contribution.id).toBe('editor-2');
-  expect(provider?.contribution.name).toBe('Editor 2');
+  expect(provider?.editor.id).toBe('editor-2');
+  expect(provider?.editor.name).toBe('Editor 2');
 
   // But both providers should exist
   expect(manager.getAllProviders()).toHaveLength(2);
diff --git a/superset-frontend/src/core/editors/EditorProviders.ts b/superset-frontend/src/core/editors/EditorProviders.ts
index e9390fd99c1..d5bbdf973a4 100644
--- a/superset-frontend/src/core/editors/EditorProviders.ts
+++ b/superset-frontend/src/core/editors/EditorProviders.ts
@@ -17,12 +17,12 @@
  * under the License.
  */
 
-import type { editors, contributions } from '@apache-superset/core';
+import type { editors } from '@apache-superset/core';
 import { Disposable } from '../models';
 
-type EditorLanguage = contributions.EditorLanguage;
+type EditorLanguage = editors.EditorLanguage;
 type EditorProvider = editors.EditorProvider;
-type EditorContribution = editors.EditorContribution;
+type Editor = editors.Editor;
 type EditorComponent = editors.EditorComponent;
 type EditorProviderRegisteredEvent = editors.EditorProviderRegisteredEvent;
 type EditorProviderUnregisteredEvent = editors.EditorProviderUnregisteredEvent;
@@ -114,15 +114,15 @@ class EditorProviders {
    * Register an editor provider.
    * When registered, the provider replaces the default editor for its supported languages.
    *
-   * @param contribution The editor contribution metadata.
+   * @param editor The editor descriptor.
    * @param component The React component implementing the editor.
    * @returns A Disposable to unregister the provider.
    */
   public registerProvider(
-    contribution: EditorContribution,
+    editor: Editor,
     component: EditorComponent,
   ): Disposable {
-    const { id, languages } = contribution;
+    const { id, languages } = editor;
 
     // Check if provider with this ID already exists
     if (this.providers.has(id)) {
@@ -132,7 +132,7 @@ class EditorProviders {
     }
 
     const provider: EditorProvider = {
-      contribution,
+      editor,
       component,
     };
 
@@ -163,10 +163,10 @@ class EditorProviders {
       return;
     }
 
-    const { contribution } = provider;
+    const { editor } = provider;
 
     // Remove language mappings for this provider
-    contribution.languages.forEach(language => {
+    editor.languages.forEach(language => {
       if (this.languageToProvider.get(language) === id) {
         this.languageToProvider.delete(language);
       }
@@ -176,7 +176,7 @@ class EditorProviders {
     this.providers.delete(id);
 
     // Fire unregistration event
-    this.unregisterEmitter.fire({ contribution });
+    this.unregisterEmitter.fire({ editor });
   }
 
   /**
diff --git a/superset-frontend/src/core/editors/index.ts b/superset-frontend/src/core/editors/index.ts
index b67d48cd42b..f7fe4caa03c 100644
--- a/superset-frontend/src/core/editors/index.ts
+++ b/superset-frontend/src/core/editors/index.ts
@@ -24,13 +24,12 @@
  * and resolution functions declared in the API types.
  */
 
-import type { contributions } from '@apache-superset/core';
 import { editors as editorsApi } from '@apache-superset/core';
-import ExtensionsManager from 'src/extensions/ExtensionsManager';
 import { Disposable } from '../models';
 import EditorProviders from './EditorProviders';
 
-type EditorLanguage = contributions.EditorLanguage;
+type EditorLanguage = editorsApi.EditorLanguage;
+type Editor = editorsApi.Editor;
 type EditorProvider = editorsApi.EditorProvider;
 type EditorComponent = editorsApi.EditorComponent;
 type EditorProviderRegisteredEvent = editorsApi.EditorProviderRegisteredEvent;
@@ -38,31 +37,20 @@ type EditorProviderUnregisteredEvent =
   editorsApi.EditorProviderUnregisteredEvent;
 
 /**
- * Register an editor provider for specific languages.
- * The contribution metadata is resolved from the extension manifest by ID.
+ * Register an editor provider as a module-level side effect.
+ * Takes the editor descriptor directly rather than looking it up
+ * from a manifest by ID.
  *
- * @param id The editor contribution ID declared in extension.json
- * @param component The React component implementing EditorProps
- * @returns A Disposable to unregister the provider
+ * @param editor The editor descriptor.
+ * @param component The React component implementing the editor.
+ * @returns A Disposable to unregister the provider.
  */
-export const registerEditorProvider = (
-  id: string,
+export const registerEditor = (
+  editor: Editor,
   component: EditorComponent,
 ): Disposable => {
-  const manager = ExtensionsManager.getInstance();
-  const contribution = manager.getEditorContributions().find(c => c.id === id);
-
-  if (!contribution) {
-    // eslint-disable-next-line no-console
-    console.warn(
-      `No editor contribution found in extension.json for id "${id}". ` +
-        'Ensure the editor is declared in the contributions.editors array.',
-    );
-    return new Disposable(() => {});
-  }
-
   const providers = EditorProviders.getInstance();
-  return providers.registerProvider(contribution, component);
+  return providers.registerProvider(editor, component);
 };
 
 /**
@@ -126,7 +114,7 @@ export const onDidUnregisterEditorProvider = (
  * Editors API object for use in the extension system.
  */
 export const editors: typeof editorsApi = {
-  registerEditorProvider,
+  registerEditor,
   getEditorProvider,
   hasEditorProvider,
   getAllEditorProviders,
diff --git a/superset-frontend/src/core/extensions/index.ts b/superset-frontend/src/core/extensions/index.ts
index cfaef49da18..ae49a135aed 100644
--- a/superset-frontend/src/core/extensions/index.ts
+++ b/superset-frontend/src/core/extensions/index.ts
@@ -17,13 +17,13 @@
  * under the License.
  */
 import { extensions as extensionsApi } from '@apache-superset/core';
-import ExtensionsManager from 'src/extensions/ExtensionsManager';
+import ExtensionsLoader from 'src/extensions/ExtensionsLoader';
 
 const getExtension: typeof extensionsApi.getExtension = id =>
-  ExtensionsManager.getInstance().getExtension(id);
+  ExtensionsLoader.getInstance().getExtension(id);
 
 const getAllExtensions: typeof extensionsApi.getAllExtensions = () =>
-  ExtensionsManager.getInstance().getExtensions();
+  ExtensionsLoader.getInstance().getExtensions();
 
 export const extensions: typeof extensionsApi = {
   getExtension,
diff --git a/superset-frontend/src/core/index.ts b/superset-frontend/src/core/index.ts
index 463beffcfda..3dba313cf97 100644
--- a/superset-frontend/src/core/index.ts
+++ b/superset-frontend/src/core/index.ts
@@ -17,24 +17,12 @@
  * under the License.
  */
 import { core as coreType } from '@apache-superset/core';
-import { getExtensionsContextValue } from '../extensions/ExtensionsContextUtils';
 import { Disposable } from './models';
 
-export const registerViewProvider: typeof coreType.registerViewProvider = (
-  id,
-  viewProvider,
-) => {
-  const { registerViewProvider: register, unregisterViewProvider: unregister } =
-    getExtensionsContextValue();
-  register(id, viewProvider);
-  return new Disposable(() => unregister(id));
-};
-
 const { GenericDataType } = coreType;
 
 export const core: typeof coreType = {
   GenericDataType,
-  registerViewProvider,
   Disposable,
 };
 
@@ -42,6 +30,8 @@ export * from './authentication';
 export * from './commands';
 export * from './editors';
 export * from './extensions';
+export * from './menus';
 export * from './models';
 export * from './sqlLab';
 export * from './utils';
+export * from './views';
diff --git a/superset-frontend/src/core/menus/index.test.ts b/superset-frontend/src/core/menus/index.test.ts
new file mode 100644
index 00000000000..95a91c5ac0b
--- /dev/null
+++ b/superset-frontend/src/core/menus/index.test.ts
@@ -0,0 +1,124 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { menus } from './index';
+
+const disposables: Array<{ dispose: () => void }> = [];
+
+afterEach(() => {
+  disposables.forEach(d => d.dispose());
+  disposables.length = 0;
+});
+
+test('register stores a menu item', () => {
+  disposables.push(
+    menus.registerMenuItem(
+      { command: 'ext.export', view: 'builtin.editor' },
+      'sqllab.editor',
+      'secondary',
+    ),
+  );
+
+  const contributions = menus.getMenu('sqllab.editor');
+  expect(contributions).toBeDefined();
+  expect(contributions?.secondary).toEqual([
+    { view: 'builtin.editor', command: 'ext.export' },
+  ]);
+});
+
+test('getMenu returns undefined for unknown location', () => {
+  expect(menus.getMenu('nonexistent')).toBeUndefined();
+});
+
+test('groups menu items by group field', () => {
+  disposables.push(
+    menus.registerMenuItem(
+      { command: 'ext.primary_action', view: 'builtin.editor' },
+      'sqllab.editor',
+      'primary',
+    ),
+    menus.registerMenuItem(
+      { command: 'ext.secondary_action', view: 'builtin.editor' },
+      'sqllab.editor',
+      'secondary',
+    ),
+    menus.registerMenuItem(
+      { command: 'ext.context_action', view: 'builtin.editor' },
+      'sqllab.editor',
+      'context',
+    ),
+  );
+
+  const contributions = menus.getMenu('sqllab.editor');
+  expect(contributions?.primary).toHaveLength(1);
+  expect(contributions?.secondary).toHaveLength(1);
+  expect(contributions?.context).toHaveLength(1);
+  expect(contributions?.primary?.[0].command).toBe('ext.primary_action');
+  expect(contributions?.secondary?.[0].command).toBe('ext.secondary_action');
+  expect(contributions?.context?.[0].command).toBe('ext.context_action');
+});
+
+test('multiple items in the same group are accumulated', () => {
+  disposables.push(
+    menus.registerMenuItem(
+      { command: 'ext.action1', view: 'builtin.editor' },
+      'sqllab.editor',
+      'secondary',
+    ),
+    menus.registerMenuItem(
+      { command: 'ext.action2', view: 'builtin.editor' },
+      'sqllab.editor',
+      'secondary',
+    ),
+  );
+
+  const contributions = menus.getMenu('sqllab.editor');
+  expect(contributions?.secondary).toHaveLength(2);
+});
+
+test('items at different locations are independent', () => {
+  disposables.push(
+    menus.registerMenuItem(
+      { command: 'ext.action1', view: 'builtin.editor' },
+      'sqllab.editor',
+      'primary',
+    ),
+    menus.registerMenuItem(
+      { command: 'ext.action2', view: 'builtin.results' },
+      'sqllab.results',
+      'primary',
+    ),
+  );
+
+  expect(menus.getMenu('sqllab.editor')?.primary).toHaveLength(1);
+  expect(menus.getMenu('sqllab.results')?.primary).toHaveLength(1);
+});
+
+test('dispose removes the menu registration', () => {
+  const disposable = menus.registerMenuItem(
+    { command: 'ext.export', view: 'builtin.editor' },
+    'sqllab.editor',
+    'secondary',
+  );
+
+  expect(menus.getMenu('sqllab.editor')).toBeDefined();
+
+  disposable.dispose();
+
+  expect(menus.getMenu('sqllab.editor')).toBeUndefined();
+});
diff --git a/superset-frontend/src/core/menus/index.ts b/superset-frontend/src/core/menus/index.ts
new file mode 100644
index 00000000000..50d0b954829
--- /dev/null
+++ b/superset-frontend/src/core/menus/index.ts
@@ -0,0 +1,83 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * @fileoverview Standalone menus registry implementation.
+ *
+ * Stores menu item registrations as module-level state.
+ * Extensions register menu items as side effects at import time.
+ */
+
+import type { menus as menusApi } from '@apache-superset/core';
+import { Disposable } from '../models';
+
+type MenuItem = menusApi.MenuItem;
+type Menu = menusApi.Menu;
+
+type StoredMenuItem = {
+  item: MenuItem;
+  location: string;
+  group: 'primary' | 'secondary' | 'context';
+};
+
+const menuItems: StoredMenuItem[] = [];
+
+const registerMenuItem: typeof menusApi.registerMenuItem = (
+  item: MenuItem,
+  location: string,
+  group: 'primary' | 'secondary' | 'context',
+): Disposable => {
+  const stored: StoredMenuItem = { item, location, group };
+  menuItems.push(stored);
+  return new Disposable(() => {
+    const index = menuItems.indexOf(stored);
+    if (index >= 0) {
+      menuItems.splice(index, 1);
+    }
+  });
+};
+
+const getMenu: typeof menusApi.getMenu = (
+  location: string,
+): Menu | undefined => {
+  const items = menuItems.filter(entry => entry.location === location);
+  if (items.length === 0) return undefined;
+
+  const result: Menu = {};
+
+  for (const { item, group } of items) {
+    if (group === 'primary') {
+      result.primary = result.primary ?? [];
+      result.primary.push(item);
+    } else if (group === 'secondary') {
+      result.secondary = result.secondary ?? [];
+      result.secondary.push(item);
+    } else if (group === 'context') {
+      result.context = result.context ?? [];
+      result.context.push(item);
+    }
+  }
+
+  return result;
+};
+
+export const menus: typeof menusApi = {
+  registerMenuItem,
+  getMenu,
+};
diff --git a/superset-frontend/src/core/models.ts b/superset-frontend/src/core/models.ts
index c0a4b721aaa..0acd614c6b4 100644
--- a/superset-frontend/src/core/models.ts
+++ b/superset-frontend/src/core/models.ts
@@ -107,7 +107,3 @@ export class Disposable implements coreType.Disposable {
     this.dispose();
   }
 }
-
-export class ExtensionContext implements coreType.ExtensionContext {
-  disposables: coreType.Disposable[] = [];
-}
diff --git a/superset-frontend/src/core/views/index.test.ts b/superset-frontend/src/core/views/index.test.ts
new file mode 100644
index 00000000000..d98a05b7f4f
--- /dev/null
+++ b/superset-frontend/src/core/views/index.test.ts
@@ -0,0 +1,112 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import { views, resolveView } from './index';
+
+const disposables: Array<{ dispose: () => void }> = [];
+
+afterEach(() => {
+  disposables.forEach(d => d.dispose());
+  disposables.length = 0;
+});
+
+test('register stores view metadata and makes it resolvable', () => {
+  const provider = () => React.createElement('div', null, 'Test');
+  disposables.push(
+    views.registerView(
+      { id: 'test.view', name: 'Test View' },
+      'sqllab.panels',
+      provider,
+    ),
+  );
+
+  expect(views.getViews('sqllab.panels')).toEqual([
+    { id: 'test.view', name: 'Test View' },
+  ]);
+  expect(resolveView('test.view')).toBeTruthy();
+});
+
+test('getContributions returns undefined for unknown location', () => {
+  expect(views.getViews('nonexistent')).toBeUndefined();
+});
+
+test('resolveView returns a placeholder element for unknown id', () => {
+  expect(resolveView('nonexistent.view')).toBeTruthy();
+});
+
+test('multiple views at the same location are returned together', () => {
+  const provider1 = () => React.createElement('div', null, 'View 1');
+  const provider2 = () => React.createElement('div', null, 'View 2');
+
+  disposables.push(
+    views.registerView(
+      { id: 'ext.view1', name: 'View One' },
+      'sqllab.panels',
+      provider1,
+    ),
+    views.registerView(
+      { id: 'ext.view2', name: 'View Two' },
+      'sqllab.panels',
+      provider2,
+    ),
+  );
+
+  const contributions = views.getViews('sqllab.panels');
+  expect(contributions).toHaveLength(2);
+  expect(contributions).toEqual([
+    { id: 'ext.view1', name: 'View One' },
+    { id: 'ext.view2', name: 'View Two' },
+  ]);
+});
+
+test('views at different locations are independent', () => {
+  const provider1 = () => React.createElement('div', null, 'Panel');
+  const provider2 = () => React.createElement('div', null, 'Status');
+
+  disposables.push(
+    views.registerView(
+      { id: 'ext.panel', name: 'Panel' },
+      'sqllab.panels',
+      provider1,
+    ),
+    views.registerView(
+      { id: 'ext.status', name: 'Status' },
+      'sqllab.statusBar',
+      provider2,
+    ),
+  );
+
+  expect(views.getViews('sqllab.panels')).toHaveLength(1);
+  expect(views.getViews('sqllab.statusBar')).toHaveLength(1);
+});
+
+test('dispose removes the view registration', () => {
+  const provider = () => React.createElement('div', null, 'Test');
+  const disposable = views.registerView(
+    { id: 'test.view', name: 'Test View' },
+    'sqllab.panels',
+    provider,
+  );
+
+  expect(views.getViews('sqllab.panels')).toHaveLength(1);
+
+  disposable.dispose();
+
+  expect(views.getViews('sqllab.panels')).toBeUndefined();
+});
diff --git a/superset-frontend/src/core/views/index.ts b/superset-frontend/src/core/views/index.ts
new file mode 100644
index 00000000000..5bed7d10910
--- /dev/null
+++ b/superset-frontend/src/core/views/index.ts
@@ -0,0 +1,83 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * @fileoverview Standalone views registry implementation.
+ *
+ * Stores view metadata and providers as module-level state.
+ * Extensions register views as side effects at import time.
+ */
+
+import React, { ReactElement } from 'react';
+import type { views as viewsApi } from '@apache-superset/core';
+import { ErrorBoundary } from 'src/components/ErrorBoundary';
+import ExtensionPlaceholder from 'src/extensions/ExtensionPlaceholder';
+import { Disposable } from '../models';
+
+type View = viewsApi.View;
+
+const viewRegistry: Map<
+  string,
+  { view: View; location: string; provider: () => ReactElement }
+> = new Map();
+
+const locationIndex: Map<string, Set<string>> = new Map();
+
+const registerView: typeof viewsApi.registerView = (
+  view: View,
+  location: string,
+  provider: () => ReactElement,
+): Disposable => {
+  const { id } = view;
+
+  viewRegistry.set(id, { view, location, provider });
+
+  const ids = locationIndex.get(location) ?? new Set();
+  ids.add(id);
+  locationIndex.set(location, ids);
+
+  return new Disposable(() => {
+    viewRegistry.delete(id);
+    locationIndex.get(location)?.delete(id);
+  });
+};
+
+export const resolveView = (id: string): ReactElement => {
+  const provider = viewRegistry.get(id)?.provider;
+  if (!provider) {
+    return React.createElement(ExtensionPlaceholder, { id });
+  }
+  return React.createElement(ErrorBoundary, null, provider());
+};
+
+const getViews: typeof viewsApi.getViews = (
+  location: string,
+): View[] | undefined => {
+  const ids = locationIndex.get(location);
+  if (!ids || ids.size === 0) return undefined;
+
+  return Array.from(ids)
+    .map(id => viewRegistry.get(id)?.view)
+    .filter((c): c is View => !!c);
+};
+
+export const views: typeof viewsApi = {
+  registerView,
+  getViews,
+};
diff --git a/superset-frontend/src/extensions/ExtensionsContext.test.tsx b/superset-frontend/src/extensions/ExtensionsContext.test.tsx
deleted file mode 100644
index da9047d0c66..00000000000
--- a/superset-frontend/src/extensions/ExtensionsContext.test.tsx
+++ /dev/null
@@ -1,150 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { ReactElement } from 'react';
-import { renderHook } from '@testing-library/react-hooks';
-import { render } from 'spec/helpers/testing-library';
-import { ExtensionsProvider, useExtensionsContext } from './ExtensionsContext';
-
-test('provides extensions context with initial empty state', () => {
-  const wrapper = ({ children }: { children: React.ReactNode }) => (
-    <ExtensionsProvider>{children}</ExtensionsProvider>
-  );
-
-  const { result } = renderHook(() => useExtensionsContext(), { wrapper });
-
-  const view = result.current.getView('non-existent');
-  expect(view).not.toBeNull();
-  // Should return a placeholder when no provider is registered
-  const { getByText } = render(view);
-  expect(
-    getByText('The extension non-existent could not be loaded.'),
-  ).toBeInTheDocument();
-  expect(
-    getByText(/This may be due to the extension not being activated/),
-  ).toBeInTheDocument();
-
-  expect(typeof result.current.getView).toBe('function');
-  expect(typeof result.current.registerViewProvider).toBe('function');
-  expect(typeof result.current.unregisterViewProvider).toBe('function');
-});
-
-test('registers a view provider', () => {
-  const wrapper = ({ children }: { children: React.ReactNode }) => (
-    <ExtensionsProvider>{children}</ExtensionsProvider>
-  );
-
-  const { result } = renderHook(() => useExtensionsContext(), { wrapper });
-
-  const mockViewProvider = (): ReactElement => <div>Mock View</div>;
-
-  result.current.registerViewProvider('test-view', mockViewProvider);
-
-  const view = result.current.getView('test-view');
-  expect(view).not.toBeNull();
-});
-
-test('unregisters a view provider', () => {
-  const wrapper = ({ children }: { children: React.ReactNode }) => (
-    <ExtensionsProvider>{children}</ExtensionsProvider>
-  );
-
-  const { result } = renderHook(() => useExtensionsContext(), { wrapper });
-
-  const mockViewProvider = (): ReactElement => (
-    <div data-test="registered-view">Mock View</div>
-  );
-
-  // First register a view provider
-  result.current.registerViewProvider('test-view', mockViewProvider);
-  const registeredView = result.current.getView('test-view');
-  const { getByTestId: getByTestIdRegistered } = render(registeredView);
-  expect(getByTestIdRegistered('registered-view')).toBeInTheDocument();
-
-  // Then unregister it - should return placeholder instead
-  result.current.unregisterViewProvider('test-view');
-  const unregisteredView = result.current.getView('test-view');
-  const { getByText } = render(unregisteredView);
-  expect(
-    getByText('The extension test-view could not be loaded.'),
-  ).toBeInTheDocument();
-  expect(
-    getByText(/This may be due to the extension not being activated/),
-  ).toBeInTheDocument();
-});
-
-test('throws error when useExtensionsContext is used outside provider', () => {
-  const { result } = renderHook(() => useExtensionsContext());
-
-  expect(result.error).toEqual(
-    Error('useExtensionsContext must be used within a ExtensionsProvider'),
-  );
-});
-
-test('getView returns the correct rendered component', () => {
-  const wrapper = ({ children }: { children: React.ReactNode }) => (
-    <ExtensionsProvider>{children}</ExtensionsProvider>
-  );
-
-  const { result } = renderHook(() => useExtensionsContext(), { wrapper });
-
-  const MockComponent = (): ReactElement => (
-    <div data-test="mock-component">Mock View Content</div>
-  );
-
-  result.current.registerViewProvider('test-view', MockComponent);
-
-  const view = result.current.getView('test-view');
-  expect(view).not.toBeNull();
-
-  // Render the returned view to verify it's wrapped in ErrorBoundary and contains the component
-  const { getByTestId } = render(view!);
-  expect(getByTestId('mock-component')).toBeInTheDocument();
-  expect(getByTestId('mock-component')).toHaveTextContent('Mock View Content');
-});
-
-test('getView returns placeholder when no provider is registered', () => {
-  const wrapper = ({ children }: { children: React.ReactNode }) => (
-    <ExtensionsProvider>{children}</ExtensionsProvider>
-  );
-
-  const { result } = renderHook(() => useExtensionsContext(), { wrapper });
-
-  const view = result.current.getView('non-existent-view');
-  expect(view).not.toBeNull();
-
-  const { getByText } = render(view);
-  expect(
-    getByText('The extension non-existent-view could not be loaded.'),
-  ).toBeInTheDocument();
-  expect(
-    getByText(/This may be due to the extension not being activated/),
-  ).toBeInTheDocument();
-});
-
-test('renders children correctly', () => {
-  const TestChild = () => <div data-test="test-child">Test Child</div>;
-
-  const { getByTestId } = render(
-    <ExtensionsProvider>
-      <TestChild />
-    </ExtensionsProvider>,
-  );
-
-  expect(getByTestId('test-child')).toBeInTheDocument();
-});
diff --git a/superset-frontend/src/extensions/ExtensionsContext.tsx b/superset-frontend/src/extensions/ExtensionsContext.tsx
deleted file mode 100644
index dadea48fe23..00000000000
--- a/superset-frontend/src/extensions/ExtensionsContext.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import {
-  createContext,
-  useContext,
-  useState,
-  ReactElement,
-  useEffect,
-  useMemo,
-} from 'react';
-import { ErrorBoundary } from 'src/components';
-import { setExtensionsContextValue } from './ExtensionsContextUtils';
-import ExtensionPlaceholder from './ExtensionPlaceholder';
-
-export interface ExtensionsContextType {
-  getView: (id: string) => ReactElement;
-  registerViewProvider: (id: string, viewProvider: () => ReactElement) => void;
-  unregisterViewProvider: (id: string) => void;
-}
-
-const ExtensionsContext = createContext<ExtensionsContextType | undefined>(
-  undefined,
-);
-
-export const ExtensionsProvider: React.FC = ({ children }) => {
-  const [viewProviders, setViewProviders] = useState<{
-    [id: string]: () => ReactElement;
-  }>({});
-
-  const registerViewProvider = (
-    id: string,
-    viewProvider: () => ReactElement,
-  ) => {
-    setViewProviders(prev => ({ ...prev, [id]: viewProvider }));
-  };
-
-  const unregisterViewProvider = (id: string) => {
-    setViewProviders(prev => {
-      const { [id]: _, ...rest } = prev;
-      return rest;
-    });
-  };
-
-  const getView = (id: string) => {
-    const viewProvider = viewProviders[id];
-    if (viewProvider) {
-      return <ErrorBoundary>{viewProvider()}</ErrorBoundary>;
-    }
-    return <ExtensionPlaceholder id={id} />;
-  };
-
-  const contextValue = useMemo(
-    () => ({ getView, registerViewProvider, unregisterViewProvider }),
-    [viewProviders],
-  );
-
-  return (
-    <ExtensionsContext.Provider value={contextValue}>
-      {children}
-    </ExtensionsContext.Provider>
-  );
-};
-
-export const useExtensionsContext = () => {
-  const context = useContext(ExtensionsContext);
-  if (!context) {
-    throw new Error(
-      'useExtensionsContext must be used within a ExtensionsProvider',
-    );
-  }
-
-  useEffect(() => {
-    setExtensionsContextValue(context);
-  }, [context]);
-
-  return context;
-};
diff --git a/superset-frontend/src/extensions/ExtensionsContextUtils.test.ts b/superset-frontend/src/extensions/ExtensionsContextUtils.test.ts
deleted file mode 100644
index ecd6b7950b0..00000000000
--- a/superset-frontend/src/extensions/ExtensionsContextUtils.test.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import {
-  setExtensionsContextValue,
-  getExtensionsContextValue,
-} from './ExtensionsContextUtils';
-import { ExtensionsContextType } from './ExtensionsContext';
-
-const mockExtensionsContext: ExtensionsContextType = {
-  getView: jest.fn(),
-  registerViewProvider: jest.fn(),
-  unregisterViewProvider: jest.fn(),
-};
-
-test('sets and gets extensions context value', () => {
-  setExtensionsContextValue(mockExtensionsContext);
-  const retrievedContext = getExtensionsContextValue();
-
-  expect(retrievedContext).toBe(mockExtensionsContext);
-  expect(retrievedContext.getView).toBe(mockExtensionsContext.getView);
-  expect(retrievedContext.registerViewProvider).toBe(
-    mockExtensionsContext.registerViewProvider,
-  );
-  expect(retrievedContext.unregisterViewProvider).toBe(
-    mockExtensionsContext.unregisterViewProvider,
-  );
-});
-
-test('throws error when getting context value before setting it', () => {
-  // Reset the context to null (this simulates initial state)
-  // We need to access the internal state, so we'll set it to a mock that will throw
-  expect(() => {
-    // Clear any previously set value by setting to null
-    (setExtensionsContextValue as any)(null);
-    getExtensionsContextValue();
-  }).toThrow('ExtensionsContext value is not set');
-});
-
-test('overwrites previous context value when setting new one', () => {
-  const firstContext: ExtensionsContextType = {
-    getView: jest.fn().mockReturnValue('first-view'),
-    registerViewProvider: jest.fn(),
-    unregisterViewProvider: jest.fn(),
-  };
-
-  const secondContext: ExtensionsContextType = {
-    getView: jest.fn().mockReturnValue('second-view'),
-    registerViewProvider: jest.fn(),
-    unregisterViewProvider: jest.fn(),
-  };
-
-  setExtensionsContextValue(firstContext);
-  expect(getExtensionsContextValue()).toBe(firstContext);
-
-  setExtensionsContextValue(secondContext);
-  expect(getExtensionsContextValue()).toBe(secondContext);
-  expect(getExtensionsContextValue().getView).toBe(secondContext.getView);
-});
diff --git a/superset-frontend/src/extensions/ExtensionsContextUtils.ts b/superset-frontend/src/extensions/ExtensionsContextUtils.ts
deleted file mode 100644
index 03dbdb71385..00000000000
--- a/superset-frontend/src/extensions/ExtensionsContextUtils.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { ExtensionsContextType } from './ExtensionsContext';
-
-let extensionsContextValue: ExtensionsContextType | null = null;
-
-export const setExtensionsContextValue = (value: ExtensionsContextType) => {
-  extensionsContextValue = value;
-};
-
-export const getExtensionsContextValue = () => {
-  if (!extensionsContextValue) {
-    throw new Error('ExtensionsContext value is not set');
-  }
-  return extensionsContextValue;
-};
diff --git a/superset-frontend/src/extensions/ExtensionsList.test.tsx b/superset-frontend/src/extensions/ExtensionsList.test.tsx
index 471a74cb523..ee2be4691d1 100644
--- a/superset-frontend/src/extensions/ExtensionsList.test.tsx
+++ b/superset-frontend/src/extensions/ExtensionsList.test.tsx
@@ -32,15 +32,11 @@ const mockInitialState = {
         id: 1,
         name: 'Test Extension 1',
         enabled: true,
-        contributions:
-          '{"menus": {"testMenu": {"primary": [{"key": "item1", "title": "Menu Item 1"}]}}, "views": {}}',
       },
       {
         id: 2,
         name: 'Test Extension 2',
         enabled: false,
-        contributions:
-          '{"commands": [{"command": "test.command", "title": "Test Command"}]}',
       },
     ],
     bulkSelectEnabled: false,
@@ -77,19 +73,6 @@ test('displays extension names in the list', async () => {
   });
 });
 
-test('displays contributions information', async () => {
-  renderWithStore();
-
-  await waitFor(
-    () => {
-      // Should show contributions-related content
-      const bodyText = document.body.textContent || '';
-      expect(bodyText).toMatch(/contribution/i);
-    },
-    { timeout: 4000 },
-  );
-});
-
 test('calls toast functions when provided', () => {
   const addDangerToast = jest.fn();
   const addSuccessToast = jest.fn();
diff --git a/superset-frontend/src/extensions/ExtensionsList.tsx b/superset-frontend/src/extensions/ExtensionsList.tsx
index f85c2cad702..bfcba1a4522 100644
--- a/superset-frontend/src/extensions/ExtensionsList.tsx
+++ b/superset-frontend/src/extensions/ExtensionsList.tsx
@@ -17,14 +17,11 @@
  * under the License.
  */
 import { t } from '@apache-superset/core';
-import { useTheme, css } from '@apache-superset/core/ui';
 import { FunctionComponent, useMemo } from 'react';
 import { useListViewResource } from 'src/views/CRUD/hooks';
 import { ListView } from 'src/components';
 import SubMenu, { SubMenuProps } from 'src/features/home/SubMenu';
 import withToasts from 'src/components/MessageToasts/withToasts';
-import { JsonModal } from 'src/components/JsonModal';
-import { safeJsonObjectParse } from 'src/components/JsonModal/utils';
 
 const PAGE_SIZE = 25;
 
@@ -43,8 +40,6 @@ const ExtensionsList: FunctionComponent<ExtensionsListProps> = ({
   addDangerToast,
   addSuccessToast,
 }) => {
-  const theme = useTheme();
-
   const {
     state: { loading, resourceCount, resourceCollection },
     fetchData,
@@ -68,30 +63,6 @@ const ExtensionsList: FunctionComponent<ExtensionsListProps> = ({
           },
         }: any) => name,
       },
-      {
-        Header: t('Contributions'),
-        accessor: 'contributions',
-        size: 'lg',
-        id: 'contributions',
-        Cell: ({
-          row: {
-            original: { contributions },
-          },
-        }: any) => (
-          <div
-            css={css`
-              color: ${theme.colorLink};
-              text-decoration: underline;
-            `}
-          >
-            <JsonModal
-              modalTitle={t('Contributions')}
-              jsonObject={safeJsonObjectParse(contributions)!}
-              jsonValue={t('View')}
-            />
-          </div>
-        ),
-      },
     ],
     [loading], // We need to monitor loading to avoid stale state in actions
   );
diff --git a/superset-frontend/src/extensions/ExtensionsLoader.test.ts b/superset-frontend/src/extensions/ExtensionsLoader.test.ts
new file mode 100644
index 00000000000..fe6a5d8c554
--- /dev/null
+++ b/superset-frontend/src/extensions/ExtensionsLoader.test.ts
@@ -0,0 +1,113 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { logging } from '@apache-superset/core';
+import type { core } from '@apache-superset/core';
+import ExtensionsLoader from './ExtensionsLoader';
+
+type Extension = core.Extension;
+
+function createMockExtension(overrides: Partial<Extension> = {}): Extension {
+  return {
+    id: 'test-extension',
+    name: 'Test Extension',
+    description: 'A test extension',
+    version: '1.0.0',
+    dependencies: [],
+    remoteEntry: '',
+    extensionDependencies: [],
+    ...overrides,
+  };
+}
+
+beforeEach(() => {
+  (ExtensionsLoader as any).instance = undefined;
+});
+
+test('creates a singleton instance', () => {
+  const instance1 = ExtensionsLoader.getInstance();
+  const instance2 = ExtensionsLoader.getInstance();
+  expect(instance1).toBe(instance2);
+});
+
+test('returns empty array when no extensions registered', () => {
+  const loader = ExtensionsLoader.getInstance();
+  expect(loader.getExtensions()).toEqual([]);
+});
+
+test('stores extension after initialization', async () => {
+  const loader = ExtensionsLoader.getInstance();
+  const ext = createMockExtension({ id: 'my-ext' });
+  await loader.initializeExtension(ext);
+  expect(loader.getExtension('my-ext')).toEqual(ext);
+});
+
+test('returns undefined for non-existent extension', () => {
+  const loader = ExtensionsLoader.getInstance();
+  expect(loader.getExtension('nonexistent')).toBeUndefined();
+});
+
+test('stores multiple extensions', async () => {
+  const loader = ExtensionsLoader.getInstance();
+  await loader.initializeExtension(createMockExtension({ id: 'ext-1' }));
+  await loader.initializeExtension(createMockExtension({ id: 'ext-2' }));
+  expect(loader.getExtensions()).toHaveLength(2);
+  expect(loader.getExtension('ext-1')).toBeDefined();
+  expect(loader.getExtension('ext-2')).toBeDefined();
+});
+
+test('initializes extension without remoteEntry', async () => {
+  const loader = ExtensionsLoader.getInstance();
+  const ext = createMockExtension({ id: 'local-ext', remoteEntry: '' });
+  await loader.initializeExtension(ext);
+  expect(loader.getExtension('local-ext')).toEqual(ext);
+});
+
+test('handles initialization errors gracefully', async () => {
+  const loader = ExtensionsLoader.getInstance();
+  const errorSpy = jest.spyOn(logging, 'error').mockImplementation();
+
+  // Intercept script element creation so onerror fires in jsdom
+  const appendChildSpy = jest
+    .spyOn(document.head, 'appendChild')
+    .mockImplementation((element: Node) => {
+      if (element instanceof HTMLScriptElement && element.onerror) {
+        setTimeout(() => {
+          (element.onerror as any)('Script load error');
+        }, 0);
+      }
+      return element;
+    });
+
+  const ext = createMockExtension({
+    id: 'broken-ext',
+    remoteEntry: 'http://broken-url/remoteEntry.js',
+  });
+
+  await loader.initializeExtension(ext);
+
+  expect(errorSpy).toHaveBeenCalledWith(
+    expect.stringContaining('Failed to initialize extension'),
+    expect.anything(),
+  );
+  // Extension should not be stored on failure
+  expect(loader.getExtension('broken-ext')).toBeUndefined();
+
+  errorSpy.mockRestore();
+  appendChildSpy.mockRestore();
+});
diff --git a/superset-frontend/src/extensions/ExtensionsLoader.ts b/superset-frontend/src/extensions/ExtensionsLoader.ts
new file mode 100644
index 00000000000..65068275382
--- /dev/null
+++ b/superset-frontend/src/extensions/ExtensionsLoader.ts
@@ -0,0 +1,161 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { SupersetClient } from '@superset-ui/core';
+import { logging } from '@apache-superset/core';
+import type { core } from '@apache-superset/core';
+
+type Extension = core.Extension;
+
+/**
+ * Loads extension modules via webpack module federation.
+ *
+ * Extensions register their contributions (commands, views, menus, editors)
+ * as module-level side effects when their module is imported. The loader
+ * only handles fetching and importing the remote modules.
+ */
+class ExtensionsLoader {
+  private static instance: ExtensionsLoader;
+
+  private extensionIndex: Map<string, Extension> = new Map();
+
+  // eslint-disable-next-line no-useless-constructor
+  private constructor() {
+    // Private constructor for singleton pattern
+  }
+
+  /**
+   * Singleton instance getter.
+   * @returns The singleton instance of ExtensionsLoader.
+   */
+  public static getInstance(): ExtensionsLoader {
+    if (!ExtensionsLoader.instance) {
+      ExtensionsLoader.instance = new ExtensionsLoader();
+    }
+    return ExtensionsLoader.instance;
+  }
+
+  /**
+   * Initializes extensions by fetching the list from the API and loading each one.
+   * @throws Error if initialization fails.
+   */
+  public async initializeExtensions(): Promise<void> {
+    const response = await SupersetClient.get({
+      endpoint: '/api/v1/extensions/',
+    });
+    const extensions: Extension[] = response.json.result;
+    await Promise.all(
+      extensions.map(async extension => {
+        await this.initializeExtension(extension);
+      }),
+    );
+  }
+
+  /**
+   * Initializes a single extension.
+   * If the extension has a remote entry, loads the module (which triggers
+   * side-effect registrations for commands, views, menus, and editors).
+   * @param extension The extension to initialize.
+   */
+  public async initializeExtension(extension: Extension) {
+    try {
+      if (extension.remoteEntry) {
+        await this.loadModule(extension);
+      }
+      this.extensionIndex.set(extension.id, extension);
+    } catch (error) {
+      logging.error(
+        `Failed to initialize extension ${extension.name}\n`,
+        error,
+      );
+    }
+  }
+
+  /**
+   * Loads a single extension module via webpack module federation.
+   * The module's top-level side effects fire contribution registrations.
+   * @param extension The extension to load.
+   */
+  private async loadModule(extension: Extension): Promise<void> {
+    const { remoteEntry, id } = extension;
+
+    // Load the remote entry script
+    await new Promise<void>((resolve, reject) => {
+      const element = document.createElement('script');
+      element.src = remoteEntry;
+      element.type = 'text/javascript';
+      element.async = true;
+      element.onload = () => resolve();
+      element.onerror = (
+        event: Event | string,
+        source?: string,
+        lineno?: number,
+        colno?: number,
+        error?: Error,
+      ) => {
+        const errorDetails = [];
+        if (source) errorDetails.push(`source: ${source}`);
+        if (lineno !== undefined) errorDetails.push(`line: ${lineno}`);
+        if (colno !== undefined) errorDetails.push(`column: ${colno}`);
+        if (error?.message) errorDetails.push(`error: ${error.message}`);
+        if (typeof event === 'string') errorDetails.push(`event: ${event}`);
+
+        const detailsStr =
+          errorDetails.length > 0 ? `\n${errorDetails.join(', ')}` : '';
+        const errorMessage = `Failed to load remote entry: ${remoteEntry}${detailsStr}`;
+
+        return reject(new Error(errorMessage));
+      };
+
+      document.head.appendChild(element);
+    });
+
+    // Initialize Webpack module federation
+    // @ts-expect-error
+    await __webpack_init_sharing__('default');
+    // Use moduleFederationName (camelCase) for webpack container access, fallback to id for compatibility
+    const containerName = (extension as any).moduleFederationName || id;
+    const container = (window as any)[containerName];
+
+    // @ts-expect-error
+    await container.init(__webpack_share_scopes__.default);
+
+    const factory = await container.get('./index');
+    // Execute the module factory - side effects fire registrations
+    factory();
+  }
+
+  /**
+   * Retrieves all extensions.
+   * @returns An array of all registered extensions.
+   */
+  public getExtensions(): Extension[] {
+    return Array.from(this.extensionIndex.values());
+  }
+
+  /**
+   * Retrieves a specific extension by its id.
+   * @param id The id of the extension.
+   * @returns The extension matching the id, or undefined if not found.
+   */
+  public getExtension(id: string): Extension | undefined {
+    return this.extensionIndex.get(id);
+  }
+}
+
+export default ExtensionsLoader;
diff --git a/superset-frontend/src/extensions/ExtensionsManager.test.ts b/superset-frontend/src/extensions/ExtensionsManager.test.ts
deleted file mode 100644
index b98e2dc08ee..00000000000
--- a/superset-frontend/src/extensions/ExtensionsManager.test.ts
+++ /dev/null
@@ -1,572 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import fetchMock from 'fetch-mock';
-import type { contributions, core } from '@apache-superset/core';
-import ExtensionsManager from './ExtensionsManager';
-
-// Type-safe mock data generators
-interface MockExtensionOptions {
-  id?: string;
-  name?: string;
-  description?: string;
-  version?: string;
-  dependencies?: string[];
-  remoteEntry?: string;
-  exposedModules?: string[];
-  extensionDependencies?: string[];
-  commands?: contributions.CommandContribution[];
-  menus?: Record<string, Record<string, contributions.MenuContribution>>;
-  views?: Record<string, Record<string, contributions.ViewContribution[]>>;
-  includeMockFunctions?: boolean;
-}
-
-/**
- * Creates a mock extension with proper typing and default values
- */
-function createMockExtension(
-  options: MockExtensionOptions = {},
-): core.Extension {
-  const {
-    id = 'test-extension',
-    name = 'Test Extension',
-    description = 'A test extension',
-    version = '1.0.0',
-    dependencies = [],
-    remoteEntry = '',
-    exposedModules = [],
-    extensionDependencies = [],
-    commands = [],
-    menus = {},
-    views = {},
-    includeMockFunctions = true,
-  } = options;
-
-  const extension: core.Extension = {
-    id,
-    name,
-    description,
-    version,
-    dependencies,
-    remoteEntry,
-    exposedModules,
-    extensionDependencies,
-    contributions: {
-      commands,
-      menus,
-      views,
-    },
-    activate: includeMockFunctions ? jest.fn() : undefined!,
-    deactivate: includeMockFunctions ? jest.fn() : undefined!,
-  };
-
-  return extension;
-}
-
-/**
- * Creates a mock command contribution with proper typing
- */
-function createMockCommand(
-  command: string,
-  overrides: Partial<contributions.CommandContribution> = {},
-): contributions.CommandContribution {
-  return {
-    command,
-    icon: `${command}-icon`,
-    title: `${command} Command`,
-    description: `A ${command} command`,
-    ...overrides,
-  };
-}
-
-/**
- * Creates a mock menu contribution with proper typing
- */
-function createMockMenu(
-  overrides: Partial<contributions.MenuContribution> = {},
-): contributions.MenuContribution {
-  return {
-    context: [],
-    primary: [],
-    secondary: [],
-    ...overrides,
-  };
-}
-
-/**
- * Creates a mock view contribution with proper typing
- */
-function createMockView(
-  id: string,
-  overrides: Partial<contributions.ViewContribution> = {},
-): contributions.ViewContribution {
-  return {
-    id,
-    name: `${id} View`,
-    ...overrides,
-  };
-}
-
-/**
- * Creates a mock menu item with proper typing
- */
-function createMockMenuItem(
-  view: string,
-  command: string,
-  overrides: Partial<contributions.MenuItem> = {},
-): contributions.MenuItem {
-  return {
-    view,
-    command,
-    ...overrides,
-  };
-}
-
-/**
- * Sets up an activated extension in the manager by manually adding context and contributions
- * This simulates what happens when an extension is properly enabled
- */
-function setupActivatedExtension(
-  manager: ExtensionsManager,
-  extension: core.Extension,
-  contextOverrides: Partial<{ disposables: { dispose: () => void }[] }> = {},
-) {
-  const context = { disposables: [], ...contextOverrides };
-  (manager as any).contextIndex.set(extension.id, context);
-  (manager as any).extensionContributions.set(extension.id, {
-    commands: extension.contributions.commands,
-    menus: extension.contributions.menus,
-    views: extension.contributions.views,
-  });
-}
-
-/**
- * Creates a fully initialized and activated extension for testing
- */
-async function createActivatedExtension(
-  manager: ExtensionsManager,
-  extensionOptions: MockExtensionOptions = {},
-  contextOverrides: Partial<{ disposables: { dispose: () => void }[] }> = {},
-): Promise<core.Extension> {
-  const mockExtension = createMockExtension({
-    ...extensionOptions,
-  });
-
-  await manager.initializeExtension(mockExtension);
-  setupActivatedExtension(manager, mockExtension, contextOverrides);
-
-  return mockExtension;
-}
-
-/**
- * Creates multiple activated extensions for testing
- */
-async function createMultipleActivatedExtensions(
-  manager: ExtensionsManager,
-  extensionConfigs: MockExtensionOptions[],
-): Promise<core.Extension[]> {
-  const extensionPromises = extensionConfigs.map(config =>
-    createActivatedExtension(manager, config),
-  );
-
-  return Promise.all(extensionPromises);
-}
-
-/**
- * Common assertions for deactivation success
- */
-function expectSuccessfulDeactivation(
-  result: boolean,
-  mockExtension?: core.Extension,
-  expectedDeactivateCalls = 1,
-) {
-  expect(result).toBe(true);
-  if (mockExtension && mockExtension.deactivate) {
-    expect(mockExtension.deactivate).toHaveBeenCalledTimes(
-      expectedDeactivateCalls,
-    );
-  }
-}
-
-/**
- * Common assertions for deactivation failure
- */
-function expectFailedDeactivation(result: boolean) {
-  expect(result).toBe(false);
-}
-
-beforeEach(() => {
-  // Clear any existing instance
-  (ExtensionsManager as any).instance = undefined;
-
-  // Setup fetch mocks for API calls
-  fetchMock.removeRoutes().clearHistory();
-  fetchMock.put('glob:*/api/v1/extensions/*', { ok: true });
-  fetchMock.delete('glob:*/api/v1/extensions/*', { ok: true });
-  fetchMock.get('glob:*/api/v1/extensions/', {
-    json: { result: [] },
-  });
-  fetchMock.get('glob:*/api/v1/extensions/*', {
-    json: {
-      result: createMockExtension({ includeMockFunctions: false }),
-    },
-  });
-});
-
-afterEach(() => {
-  // Clean up after each test
-  (ExtensionsManager as any).instance = undefined;
-  fetchMock.removeRoutes().clearHistory();
-});
-
-test('creates singleton instance', () => {
-  const manager1 = ExtensionsManager.getInstance();
-  const manager2 = ExtensionsManager.getInstance();
-
-  expect(manager1).toBe(manager2);
-  expect(manager1).toBeInstanceOf(ExtensionsManager);
-});
-
-test('singleton maintains state across multiple getInstance calls', async () => {
-  const manager1 = ExtensionsManager.getInstance();
-  const mockExtension = createMockExtension();
-
-  await manager1.initializeExtension(mockExtension);
-
-  const manager2 = ExtensionsManager.getInstance();
-  const extensions = manager2.getExtensions();
-
-  expect(extensions).toHaveLength(1);
-  expect(extensions[0]).toEqual(mockExtension);
-});
-
-test('returns empty array for getExtensions initially', () => {
-  const manager = ExtensionsManager.getInstance();
-  const extensions = manager.getExtensions();
-
-  expect(Array.isArray(extensions)).toBe(true);
-  expect(extensions).toHaveLength(0);
-});
-test('returns undefined for non-existent extension', () => {
-  const manager = ExtensionsManager.getInstance();
-  const extension = manager.getExtension('non-existent-extension');
-
-  expect(extension).toBeUndefined();
-});
-
-test('can store and retrieve extensions using initializeExtension', async () => {
-  const manager = ExtensionsManager.getInstance();
-  const mockExtension = createMockExtension();
-
-  await manager.initializeExtension(mockExtension);
-
-  const extensions = manager.getExtensions();
-  expect(extensions).toHaveLength(1);
-  expect(extensions[0]).toEqual(mockExtension);
-
-  const retrievedExtension = manager.getExtension('test-extension');
-  expect(retrievedExtension).toEqual(mockExtension);
-});
-
-test('handles multiple extensions', async () => {
-  const manager = ExtensionsManager.getInstance();
-
-  const extension1 = createMockExtension({
-    id: 'extension-1',
-    name: 'Extension 1',
-  });
-
-  const extension2 = createMockExtension({
-    id: 'extension-2',
-    name: 'Extension 2',
-  });
-
-  await manager.initializeExtension(extension1);
-  await manager.initializeExtension(extension2);
-
-  const extensions = manager.getExtensions();
-  expect(extensions).toHaveLength(2);
-
-  expect(manager.getExtension('extension-1')).toEqual(extension1);
-  expect(manager.getExtension('extension-2')).toEqual(extension2);
-
-  expect(manager.getExtension('extension-1')?.name).toBe('Extension 1');
-  expect(manager.getExtension('extension-2')?.name).toBe('Extension 2');
-});
-
-test('initializeExtension properly stores extension in manager', async () => {
-  const manager = ExtensionsManager.getInstance();
-
-  const mockExtension = createMockExtension({
-    id: 'test-extension-init',
-    name: 'Test Extension',
-    description: 'A test extension for initialization',
-  });
-
-  expect(manager.getExtension('test-extension-init')).toBeUndefined();
-  expect(manager.getExtensions()).toHaveLength(0);
-
-  await manager.initializeExtension(mockExtension);
-
-  expect(manager.getExtension('test-extension-init')).toBeDefined();
-  expect(manager.getExtensions()).toHaveLength(1);
-  expect(manager.getExtension('test-extension-init')?.name).toBe(
-    'Test Extension',
-  );
-  expect(manager.getExtension('test-extension-init')?.description).toBe(
-    'A test extension for initialization',
-  );
-});
-
-test('initializeExtension handles extension without remoteEntry', async () => {
-  const manager = ExtensionsManager.getInstance();
-
-  const mockExtension = createMockExtension({
-    id: 'simple-extension',
-    name: 'Simple Extension',
-    description: 'Extension without remote entry',
-    remoteEntry: '',
-    commands: [createMockCommand('simple.command')],
-  });
-
-  expect(manager.getExtension('simple-extension')).toBeUndefined();
-
-  await manager.initializeExtension(mockExtension);
-
-  expect(manager.getExtension('simple-extension')).toBeDefined();
-  expect(manager.getExtensions()).toHaveLength(1);
-  expect(manager.getExtension('simple-extension')?.name).toBe(
-    'Simple Extension',
-  );
-
-  // Since extension has no remoteEntry, activate should not be called
-  expect(mockExtension.activate).not.toHaveBeenCalled();
-});
-
-test('getMenuContributions returns undefined initially', () => {
-  const manager = ExtensionsManager.getInstance();
-  const menuContributions = manager.getMenuContributions('non.existent');
-
-  expect(menuContributions).toBeUndefined();
-});
-
-test('getViewContributions returns undefined initially', () => {
-  const manager = ExtensionsManager.getInstance();
-  const viewContributions = manager.getViewContributions('non.existent');
-
-  expect(viewContributions).toBeUndefined();
-});
-
-test('getCommandContributions returns empty array initially', () => {
-  const manager = ExtensionsManager.getInstance();
-  const commandContributions = manager.getCommandContributions();
-
-  expect(Array.isArray(commandContributions)).toBe(true);
-  expect(commandContributions).toHaveLength(0);
-});
-
-test('getCommandContribution returns undefined for non-existent command', () => {
-  const manager = ExtensionsManager.getInstance();
-  const command = manager.getCommandContribution('nonexistent.command');
-
-  expect(command).toBeUndefined();
-});
-
-test('deactivateExtension successfully deactivates an enabled extension', async () => {
-  const manager = ExtensionsManager.getInstance();
-  const mockExtension = await createActivatedExtension(manager, {
-    commands: [createMockCommand('test.command')],
-  });
-
-  // Verify extension has contributions after setup
-  expect(manager.getCommandContributions()).toHaveLength(1);
-
-  // Deactivate the extension
-  const result = manager.deactivateExtension('test-extension');
-
-  expectSuccessfulDeactivation(result, mockExtension);
-});
-
-test('deactivateExtension disposes of context disposables', async () => {
-  const manager = ExtensionsManager.getInstance();
-  const mockDisposable = { dispose: jest.fn() };
-
-  await createActivatedExtension(
-    manager,
-    {},
-    {
-      disposables: [mockDisposable],
-    },
-  );
-
-  // Verify disposable is not yet disposed
-  expect(mockDisposable.dispose).not.toHaveBeenCalled();
-
-  // Deactivate the extension
-  const result = manager.deactivateExtension('test-extension');
-
-  expectSuccessfulDeactivation(result);
-  expect(mockDisposable.dispose).toHaveBeenCalledTimes(1);
-});
-
-test('deactivateExtension handles extension without deactivate function', async () => {
-  const manager = ExtensionsManager.getInstance();
-  await createActivatedExtension(manager, {
-    includeMockFunctions: false, // Don't create mock functions
-  });
-
-  // Deactivate should still return true even without deactivate function
-  const result = manager.deactivateExtension('test-extension');
-
-  expectSuccessfulDeactivation(result);
-});
-
-test('deactivateExtension returns false for non-existent extension', () => {
-  const manager = ExtensionsManager.getInstance();
-
-  const result = manager.deactivateExtension('non-existent-extension');
-
-  expectFailedDeactivation(result);
-});
-
-test('deactivateExtension returns false for extension without context', async () => {
-  const manager = ExtensionsManager.getInstance();
-  const mockExtension = createMockExtension({
-    // Extension without context created
-  });
-
-  await manager.initializeExtension(mockExtension);
-
-  const result = manager.deactivateExtension('test-extension');
-
-  expectFailedDeactivation(result);
-});
-
-test('deactivateExtension handles errors during deactivation gracefully', async () => {
-  const manager = ExtensionsManager.getInstance();
-  const mockExtension = await createActivatedExtension(manager);
-
-  // Override the deactivate function to throw an error
-  mockExtension.deactivate = jest.fn(() => {
-    throw new Error('Deactivation error');
-  });
-
-  // Should return false when deactivation throws an error
-  const result = manager.deactivateExtension('test-extension');
-
-  expectFailedDeactivation(result);
-  expect(mockExtension.deactivate).toHaveBeenCalledTimes(1);
-});
-
-test('deactivateExtension handles errors during dispose gracefully', async () => {
-  const manager = ExtensionsManager.getInstance();
-  const mockDisposable = {
-    dispose: jest.fn(() => {
-      throw new Error('Dispose error');
-    }),
-  };
-
-  await createActivatedExtension(
-    manager,
-    {},
-    {
-      disposables: [mockDisposable],
-    },
-  );
-
-  // Should return false when disposal throws an error
-  const result = manager.deactivateExtension('test-extension');
-
-  expectFailedDeactivation(result);
-  expect(mockDisposable.dispose).toHaveBeenCalledTimes(1);
-});
-
-test('handles contributions with menu items', async () => {
-  const manager = ExtensionsManager.getInstance();
-
-  await createActivatedExtension(manager, {
-    commands: [
-      createMockCommand('ext1.command1'),
-      createMockCommand('ext1.command2'),
-    ],
-    menus: {
-      test: {
-        menu: createMockMenu({
-          primary: [
-            createMockMenuItem('test-view', 'ext1.command1'),
-            createMockMenuItem('test-view2', 'ext1.command2'),
-          ],
-          secondary: [createMockMenuItem('test-view3', 'ext1.command1')],
-        }),
-      },
-    },
-    views: {
-      test: {
-        view: [createMockView('test-view-1'), createMockView('test-view-2')],
-      },
-    },
-  });
-
-  // Test command contributions
-  const commands = manager.getCommandContributions();
-  expect(commands).toHaveLength(2);
-  expect(commands.find(cmd => cmd.command === 'ext1.command1')).toBeDefined();
-  expect(commands.find(cmd => cmd.command === 'ext1.command2')).toBeDefined();
-
-  // Test menu contributions
-  const menuContributions = manager.getMenuContributions('test.menu');
-  expect(menuContributions).toBeDefined();
-  expect(menuContributions?.primary).toHaveLength(2);
-  expect(menuContributions?.secondary).toHaveLength(1);
-
-  // Test view contributions
-  const viewContributions = manager.getViewContributions('test.view');
-  expect(viewContributions).toBeDefined();
-  expect(viewContributions).toHaveLength(2);
-});
-
-test('handles non-existent menu and view contributions', () => {
-  const manager = ExtensionsManager.getInstance();
-
-  expect(manager.getMenuContributions('non.existent')).toBeUndefined();
-  expect(manager.getViewContributions('non.existent')).toBeUndefined();
-  expect(manager.getCommandContribution('nonexistent.command')).toBeUndefined();
-});
-
-test('merges contributions from multiple extensions', async () => {
-  const manager = ExtensionsManager.getInstance();
-
-  await createMultipleActivatedExtensions(manager, [
-    {
-      id: 'extension-1',
-      name: 'Extension 1',
-      commands: [createMockCommand('ext1.command')],
-    },
-    {
-      id: 'extension-2',
-      name: 'Extension 2',
-      commands: [createMockCommand('ext2.command')],
-    },
-  ]);
-
-  const commands = manager.getCommandContributions();
-  expect(commands).toHaveLength(2);
-
-  expect(manager.getCommandContribution('ext1.command')).toBeDefined();
-  expect(manager.getCommandContribution('ext2.command')).toBeDefined();
-});
diff --git a/superset-frontend/src/extensions/ExtensionsManager.ts b/superset-frontend/src/extensions/ExtensionsManager.ts
deleted file mode 100644
index d546b72debc..00000000000
--- a/superset-frontend/src/extensions/ExtensionsManager.ts
+++ /dev/null
@@ -1,355 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { SupersetClient } from '@superset-ui/core';
-import { logging } from '@apache-superset/core';
-import type { contributions, core } from '@apache-superset/core';
-import { ExtensionContext } from '../core/models';
-
-type MenuContribution = contributions.MenuContribution;
-type MenuContributions = contributions.MenuContributions;
-type ViewContribution = contributions.ViewContribution;
-type ViewContributions = contributions.ViewContributions;
-type CommandContribution = contributions.CommandContribution;
-type EditorContribution = contributions.EditorContribution;
-type Extension = core.Extension;
-
-class ExtensionsManager {
-  private static instance: ExtensionsManager;
-
-  private extensionIndex: Map<string, Extension> = new Map();
-
-  private contextIndex: Map<string, ExtensionContext> = new Map();
-
-  private extensionContributions: Map<
-    string,
-    {
-      menus?: MenuContributions;
-      views?: ViewContributions;
-      commands?: CommandContribution[];
-      editors?: EditorContribution[];
-    }
-  > = new Map();
-
-  // eslint-disable-next-line no-useless-constructor
-  private constructor() {
-    // Private constructor for singleton pattern
-  }
-
-  /**
-   * Singleton instance getter.
-   * @returns The singleton instance of ExtensionsManager.
-   */
-  public static getInstance(): ExtensionsManager {
-    if (!ExtensionsManager.instance) {
-      ExtensionsManager.instance = new ExtensionsManager();
-    }
-    return ExtensionsManager.instance;
-  }
-
-  /**
-   * Initializes extensions.
-   * @throws Error if initialization fails.
-   */
-  public async initializeExtensions(): Promise<void> {
-    const response = await SupersetClient.get({
-      endpoint: '/api/v1/extensions/',
-    });
-    const extensions: Extension[] = response.json.result;
-    await Promise.all(
-      extensions.map(async extension => {
-        await this.initializeExtension(extension);
-      }),
-    );
-  }
-
-  /**
-   * Initializes an extension by its instance.
-   * If the extension has a remote entry, it will load the module.
-   * @param extension The extension to initialize.
-   */
-  public async initializeExtension(extension: Extension) {
-    try {
-      let loadedExtension = extension;
-      if (extension.remoteEntry) {
-        loadedExtension = await this.loadModule(extension);
-        this.enableExtension(loadedExtension);
-      }
-      this.extensionIndex.set(loadedExtension.id, loadedExtension);
-    } catch (error) {
-      logging.error(
-        `Failed to initialize extension ${extension.name}\n`,
-        error,
-      );
-    }
-  }
-
-  /**
-   * Enables an extension by its instance.
-   * @param extension The extension to enable.
-   */
-  private enableExtension(extension: Extension): void {
-    const { id } = extension;
-    if (extension && typeof extension.activate === 'function') {
-      // If already enabled, do nothing
-      if (this.contextIndex.has(id)) {
-        return;
-      }
-      const context = new ExtensionContext();
-      this.contextIndex.set(id, context);
-      // TODO: Activate based on activation events
-      this.activateExtension(extension, context);
-      this.indexContributions(extension);
-    }
-  }
-
-  /**
-   * Loads a single extension module.
-   * @param extension The extension to load.
-   * @returns The loaded extension with activate and deactivate methods.
-   */
-  private async loadModule(extension: Extension): Promise<Extension> {
-    const { remoteEntry, id, exposedModules } = extension;
-
-    // Load the remote entry script
-    await new Promise<void>((resolve, reject) => {
-      const element = document.createElement('script');
-      element.src = remoteEntry;
-      element.type = 'text/javascript';
-      element.async = true;
-      element.onload = () => resolve();
-      element.onerror = (
-        event: Event | string,
-        source?: string,
-        lineno?: number,
-        colno?: number,
-        error?: Error,
-      ) => {
-        const errorDetails = [];
-        if (source) errorDetails.push(`source: ${source}`);
-        if (lineno !== undefined) errorDetails.push(`line: ${lineno}`);
-        if (colno !== undefined) errorDetails.push(`column: ${colno}`);
-        if (error?.message) errorDetails.push(`error: ${error.message}`);
-        if (typeof event === 'string') errorDetails.push(`event: ${event}`);
-
-        const detailsStr =
-          errorDetails.length > 0 ? `\n${errorDetails.join(', ')}` : '';
-        const errorMessage = `Failed to load remote entry: ${remoteEntry}${detailsStr}`;
-
-        return reject(new Error(errorMessage));
-      };
-
-      document.head.appendChild(element);
-    });
-
-    // Initialize Webpack module federation
-    // @ts-expect-error
-    await __webpack_init_sharing__('default');
-    // Use moduleFederationName (camelCase) for webpack container access, fallback to id for compatibility
-    const containerName = (extension as any).moduleFederationName || id;
-    const container = (window as any)[containerName];
-
-    // @ts-expect-error
-    await container.init(__webpack_share_scopes__.default);
-
-    const factory = await container.get(exposedModules[0]);
-    const Module = factory();
-    return {
-      ...extension,
-      activate: Module.activate,
-      deactivate: Module.deactivate,
-    };
-  }
-
-  /**
-   * Activates an extension if it has an activate method.
-   * @param extension The extension to activate.
-   * @param context The context to pass to the activate method.
-   */
-  public activateExtension(
-    extension: Extension,
-    context: ExtensionContext,
-  ): void {
-    if (extension.activate) {
-      try {
-        extension.activate(context);
-      } catch (err) {
-        logging.warn(`Error activating ${extension.name}`, err);
-      }
-    }
-  }
-
-  /**
-   * Deactivates an extension.
-   * @param id The id of the extension to deactivate.
-   * @returns True if deactivated, false otherwise.
-   */
-  public deactivateExtension(id: string): boolean {
-    const extension = this.extensionIndex.get(id);
-    const context = extension ? this.contextIndex.get(extension.id) : undefined;
-    if (extension && context) {
-      try {
-        // Dispose of all disposables in the context
-        if (context.disposables) {
-          context.disposables.forEach(d => d.dispose());
-          context.disposables = [];
-        }
-        if (typeof extension.deactivate === 'function') {
-          extension.deactivate();
-        }
-        return true;
-      } catch (err) {
-        logging.warn(`Error deactivating ${extension.name}`, err);
-      }
-    }
-    return false;
-  }
-
-  /**
-   * Indexes contributions from an extension for quick retrieval.
-   * @param extension The extension to index.
-   */
-  private indexContributions(extension: Extension): void {
-    const { contributions, id } = extension;
-    this.extensionContributions.set(id, {
-      menus: contributions.menus,
-      views: contributions.views,
-      commands: contributions.commands,
-      editors: contributions.editors,
-    });
-  }
-
-  /**
-   * Retrieves menu contributions for a specific key.
-   * @param key The key of the menu contributions in format "scope.location" (e.g., "sqllab.editor").
-   * @returns The menu contributions matching the key, or undefined if not found.
-   */
-  public getMenuContributions(key: string): MenuContribution | undefined {
-    const [scope, location] = key.split('.');
-    if (!scope || !location) {
-      return undefined;
-    }
-    const merged: MenuContribution = {
-      context: [],
-      primary: [],
-      secondary: [],
-    };
-    for (const ext of this.extensionContributions.values()) {
-      const scopeMenus = ext.menus?.[scope as keyof MenuContributions];
-      const menu =
-        scopeMenus?.[location as keyof NonNullable<typeof scopeMenus>];
-      if (menu) {
-        if (menu.context) merged.context!.push(...menu.context);
-        if (menu.primary) merged.primary!.push(...menu.primary);
-        if (menu.secondary) merged.secondary!.push(...menu.secondary);
-      }
-    }
-    if (
-      (merged.context?.length ?? 0) === 0 &&
-      (merged.primary?.length ?? 0) === 0 &&
-      (merged.secondary?.length ?? 0) === 0
-    ) {
-      return undefined;
-    }
-    return merged;
-  }
-
-  /**
-   * Retrieves view contributions for a specific key.
-   * @param key The key of the view contributions in format "scope.location" (e.g., "sqllab.panels").
-   * @returns An array of view contributions matching the key, or undefined if not found.
-   */
-  public getViewContributions(key: string): ViewContribution[] | undefined {
-    const [scope, location] = key.split('.');
-    if (!scope || !location) {
-      return undefined;
-    }
-    let result: ViewContribution[] = [];
-    for (const ext of this.extensionContributions.values()) {
-      const scopeViews = ext.views?.[scope as keyof ViewContributions];
-      const views =
-        scopeViews?.[location as keyof NonNullable<typeof scopeViews>];
-      if (views) {
-        result = result.concat(views);
-      }
-    }
-    return result.length > 0 ? result : undefined;
-  }
-
-  /**
-   * Retrieves all command contributions.
-   * @returns An array of all command contributions.
-   */
-  public getCommandContributions(): CommandContribution[] {
-    const result: CommandContribution[] = [];
-    for (const ext of this.extensionContributions.values()) {
-      if (ext.commands) {
-        result.push(...ext.commands);
-      }
-    }
-    return result;
-  }
-
-  /**
-   * Retrieves a specific command contribution by its key.
-   * @param key The key of the command contribution.
-   * @returns The command contribution matching the key, or undefined if not found.
-   */
-  public getCommandContribution(key: string): CommandContribution | undefined {
-    for (const ext of this.extensionContributions.values()) {
-      if (ext.commands) {
-        const found = ext.commands.find(cmd => cmd.command === key);
-        if (found) return found;
-      }
-    }
-    return undefined;
-  }
-
-  /**
-   * Retrieves all extensions.
-   * @returns An array of all registered extensions.
-   */
-  public getExtensions(): Extension[] {
-    return Array.from(this.extensionIndex.values());
-  }
-
-  /**
-   * Retrieves a specific extension by its id.
-   * @param id The id of the extension.
-   * @returns The extension matching the id, or undefined if not found.
-   */
-  public getExtension(id: string): Extension | undefined {
-    return this.extensionIndex.get(id);
-  }
-
-  /**
-   * Retrieves all editor contributions from all extensions.
-   * @returns An array of all editor contributions.
-   */
-  public getEditorContributions(): EditorContribution[] {
-    const result: EditorContribution[] = [];
-    for (const ext of this.extensionContributions.values()) {
-      if (ext.editors) {
-        result.push(...ext.editors);
-      }
-    }
-    return result;
-  }
-}
-
-export default ExtensionsManager;
diff --git a/superset-frontend/src/extensions/ExtensionsStartup.test.tsx b/superset-frontend/src/extensions/ExtensionsStartup.test.tsx
index dfa870e4766..944fe4b71fb 100644
--- a/superset-frontend/src/extensions/ExtensionsStartup.test.tsx
+++ b/superset-frontend/src/extensions/ExtensionsStartup.test.tsx
@@ -21,7 +21,7 @@ import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
 import { logging } from '@apache-superset/core';
 import fetchMock from 'fetch-mock';
 import ExtensionsStartup from './ExtensionsStartup';
-import ExtensionsManager from './ExtensionsManager';
+import ExtensionsLoader from './ExtensionsLoader';
 
 // Mock the isFeatureEnabled function
 jest.mock('@superset-ui/core', () => ({
@@ -46,8 +46,8 @@ beforeEach(() => {
   // Clear the window.superset object
   delete (window as any).superset;
 
-  // Clear any existing ExtensionsManager instance
-  (ExtensionsManager as any).instance = undefined;
+  // Clear any existing ExtensionsLoader instance
+  (ExtensionsLoader as any).instance = undefined;
 
   // Reset feature flag mock to enabled by default
   mockIsFeatureEnabled.mockReset();
@@ -63,7 +63,7 @@ beforeEach(() => {
 afterEach(() => {
   // Clean up after each test
   delete (window as any).superset;
-  (ExtensionsManager as any).instance = undefined;
+  (ExtensionsLoader as any).instance = undefined;
 
   // Reset mocks
   mockIsFeatureEnabled.mockReset();
@@ -76,15 +76,15 @@ test('renders without crashing', () => {
     initialState: mockInitialState,
   });
 
-  // Component renders null, so just check it doesn't throw
+  // Component renders null initially (before extensions are loaded)
   expect(true).toBe(true);
 });
 
 test('sets up global superset object when user is logged in', async () => {
   // Mock initializeExtensions to avoid API calls in this test
-  const manager = ExtensionsManager.getInstance();
+  const loader = ExtensionsLoader.getInstance();
   const initializeSpy = jest
-    .spyOn(manager, 'initializeExtensions')
+    .spyOn(loader, 'initializeExtensions')
     .mockImplementation(() => Promise.resolve());
 
   render(<ExtensionsStartup />, {
@@ -99,6 +99,8 @@ test('sets up global superset object when user is logged in', async () => {
     expect((window as any).superset.core).toBeDefined();
     expect((window as any).superset.commands).toBeDefined();
     expect((window as any).superset.extensions).toBeDefined();
+    expect((window as any).superset.menus).toBeDefined();
+    expect((window as any).superset.views).toBeDefined();
     expect((window as any).superset.sqlLab).toBeDefined();
   });
 
@@ -117,11 +119,11 @@ test('does not set up global superset object when user is not logged in', async
   });
 });
 
-test('initializes ExtensionsManager when user is logged in', async () => {
+test('initializes ExtensionsLoader when user is logged in', async () => {
   // Mock initializeExtensions to avoid API calls, but track that it was called
-  const manager = ExtensionsManager.getInstance();
+  const loader = ExtensionsLoader.getInstance();
   const initializeSpy = jest
-    .spyOn(manager, 'initializeExtensions')
+    .spyOn(loader, 'initializeExtensions')
     .mockImplementation(() => Promise.resolve());
 
   render(<ExtensionsStartup />, {
@@ -130,17 +132,17 @@ test('initializes ExtensionsManager when user is logged in', async () => {
   });
 
   await waitFor(() => {
-    // Verify ExtensionsManager initialization was called
+    // Verify ExtensionsLoader initialization was called
     expect(initializeSpy).toHaveBeenCalledTimes(1);
-    // The manager should exist and be ready to use
-    expect(manager).toBeDefined();
-    expect(manager.getExtensions).toBeDefined();
+    // The loader should exist and be ready to use
+    expect(loader).toBeDefined();
+    expect(loader.getExtensions).toBeDefined();
   });
 
   initializeSpy.mockRestore();
 });
 
-test('does not initialize ExtensionsManager when user is not logged in', async () => {
+test('does not initialize ExtensionsLoader when user is not logged in', async () => {
   render(<ExtensionsStartup />, {
     useRedux: true,
     initialState: mockInitialStateNoUser,
@@ -148,20 +150,20 @@ test('does not initialize ExtensionsManager when user is not logged in', async (
 
   // Wait for the useEffect to complete and verify no initialization happened
   await waitFor(() => {
-    const manager = ExtensionsManager.getInstance();
-    expect(manager).toBeDefined();
+    const loader = ExtensionsLoader.getInstance();
+    expect(loader).toBeDefined();
     // Since no initialization happened, there should be no extensions loaded initially
-    expect(manager.getExtensions()).toEqual([]);
+    expect(loader.getExtensions()).toEqual([]);
   });
 });
 
 test('only initializes once even with multiple renders', async () => {
-  // Track calls to the manager's public API
-  const manager = ExtensionsManager.getInstance();
-  const originalInitialize = manager.initializeExtensions;
+  // Track calls to the loader's public API
+  const loader = ExtensionsLoader.getInstance();
+  const originalInitialize = loader.initializeExtensions;
   let initializeCallCount = 0;
 
-  manager.initializeExtensions = jest.fn().mockImplementation(() => {
+  loader.initializeExtensions = jest.fn().mockImplementation(() => {
     initializeCallCount += 1;
     return Promise.resolve();
   });
@@ -187,10 +189,10 @@ test('only initializes once even with multiple renders', async () => {
   expect(initializeCallCount).toBe(1);
 
   // Restore original method
-  manager.initializeExtensions = originalInitialize;
+  loader.initializeExtensions = originalInitialize;
 });
 
-test('initializes ExtensionsManager and logs success when EnableExtensions feature flag is enabled', async () => {
+test('initializes ExtensionsLoader and logs success when EnableExtensions feature flag is enabled', async () => {
   // Ensure feature flag is enabled
   mockIsFeatureEnabled.mockImplementation(
     (flag: FeatureFlag) => flag === FeatureFlag.EnableExtensions,
@@ -199,8 +201,8 @@ test('initializes ExtensionsManager and logs success when EnableExtensions featu
   const infoSpy = jest.spyOn(logging, 'info').mockImplementation();
 
   // Mock the initializeExtensions method to succeed
-  const originalInitialize = ExtensionsManager.prototype.initializeExtensions;
-  ExtensionsManager.prototype.initializeExtensions = jest
+  const originalInitialize = ExtensionsLoader.prototype.initializeExtensions;
+  ExtensionsLoader.prototype.initializeExtensions = jest
     .fn()
     .mockImplementation(() => Promise.resolve());
 
@@ -216,7 +218,7 @@ test('initializes ExtensionsManager and logs success when EnableExtensions featu
     );
     // Verify initialization was called
     expect(
-      ExtensionsManager.prototype.initializeExtensions,
+      ExtensionsLoader.prototype.initializeExtensions,
     ).toHaveBeenCalledTimes(1);
     // Verify success message was logged
     expect(infoSpy).toHaveBeenCalledWith(
@@ -225,17 +227,17 @@ test('initializes ExtensionsManager and logs success when EnableExtensions featu
   });
 
   // Restore original method
-  ExtensionsManager.prototype.initializeExtensions = originalInitialize;
+  ExtensionsLoader.prototype.initializeExtensions = originalInitialize;
   infoSpy.mockRestore();
 });
 
-test('does not initialize ExtensionsManager when EnableExtensions feature flag is disabled', async () => {
+test('does not initialize ExtensionsLoader when EnableExtensions feature flag is disabled', async () => {
   // Disable the feature flag
   mockIsFeatureEnabled.mockReturnValue(false);
 
-  const manager = ExtensionsManager.getInstance();
+  const loader = ExtensionsLoader.getInstance();
   const initializeSpy = jest
-    .spyOn(manager, 'initializeExtensions')
+    .spyOn(loader, 'initializeExtensions')
     .mockImplementation();
 
   render(<ExtensionsStartup />, {
@@ -257,15 +259,15 @@ test('does not initialize ExtensionsManager when EnableExtensions feature flag i
   initializeSpy.mockRestore();
 });
 
-test('logs error when ExtensionsManager initialization fails', async () => {
+test('logs error when ExtensionsLoader initialization fails', async () => {
   // Ensure feature flag is enabled
   mockIsFeatureEnabled.mockReturnValue(true);
 
   const errorSpy = jest.spyOn(logging, 'error').mockImplementation();
 
   // Mock the initializeExtensions method to throw an error
-  const originalInitialize = ExtensionsManager.prototype.initializeExtensions;
-  ExtensionsManager.prototype.initializeExtensions = jest
+  const originalInitialize = ExtensionsLoader.prototype.initializeExtensions;
+  ExtensionsLoader.prototype.initializeExtensions = jest
     .fn()
     .mockImplementation(() => {
       throw new Error('Test initialization error');
@@ -289,6 +291,6 @@ test('logs error when ExtensionsManager initialization fails', async () => {
   });
 
   // Restore original method
-  ExtensionsManager.prototype.initializeExtensions = originalInitialize;
+  ExtensionsLoader.prototype.initializeExtensions = originalInitialize;
   errorSpy.mockRestore();
 });
diff --git a/superset-frontend/src/extensions/ExtensionsStartup.tsx b/superset-frontend/src/extensions/ExtensionsStartup.tsx
index b6f03eb5164..8c16058ae21 100644
--- a/superset-frontend/src/extensions/ExtensionsStartup.tsx
+++ b/superset-frontend/src/extensions/ExtensionsStartup.tsx
@@ -26,12 +26,13 @@ import {
   commands,
   editors,
   extensions,
+  menus,
   sqlLab,
+  views,
 } from 'src/core';
 import { useSelector } from 'react-redux';
 import { RootState } from 'src/views/store';
-import { useExtensionsContext } from './ExtensionsContext';
-import ExtensionsManager from './ExtensionsManager';
+import ExtensionsLoader from './ExtensionsLoader';
 
 declare global {
   interface Window {
@@ -41,16 +42,16 @@ declare global {
       commands: typeof commands;
       editors: typeof editors;
       extensions: typeof extensions;
+      menus: typeof menus;
       sqlLab: typeof sqlLab;
+      views: typeof views;
     };
   }
 }
 
-const ExtensionsStartup = () => {
-  // Initialize the extensions context before initializing extensions
-  // This is a prerequisite for the ExtensionsManager to work correctly
-  useExtensionsContext();
-
+const ExtensionsStartup: React.FC<{ children?: React.ReactNode }> = ({
+  children,
+}) => {
   const [initialized, setInitialized] = useState(false);
 
   const userId = useSelector<RootState, number | undefined>(
@@ -58,8 +59,11 @@ const ExtensionsStartup = () => {
   );
 
   useEffect(() => {
-    // Skip initialization if already initialized or if user is not logged in
-    if (initialized || !userId) {
+    if (initialized) return;
+
+    if (!userId) {
+      // No user logged in — nothing to initialize
+      setInitialized(true);
       return;
     }
 
@@ -71,22 +75,31 @@ const ExtensionsStartup = () => {
       commands,
       editors,
       extensions,
+      menus,
       sqlLab,
+      views,
     };
 
-    // Initialize extensions
-    if (isFeatureEnabled(FeatureFlag.EnableExtensions)) {
-      try {
-        ExtensionsManager.getInstance().initializeExtensions();
-        supersetCore.logging.info('Extensions initialized successfully.');
-      } catch (error) {
-        supersetCore.logging.error('Error setting up extensions:', error);
+    const setup = async () => {
+      if (isFeatureEnabled(FeatureFlag.EnableExtensions)) {
+        try {
+          await ExtensionsLoader.getInstance().initializeExtensions();
+          supersetCore.logging.info('Extensions initialized successfully.');
+        } catch (error) {
+          supersetCore.logging.error('Error setting up extensions:', error);
+        }
       }
-    }
-    setInitialized(true);
+      setInitialized(true);
+    };
+
+    setup();
   }, [initialized, userId]);
 
-  return null;
+  if (!initialized) {
+    return null;
+  }
+
+  return <>{children}</>;
 };
 
 export default ExtensionsStartup;
diff --git a/superset-frontend/src/views/App.tsx b/superset-frontend/src/views/App.tsx
index 87df93f8b28..6784305350e 100644
--- a/superset-frontend/src/views/App.tsx
+++ b/superset-frontend/src/views/App.tsx
@@ -75,35 +75,36 @@ const App = () => (
     <ScrollToTop />
     <LocationPathnameLogger />
     <RootContextProviders>
-      <ExtensionsStartup />
       <Menu
         data={bootstrapData.common.menu_data}
         isFrontendRoute={isFrontendRoute}
       />
-      <Switch>
-        {routes.map(({ path, Component, props = {}, Fallback = Loading }) => (
-          <Route path={path} key={path}>
-            <Suspense fallback={<Fallback />}>
-              <Layout>
-                <Layout.Content
-                  css={css`
-                    display: flex;
-                    flex-direction: column;
-                  `}
-                >
-                  <ErrorBoundary
+      <ExtensionsStartup>
+        <Switch>
+          {routes.map(({ path, Component, props = {}, Fallback = Loading }) => (
+            <Route path={path} key={path}>
+              <Suspense fallback={<Fallback />}>
+                <Layout>
+                  <Layout.Content
                     css={css`
-                      margin: 16px;
+                      display: flex;
+                      flex-direction: column;
                     `}
                   >
-                    <Component user={bootstrapData.user} {...props} />
-                  </ErrorBoundary>
-                </Layout.Content>
-              </Layout>
-            </Suspense>
-          </Route>
-        ))}
-      </Switch>
+                    <ErrorBoundary
+                      css={css`
+                        margin: 16px;
+                      `}
+                    >
+                      <Component user={bootstrapData.user} {...props} />
+                    </ErrorBoundary>
+                  </Layout.Content>
+                </Layout>
+              </Suspense>
+            </Route>
+          ))}
+        </Switch>
+      </ExtensionsStartup>
       <ToastContainer />
     </RootContextProviders>
   </Router>
diff --git a/superset-frontend/src/views/RootContextProviders.tsx b/superset-frontend/src/views/RootContextProviders.tsx
index 19af8ddf4b8..a4cfb95c42d 100644
--- a/superset-frontend/src/views/RootContextProviders.tsx
+++ b/superset-frontend/src/views/RootContextProviders.tsx
@@ -27,7 +27,6 @@ import { DynamicPluginProvider } from 'src/components';
 import { EmbeddedUiConfigProvider } from 'src/components/UiConfigContext';
 import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
 import { ThemeController } from 'src/theme/ThemeController';
-import { ExtensionsProvider } from 'src/extensions/ExtensionsContext';
 import { store } from './store';
 import '../preamble';
 import querystring from 'query-string';
@@ -54,15 +53,13 @@ export const RootContextProviders: React.FC = ({ children }) => {
                     querystring.stringify(object, { encode: false }),
                 }}
               >
-                <ExtensionsProvider>
-                  {RootContextProviderExtension ? (
-                    <RootContextProviderExtension>
-                      {children}
-                    </RootContextProviderExtension>
-                  ) : (
-                    children
-                  )}
-                </ExtensionsProvider>
+                {RootContextProviderExtension ? (
+                  <RootContextProviderExtension>
+                    {children}
+                  </RootContextProviderExtension>
+                ) : (
+                  children
+                )}
               </QueryParamProvider>
             </DynamicPluginProvider>
           </EmbeddedUiConfigProvider>
diff --git a/superset/extensions/utils.py b/superset/extensions/utils.py
index 7927009e83c..00639105016 100644
--- a/superset/extensions/utils.py
+++ b/superset/extensions/utils.py
@@ -241,7 +241,6 @@ def build_extension_data(extension: LoadedExtension) -> dict[str, Any]:
     }
     if manifest.frontend:
         frontend = manifest.frontend
-        module_federation = frontend.moduleFederation
         remote_entry_url = (
             f"/api/v1/extensions/{manifest.publisher}/"
             f"{manifest.name}/{frontend.remoteEntry}"
@@ -249,9 +248,7 @@ def build_extension_data(extension: LoadedExtension) -> dict[str, Any]:
         extension_data.update(
             {
                 "remoteEntry": remote_entry_url,
-                "exposedModules": module_federation.exposes,
-                "moduleFederationName": module_federation.name,
-                "contributions": frontend.contributions.model_dump(),
+                "moduleFederationName": frontend.moduleFederationName,
             }
         )
     return extension_data
diff --git a/tests/unit_tests/extensions/test_types.py b/tests/unit_tests/extensions/test_types.py
index 0df29ac3622..79182c652c5 100644
--- a/tests/unit_tests/extensions/test_types.py
+++ b/tests/unit_tests/extensions/test_types.py
@@ -20,13 +20,10 @@
 import pytest
 from pydantic import ValidationError
 from superset_core.extensions.types import (
-    ContributionConfig,
     ExtensionConfig,
     ExtensionConfigBackend,
-    ExtensionConfigFrontend,
     Manifest,
     ManifestBackend,
-    ModuleFederationConfig,
 )
 
 # =============================================================================
@@ -49,7 +46,6 @@ def test_extension_config_minimal():
     assert config.version == "0.0.0"
     assert config.dependencies == []
     assert config.permissions == []
-    assert config.frontend is None
     assert config.backend is None
 
 
@@ -65,21 +61,6 @@ def test_extension_config_full():
             "description": "A query insights extension",
             "dependencies": ["other-extension"],
             "permissions": ["can_read", "can_view"],
-            "frontend": {
-                "contributions": {
-                    "views": {
-                        "sqllab": {
-                            "panels": [
-                                {
-                                    "id": "query_insights.main",
-                                    "name": "Query Insights",
-                                }
-                            ]
-                        }
-                    }
-                },
-                "moduleFederation": {"exposes": ["./index"]},
-            },
             "backend": {
                 "entryPoints": ["query_insights.entrypoint"],
                 "files": ["backend/src/query_insights/**/*.py"],
@@ -94,8 +75,6 @@ def test_extension_config_full():
     assert config.description == "A query insights extension"
     assert config.dependencies == ["other-extension"]
     assert config.permissions == ["can_read", "can_view"]
-    assert config.frontend is not None
-    assert config.frontend.moduleFederation.exposes == ["./index"]
     assert config.backend is not None
     assert config.backend.entryPoints == ["query_insights.entrypoint"]
     assert config.backend.files == ["backend/src/query_insights/**/*.py"]
@@ -201,7 +180,7 @@ def test_manifest_minimal():
 
 
 def test_manifest_with_frontend():
-    """Test Manifest with frontend section requires remoteEntry."""
+    """Test Manifest frontend section requires remoteEntry and moduleFederationName."""
     manifest = Manifest.model_validate(
         {
             "id": "my-org.my-extension",
@@ -210,14 +189,13 @@ def test_manifest_with_frontend():
             "displayName": "My Extension",
             "frontend": {
                 "remoteEntry": "remoteEntry.abc123.js",
-                "contributions": {},
-                "moduleFederation": {"exposes": ["./index"]},
+                "moduleFederationName": "myOrg_myExtension",
             },
         }
     )
     assert manifest.frontend is not None
     assert manifest.frontend.remoteEntry == "remoteEntry.abc123.js"
-    assert manifest.frontend.moduleFederation.exposes == ["./index"]
+    assert manifest.frontend.moduleFederationName == "myOrg_myExtension"
 
 
 def test_manifest_frontend_missing_remote_entry():
@@ -229,7 +207,7 @@ def test_manifest_frontend_missing_remote_entry():
                 "publisher": "my-org",
                 "name": "my-extension",
                 "displayName": "My Extension",
-                "frontend": {"contributions": {}, "moduleFederation": {}},
+                "frontend": {"moduleFederationName": "myOrg_myExtension"},
             }
         )
     assert "remoteEntry" in str(exc_info.value)
@@ -265,35 +243,6 @@ def test_manifest_backend_no_files_field():
     assert not hasattr(manifest.backend, "files")
 
 
-# =============================================================================
-# Shared component tests
-# =============================================================================
-
-
-def test_module_federation_config_defaults():
-    """Test ModuleFederationConfig has correct defaults."""
-    config = ModuleFederationConfig.model_validate({})
-    assert config.exposes == []
-    assert config.filename == "remoteEntry.js"
-    assert config.shared == {}
-    assert config.remotes == {}
-
-
-def test_contribution_config_defaults():
-    """Test ContributionConfig has correct defaults."""
-    config = ContributionConfig.model_validate({})
-    assert config.commands == []
-    assert config.views == {}
-    assert config.menus == {}
-
-
-def test_extension_config_frontend_defaults():
-    """Test ExtensionConfigFrontend has correct defaults."""
-    frontend = ExtensionConfigFrontend.model_validate({})
-    assert frontend.contributions.commands == []
-    assert frontend.moduleFederation.exposes == []
-
-
 def test_extension_config_backend_defaults():
     """Test ExtensionConfigBackend has correct defaults."""
     backend = ExtensionConfigBackend.model_validate({})