Contributing to Meshery UI - Extension Contract and Event Bus

Table of Contents

The boundary

Meshery extensions β€” Kanvas being the one most contributors will meet β€” are not compiled with Meshery UI. They are separately-built, separately-released JavaScript bundles that Meshery Server proxies and Meshery UI fetches, evaluates, and mounts at runtime.

  meshery/meshery (host)                      meshery/meshery-extensions (bundle)
  ─────────────────────────                   ──────────────────────────────────
  NavigatorExtension.tsx                      App.tsx
      β”‚                                            β”‚
      β”‚  injectProps  ────────────────────────►  destructured, normalized,
      β”‚  (one untyped object)                    stashed in module-scope maps
      β”‚                                            β”‚
      β”‚  ◄──────────  mesheryEventBus  ──────────►  β”‚   (bi-directional, typed)
      β”‚                                            β”‚
      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                     β”‚
             @sistent/sistent
          mesheryExtensionContract
     (event literals, payload types, declared
      capabilities, contract version)

Everything crossing that line is either a key on injectProps or an event on mesheryEventBus. Both are declared once, in sistent, and shared by the two repos.

Why there is a contract

There is no compile-time link across the boundary:

  • The host never imports the extension, so TypeScript cannot verify that the keys the host provides are the keys the extension reads.
  • The extension never imports the host, so renaming a key here still compiles there.
  • Bundle and host are released independently. A bundle built against one revision of the host may be loaded by an older or newer host, indefinitely.

A rename on either side therefore does not fail the build and does not throw. It resolves to undefined at the extension’s use site, and the feature becomes a silent runtime no-op. This is not hypothetical β€” two shipped examples:

ChangeSymptom in production
OPEN_DESIGN_IN_KANVAS β†’ OPEN_DESIGN_IN_EXTENSION“Open in Kanvas” published an event nobody was subscribed to. No error; the click did nothing.
capabilitiesRegistry β†’ providerCapabilitiesThe extension constructed its access-control object from undefined. No error; permission-gated UI silently misbehaved.

The contract exists to convert that class of failure into a compile error or a failing test.

Where the contract lives

The contract is declared once, in the mesheryExtensionContract module of @sistent/sistent. Read that module for the authoritative definitions. The exports Meshery UI consumes:

ExportKindPurpose
MESHERY_EXTENSION_EVENTconst enum-like objectEvery event literal allowed on the bus. Derive every event type from this β€” never type the string.
MesheryExtensionEventtypeDiscriminated union of { type, data } across all contract events.
MesheryExtensionEventBustypeType of the shared bus singleton.
EventBus<T>classThe bus implementation itself (RxJS-backed).
MESHERY_EXTENSION_CONTRACT_VERSIONconstRevision of the contract the host was built against; injected into every extension.
reportInjectedCapabilitiesfunctionGiven an injectProps bag, reports which declared capabilities are present and which are missing.
isInjectedCapabilityReportSatisfiedfunctionWhether a report has no missing capabilities.
describeInjectedCapabilityReportfunctionHuman-readable rendering of a report, used as test-failure output.
createCanShowfunctionBuilds the permission-gated CanShow component, and publishes missing-permission / missing-capability events onto the bus it is given.

How an extension gets loaded

The full path from provider capability to mounted component:

  1. The provider declares the extension. GET /api/provider/capabilities returns an extensions object keyed by extension point β€” navigator, account, userPrefs, collaborator, plus full_page entries. Each entry carries an href (the route it claims) and a component (the bundle URI).
  2. The Navigator renders menu entries for the navigator extension point. ui/components/layout/Navigator/Navigator.tsx runs the declared entries through ExtensionPointSchemaValidator('navigator') and renders them as menu items.
  3. The route resolves. Navigating to a declared href lands on the catch-all page ui/pages/extension/[...component].tsx, which matches the current path against the provider’s declared extensions and dispatches extensionType into Redux.
  4. ExtensionSandbox picks the bundle. ui/components/ExtensionSandbox.tsx walks the extension tree for the entry whose href matches the current path, takes its component URI, and turns it into a fetchable URL with createPathForRemoteComponent() β€” which prefixes /api/provider/extension. The bundle is therefore served through Meshery Server, not fetched cross-origin from the provider.
  5. NavigatorExtension loads and mounts it. ui/components/layout/Navigator/NavigatorExtension.tsx calls useRemoteComponent(url), which fetches the bundle, evaluates it as a CommonJS module against the shared runtime, and returns module.exports.default. The host then renders it with exactly one prop: injectProps.

For non-navigator extension points, ExtensionSandbox renders the generic ui/components/general/RemoteComponent instead, which forwards whatever props it was given but mounts the bundle without the capability bag. Only the Navigator extension point receives injectProps. That path also has none of the export-shape diagnostics described below β€” a mis-built bundle mounted through it still fails with React’s opaque error, so debug it against the checklist in Troubleshooting rather than waiting for a useful message.

The shared runtime

A remote bundle must not ship its own copy of React β€” two React instances in one page produce “invalid hook call” failures, and two MUI/emotion instances produce broken theming. So the bundle is built with those packages as externals, and the host satisfies each require() at evaluation time from ui/remote-component.config.js:

module.exports = {
  resolve: {
    react: require('react'),
    'react-dom': require('react-dom'),
    '@mui/material': require('@mui/material'),
    '@emotion/react': require('@emotion/react'),
    // …plus xstate, redux, rjsf, cytoscape, dockview, xterm, dnd-kit, tippy, …
  },
};

Three consequences worth knowing:

  • A missing key is a hard failure. If a bundle require()s something absent from resolve, remote-component throws "… does not exist in dependencies" and the extension does not mount. Adding a new third-party dependency to an extension therefore requires a matching entry here.
  • CSS subpath imports need stub entries. Bundles do side-effect-only imports such as require('@xterm/xterm/css/xterm.css'). The actual stylesheets are injected globally in ui/pages/_app.tsx, so the config maps those subpaths to {} β€” a key must exist or the resolver throws, but the value is irrelevant.
  • Versions must stay in lockstep. The host’s installed version of a shared package is the one the extension gets, regardless of what the extension declared. A major-version bump on either side is a coordinated change.

Export shape and its two failure modes

The bundle must export its component as a CommonJS default:

module.exports = { default: Component, __esModule: true };

In practice that means building with output.library.type = "commonjs2". A bundle built any other way still loads β€” it just resolves to nothing useful. Two distinct failures follow, and NavigatorExtension reports them with different messages on purpose:

FailureWhat the loader returnsActual causeRemedy the host reports
No CommonJS default exportRemoteComponent === undefined, err === undefined β€” the hook does not throwBundler configurationRebuild with output.library.type = "commonjs2" and republish
Default export is not a component (e.g. <Foo /> instead of Foo)An object carrying a $$typeof tagDefect in the extension’s own sourceExport the component itself, not a rendered element

Without those guards, both cases surface only as React’s opaque “Element type is invalid … got: undefined”, thrown from deep inside the render tree, far from the real problem.

The component check is deliberately an allow-list of $$typeof tags (react.forward_ref, react.memo, react.lazy) rather than a bare '$$typeof' in value test. Many React-internal values carry $$typeof without being component types β€” most notably an already-rendered element, which is precisely the mistake the second row above describes. A bare check would wave it straight through.

If an extension renders blank with nothing in the console, inspect the bundle’s export shape before anything else.

The injected capability bag

buildExtensionInjectProps in NavigatorExtension.tsx builds the bag. It is deliberately a pure factory β€” everything it needs from React state or context arrives as a parameter β€” so a unit test can build the bag and assert it against the contract without rendering anything. Keep it that way; the moment it reads a hook directly, the gate test gets harder to write than the shortcut is worth.

The component memoizes the result on its inputs, so extensions are not re-rendered by an unrelated store update.

What is in the bag

GroupKeys
ContractcontractVersion
ComponentsPatternServiceFormCore, InfoModal, ViewInfoModal, ExportModal, GenericRJSFModal, RJSForm, TypingFilter, CreateModelModal, ImportModelModal, ValidateDesign, DryRunDesign, DeployStepper, UnDeployStepper, MesheryPerformanceComponent, StructuredDataFormatter, RelationshipEvaluationResponseFormatter, ThemeTogglerCore, _PromptComponent
Hookshooks.CAN, hooks.useHasPermission, hooks.useFilterK8sContexts, hooks.useDynamicComponent, and useNotificationHook at top level
StateproviderCapabilities, selectedK8sContexts, currentOrganization, mesheryStore
CallbacksopenWorkspaceModal, openRegistryModal, SetCurrentLoadedResourceInOrgWorkspaceSession
MachinesdesignValidationMachine
BusmesheryEventBus
Access controlProviderUiAccessControlClass (and its legacy alias CapabilitiesRegistryClass)
Legacyresolver

Two of these deserve elaboration:

mesheryStore is getter-based, not a snapshot.

const extensionExposedMesheryStore = {
  selectedK8sClusters: { get: () => selectSelectedK8sClusters(store.getState()) },
  k8sConfig: { get: () => selectK8sConfig(store.getState()) },
};

The extension calls .get() at the moment it needs the value, so it always reads live store state. Handing over a plain value instead would freeze it at mount time. Add new store exposure the same way.

ProviderUiAccessControlClass is a class, not an instance. The host passes the class; the extension constructs it with the raw /api/provider/capabilities payload (new CapabilitiesRegistryClass(capabilitiesRegistry)). ProviderUiAccessControl (ui/utils/disabledComponents.js) answers the provider-driven questions β€” isNavigatorComponentEnabled, isHeaderComponentEnabled, isExtensionComponentEnabled β€” which in a restricted (playground) environment walk restrictedAccess.allowedComponents to decide whether a given piece of UI may render at all.

How the extension consumes it

On the extension side (meshery-extensions/ui/src/App.tsx and ui/src/globals/mesherySdk.ts), the bag is destructured once at the top level and stashed into module-scoped maps, so deeply-nested extension code can reach it without prop drilling:

setMesheryFunctionality("mesheryEventBus", mesheryEventBus);
setMesheryFunctionality("CAN", normalizedCAN);
injectedReactComponents.set(GLOBAL_COMPONENTS.ValidateDesign, normalizedValidateDesign);
injectedMachines.set(GLOBAL_MACHINES.DesignValidatorMachine, normalizedDesignValidationMachine);

and read back through typed accessors β€” getMesheryEventBus(), getCapabilitiesRegistry(), createInjectedComponent("ExportDesignModal"), and so on. injectedMesheryFunctionalitiesMap in mesherySdk.ts is the extension’s local typing of the same contract.

Every value passes through normalizeInjectedValue first. It repeatedly unwraps { default, __esModule } wrappers until it reaches the real value:

while (hasDefaultExport(resolvedValue) && !seen.has(resolvedValue)) {
  resolvedValue = resolvedValue.default;
}

This exists because the ESM/CJS interop between host build, remote-component evaluation and bundle build can double-wrap an injected component, so Component arrives as { default: Component }. Rendering that directly fails with the same opaque React error as a bad export. If you inject a value that is itself an object with a default key, be aware it will be unwrapped β€” the seen set only guards against cycles, not against legitimately-shaped data.

Contract versioning

contractVersion: MESHERY_EXTENSION_CONTRACT_VERSION is injected into every bundle. Nothing at build time can catch a host/bundle mismatch, because bundles are published artifacts loaded by whichever host happens to be deployed. The version gives the extension a runtime signal it can use to degrade gracefully or warn, rather than silently misbehaving.

Bump it in sistent whenever a change is not backward-compatible for already-published bundles.

Backward compatibility

Published bundles are loaded by new hosts, so keys cannot simply be deleted:

  • CapabilitiesRegistryClass is retained as an alias of ProviderUiAccessControlClass. The rename happened here; the alias keeps older bundles working. The unit test asserts the two keys are the same reference, so the alias cannot drift.
  • resolver is retained even though Meshery Server no longer exposes any GraphQL subscription. Published bundles call resolver.subscription.ConfigurationSubscription(cb, vars) and later .dispose() on the returned handle; handing them undefined would throw at mount. The host supplies an inert subscription matching the old call shape β€” (onNext, variables) => ({ dispose }) β€” that never invokes onNext. New extensions must read designs and filters from the REST API.

When you retire a capability, keep an alias or an inert stand-in until every published bundle has been rebuilt, and leave a comment saying why the dead-looking key is there. Otherwise the next contributor deletes it as cruft.

Worked example: adding a capability

Say you want extensions to be able to open the environment-selection modal.

  1. Declare it in sistent. Add openEnvironmentModal to the declared capability list in mesheryExtensionContract so reportInjectedCapabilities knows to look for it.

  2. Add the parameter to the factory in NavigatorExtension.tsx β€” it comes from a hook or context, so it must be a parameter, not a direct hook call:

    export type ExtensionInjectPropsDeps = {
      // …existing
      openEnvironmentModal: unknown;
    };
    
    export const buildExtensionInjectProps = ({ /* … */ openEnvironmentModal }) => ({
      // …existing
      openEnvironmentModal,
    });
    
  3. Wire it at the call site, inside the existing useMemo, and add it to the dependency array β€” a missed dependency here hands the extension a stale callback.

  4. Consume it in the extension: add it to AppProps['injectProps'] and injectedMesheryFunctionalitiesMap, then setMesheryFunctionality("openEnvironmentModal", …).

  5. Run npm test in ui/. The capability-report assertion in NavigatorExtension.test.tsx now covers the new key; if you forgot step 2 or 3 it fails here rather than in production.

The Meshery event bus

ui/utils/eventBus.ts exports the singleton:

import { EventBus, type MesheryExtensionEvent, type MesheryExtensionEventBus } from '@sistent/sistent';

export const mesheryEventBus: MesheryExtensionEventBus = new EventBus<MesheryExtensionEvent>();

Delivery semantics

The implementation is a thin wrapper over an RxJS Subject:

publish(event) { this.eventSubject.next(event); }
on(type)       { return this.eventObservable.pipe(filter((e) => e.type === type)); }
onAny()        { return this.eventObservable; }

That has three practical consequences:

  • No replay, no buffering. It is a plain Subject, not a ReplaySubject or BehaviorSubject. An event published while nobody is subscribed is dropped permanently β€” a late subscriber never sees it. This is the reason the navigation publishers first check isExtensionOpen(): publishing to an unmounted extension would be a no-op, so they fall back to a router push instead.
  • Delivery is synchronous. publish() runs every matching subscriber before it returns. Do not rely on a publish being deferred, and be aware that an exception thrown by one subscriber propagates back into the publisher.
  • on() filters at runtime but is typed as the full union. See the narrowing note under Subscribing.

Subscriptions are RxJS subscriptions. Hold the handle and unsubscribe() on teardown β€” this bus is a module-scoped singleton that outlives every component, so a leaked subscription leaks for the lifetime of the page.

MethodBehavior
publish(event)Emits a MesheryExtensionEvent. Type-checked against the contract union. Synchronous.
on(type)Observable of events of that type only.
onAny()Observable of every event, regardless of type. Useful for debugging; avoid in feature code.

Publishing

Always derive the literal from the enum:

import { MESHERY_EXTENSION_EVENT } from '@sistent/sistent';
import { mesheryEventBus } from '@/utils/eventBus';

mesheryEventBus.publish({
  type: MESHERY_EXTENSION_EVENT.OpenDesignInExtension,
  data: { designId, designName },
});

The host’s navigation helpers in ui/utils/utils.tsx all follow the same shape β€” publish if the extension is mounted, otherwise navigate:

export const openDesignInExtension = (designId, designName, router) => {
  if (isExtensionOpen()) {
    mesheryEventBus.publish({
      type: MESHERY_EXTENSION_EVENT.OpenDesignInExtension,
      data: { designId, designName },
    });
    return;
  }
  router.push(`/extension/meshmap?mode=design&type=design&id=${designId}`);
};

Preserve that branch when adding similar actions. Given the no-replay semantics above, publishing without the guard means the action is silently lost whenever the extension is not already mounted.

Subscribing

ui/store/index.ts wires the store to the bus and shows the one non-obvious detail:

mesheryEventBus.on(MESHERY_EXTENSION_EVENT.DispatchToMesheryStore).subscribe((event) => {
  // `EventBus.on` filters at runtime but is typed as the full event union, so the
  // discriminant has to be re-checked here for `event.data` to narrow to a
  // dispatchable action rather than the union of every event payload.
  if (event.type !== MESHERY_EXTENSION_EVENT.DispatchToMesheryStore) return;
  store.dispatch(event.data);
});

The redundant-looking if is required for narrowing β€” on() is declared as Observable<T>, not Observable<Extract<T, { type: E }>>. Do not delete it.

On the extension side the same bus is bridged into XState with fromCallback, so a state machine can receive host events as ordinary machine events:

export const MesheryEventsSubscriberActor = (events: MesheryEventTypes["type"][]) =>
  fromCallback(({ sendBack }) => {
    const subscribers = events.map((eventType) =>
      getMesheryEventBus().on(eventType).subscribe(sendBack)
    );
    return () => subscribers.forEach((s) => s.unsubscribe());
  });

Note the teardown function β€” that is the unsubscribe() discipline described above, and the pattern to copy for any long-lived subscriber.

Event reference

MESHERY_EXTENSION_EVENT in sistent is authoritative. The events Meshery UI publishes or subscribes to today:

Enum memberWire literalDirectionPayloadSite
DispatchToMesheryStoreDISPATCH_TO_MESHERY_STOREextension β†’ host{ type, payload? } β€” a Redux actionconsumed in ui/store/index.ts
K8sContextsUpdatedK8S_CONTEXTS_UPDATEDhost β†’ extension{ selectedK8sContexts }setK8sContexts thunk, ui/store/slices/mesheryUi.ts
OpenDesignInExtensionOPEN_DESIGN_IN_EXTENSIONhost β†’ extension{ designId, designName }openDesignInExtension, ui/utils/utils.tsx
OpenViewInExtensionOPEN_VIEW_IN_EXTENSIONhost β†’ extension{ viewId, viewName }openViewInExtension, ui/utils/utils.tsx
OpenViewScopedToDesignOPEN_VIEW_SCOPED_TO_DESIGNhost β†’ extension{ designId, designName }openViewScopedToDesignInOperator, ui/utils/utils.tsx
MergeDesignMERGE_DESIGNhost β†’ extension{ id, name }mergeDesignWithCurrent, ui/utils/utils.tsx

Plus the access-control events that shared sistent components emit onto whichever bus they are given β€” in Meshery UI that wiring is createCanShow(getProviderUiAccessControl, CAN, () => mesheryEventBus) in ui/utils/can.ts:

Wire literalDirectionMeaning
MISSING_PERMISSIONhost β†’ extensionA CanShow-gated element was denied by the user’s abilities.
MISSING_CAPABILITYhost β†’ extensionA CanShow-gated element was denied by the provider’s declared capabilities.
FeatureRequiresUserAccounteitherA feature was attempted that requires a signed-in account.

Kanvas subscribes to those three to drive its anonymous-user flow β€” prompting sign-in or an upgrade instead of rendering a dead control.

Worked example: adding an event

Say the host needs to tell the extension that the active workspace changed.

  1. Declare it in sistent β€” add the literal to MESHERY_EXTENSION_EVENT and the payload member to the MesheryExtensionEvent union:

    WorkspaceChanged: 'WORKSPACE_CHANGED',
    // …
    type WorkspaceChangedEvent = {
      type: typeof MESHERY_EXTENSION_EVENT.WorkspaceChanged;
      data: { workspaceId: string; workspaceName: string };
    };
    
  2. Publish from the host, deriving the literal from the enum, and guarding with isExtensionOpen() if the event is a navigation action:

    mesheryEventBus.publish({
      type: MESHERY_EXTENSION_EVENT.WorkspaceChanged,
      data: { workspaceId, workspaceName },
    });
    
  3. Assert the publish site in the owning slice/util test, the way ui/store/slices/__tests__/mesheryUi.test.ts asserts K8sContextsUpdated.

  4. Subscribe in the extension β€” add the literal to MesheryEventTypes and to the relevant MesheryEventsSubscriberActor list (or a direct .on(...).subscribe(...) with teardown).

  5. Bump MESHERY_EXTENSION_CONTRACT_VERSION if the change is not backward-compatible.

  6. Update the event reference table on this page.

Skipping step 6 is how the table goes stale, and a stale table is worse than none β€” it is what contributors read instead of the sistent module.

Changing the contract

The order matters. Sistent first, then host, then extension:

  1. Change sistent. Add the event literal and payload, or the declared capability, in mesheryExtensionContract.
  2. Bump MESHERY_EXTENSION_CONTRACT_VERSION for anything not backward-compatible with already-published bundles.
  3. Update the host β€” publish/subscribe via the enum member; add the capability to buildExtensionInjectProps and its call site’s useMemo dependencies.
  4. Update meshery/meshery-extensions to read the new key or handle the new event.
  5. Keep an alias or inert stand-in for anything renamed or removed until published bundles are rebuilt.
  6. Run npm test in ui/ and update this page.

Working against an unreleased sistent

Contract changes almost always mean an unreleased @sistent/sistent. During development, ui/node_modules/@sistent/sistent is typically overwritten in place with a locally-built dist. A green local run then proves nothing about CI, which installs the published package.

Re-verify with npm ci before declaring a contract change done. A local build usually keeps the published version string, so matching versions are not evidence that the installed contents are the published ones. A version mismatch against ui/package.json is a useful tell that this has happened; a match proves nothing.

Forbidden

  • MUST NOT type an event literal by hand β€” derive it from MESHERY_EXTENSION_EVENT.
  • MUST NOT construct the bus as a bare new EventBus(); it must be new EventBus<MesheryExtensionEvent>().
  • MUST NOT use mesheryEventBus for communication that stays inside Meshery UI.
  • MUST NOT redeclare contract events or injected keys locally in meshery/meshery.
  • MUST NOT drop an injected key without an alias or inert stand-in while older bundles are still in the wild.
  • MUST NOT call hooks directly inside buildExtensionInjectProps β€” it must stay a pure factory so the gate test can call it.
  • MUST NOT add a third-party dependency to an extension without a matching entry in ui/remote-component.config.js.

Tests that gate the boundary

Run with npm test in ui/ (Vitest).

FileWhat it guarantees
ui/components/layout/Navigator/NavigatorExtension.test.tsxThe bag built by buildExtensionInjectProps satisfies every capability the contract declares, via reportInjectedCapabilities / isInjectedCapabilityReportSatisfied, with describeInjectedCapabilityReport as the failure message. This is the gate that catches a capability rename before merge. Also covers contractVersion being injected, the CapabilitiesRegistryClass alias holding, the getter-based mesheryStore, the missing-default-export and not-a-component diagnostics (including that only the former mentions commonjs2), memo/forwardRef-wrapped exports, and that a missing export is not reported while still loading.
ui/utils/__tests__/eventBus.test.tsThe bus is a singleton exposing publish/on/onAny; on filters by type while onAny does not; unsubscribing stops delivery; and β€” via @ts-expect-error β€” an event literal outside the contract fails the build.
ui/store/slices/__tests__/mesheryUi.test.tsThe setK8sContexts thunk publishes K8sContextsUpdated with the selected contexts.

If you add an event, add a publish-site assertion alongside these. An event nobody asserts on is indistinguishable from an event nobody receives.

Troubleshooting

SymptomLikely causeWhere to look
Extension area is blank, no console errorBundle has no CommonJS default exportThe host’s own error panel now reports this; rebuild with output.library.type = "commonjs2"
React: “Element type is invalid … got: undefined”Same as above, from an older host, or a double-wrapped injected componentBundle export shape; normalizeInjectedValue on the extension side
"… does not exist in dependencies"Bundle require()s a package absent from the shared runtimeui/remote-component.config.js
“Invalid hook call” inside the extensionTwo React copies β€” the bundle shipped its own instead of externalizing itExtension build config; resolve.react in the shared runtime
A host action does nothing in the extensionEvent literal renamed on one side, or published while the extension was unmountedMESHERY_EXTENSION_EVENT; the isExtensionOpen() guard
An injected value is undefined in the extensionCapability renamed or dropped, or missing from the useMemo depsbuildExtensionInjectProps; run the gate test
A feature works locally, fails in CInode_modules/@sistent/sistent overwritten with a local distRe-run npm ci and retest
Permission-gated UI misbehavesProviderUiAccessControl constructed from the wrong payloadui/utils/disabledComponents.js; capabilitiesRegistry vs providerCapabilities

File reference

PathRole
ui/utils/eventBus.tsThe singleton bus.
ui/components/layout/Navigator/NavigatorExtension.tsxLoads the bundle; builds and injects the capability bag; reports load failures.
ui/components/ExtensionSandbox.tsxResolves which bundle URI serves the current path, for each extension point.
ui/pages/extension/[...component].tsxCatch-all route for full-page extensions.
ui/remote-component.config.jsShared runtime handed to remote bundles.
ui/utils/utils.tsxHost-side navigation publishers and isExtensionOpen().
ui/store/index.tsSubscribes the Redux store to DispatchToMesheryStore.
ui/store/slices/mesheryUi.tsPublishes K8sContextsUpdated.
ui/utils/can.tsWires createCanShow to the bus for permission/capability events.
ui/utils/disabledComponents.jsProviderUiAccessControl β€” provider-driven UI gating.
@sistent/sistent β†’ mesheryExtensionContractThe contract itself. Authoritative.
meshery-extensions/ui/src/App.tsxExtension-side entry point; destructures injectProps.
meshery-extensions/ui/src/globals/mesherySdk.tsExtension-side accessors and local typing of the contract.