Skill v1.0.1
currentAutomated scan100/100+3 new
version: "1.0.1" name: "extensions-internals" description: "Deep extension system implementation knowledge: IExtensionLoader interface contracts, IndexedDB storage schema, version-compare cache strategy, contribution point registration, extension sandbox, and the .foxe packaging format."
Extensions Internals Skill
IExtensionLoader Interface
Defined in packages/suite-base/src/services/extension/IExtensionLoader.ts:
export type TypeExtensionLoader = "browser" | "server" | "filesystem";export type LoadedExtension = {buffer?: Uint8Array;raw: string;};export type InstallExtensionProps = {foxeFileData: Uint8Array;file?: File;externalId?: string;};export interface IExtensionLoader {readonly namespace: Namespace; // "org" | "local"readonly type: TypeExtensionLoader; // "browser" | "server" | "filesystem"getExtension(id: string): Promise<ExtensionInfo | undefined>;getExtensions(): Promise<ExtensionInfo[]>;loadExtension(id: string): Promise<LoadedExtension>;installExtension(data: InstallExtensionProps): Promise<ExtensionInfo>;uninstallExtension(id: string): Promise<void>;}
⚠️installExtensiontakes anInstallExtensionPropsobject (withfoxeFileData), not aURL string. The type union is"browser" | "server" | "filesystem"— there is no"indexeddb"or"remote"value.
Implementations:
| Class | File | namespace | type | |
|---|---|---|---|---|
IdbExtensionLoader | services/extension/IdbExtensionLoader.ts | constructor arg | "browser" | |
RemoteExtensionLoader | services/extension/RemoteExtensionLoader.ts | constructor arg | "server" | |
DesktopExtensionLoader | suite-desktop/src/renderer/services/DesktopExtensionLoader.ts | "local" | "filesystem" |
All loaders implement this interface — the catalog provider doesn't know the backing store.
IdbExtensionLoader + IdbExtensionStorage (IndexedDB)
IdbExtensionLoader delegates persistence to IdbExtensionStorage (packages/suite-base/src/services/extension/IdbExtensionStorage.ts).
Storage Schema
- Database name: `
${KEY_WORKSPACE_PREFIX}lichtblick-extensions-${namespace}` (one DB per namespace) - Database version:
1 - Object store
metadata— keyPath"id", valueExtensionInfo - Object store
extensions— keyPath"info.id", valueStoredExtension
Install Flow (installExtension)
1. Receive InstallExtensionProps { foxeFileData, file?, externalId? }2. Decompress + extract package.json / dist entry from the .foxe3. validatePackageInfo() builds ExtensionInfo4. Persist ExtensionInfo to `metadata` + StoredExtension to `extensions`
Load Flow (loadExtension)
1. storage.get(id) → StoredExtension (throws "Extension not found" if missing)2. decompressFile(content) → extractFoxeFileContent(ALLOWED_FILES.EXTENSION)3. Return LoadedExtension { buffer?, raw }
Namespace Isolation
"org"extensions: managed by organization, auto-synced"local"extensions: user-installed, never auto-removed- Each namespace gets its own IndexedDB database (suffix
-{namespace})
DesktopExtensionLoader
packages/suite-desktop/src/renderer/services/DesktopExtensionLoader.ts \u2014 namespace = "local", type = "filesystem". Reads installed extensions from the desktop file system (via the preload bridge) rather than IndexedDB. Same IExtensionLoader contract.
RemoteExtensionLoader
packages/suite-base/src/services/extension/RemoteExtensionLoader.ts \u2014 type = "server" (the remote/org loader). The type value is `"server"`, not `"remote"` \u2014 the only valid type values are "browser", "server", and "filesystem".
.foxe Package Format (Detail)
Structure
├── package.json (required)├── dist/│ └── index.js (required — bundled extension entry)├── README.md (optional)└── CHANGELOG.md (optional)
package.json Required Fields
{"name": "@publisher/extension-name","version": "1.2.3","displayName": "Human Readable Name","description": "What this extension does","publisher": "publisher-name","main": "dist/index.js"}
Thepackage.jsoncarries metadata only. Contributions (panels, converters, aliases, cameramodels) are registered at runtime inactivate(ctx)\u2014 see "Contribution Registration" below.
Contribution Registration (Dynamic)
⚠️ Contributions are not declared statically inpackage.json. There is nolichtblick.panels/messageConverterscontributions key. Instead the extension's bundledsource is executed and registers contributions at runtime via theactivate(ctx)callback.
buildContributionPoints.ts (packages/suite-base/src/providers/helpers/buildContributionPoints.ts) executes the extension source with new Function("module", "require", source), then calls module.exports.activate(ctx). The ExtensionContext (ctx) exposes:
const ctx: ExtensionContext = {mode, // "production" | "test" | "development"registerPanel(registration), // panelId = `${qualifiedName}.${registration.name}`registerMessageConverter(args), // collects InstalledMessageConverter + panelSettingsregisterTopicAliases(aliasFn), // note: registerTopicAliases (plural), not ...FunctionregisterCameraModel({ name, modelBuilder }),};
buildContributionPoints returns the accumulated { panels, messageConverters, topicAliasFunctions, panelSettings, cameraModels }.
ExtensionCatalogProvider (Zustand Store)
State Shape
interface ExtensionCatalogState {installedExtensions: ExtensionInfo[];installedPanels: Map<string, RegisteredPanel>;installedMessageConverters: MessageConverter[];installedTopicAliasFunctions: TopicAliasFunction[];installedCameraModels: CameraModel[];// ActionsrefreshExtensions: () => Promise<void>;installExtension: (loader: IExtensionLoader, url: string) => Promise<void>;uninstallExtension: (loader: IExtensionLoader, id: string) => Promise<void>;}
Registration Flow
refreshExtensions() called→ For each loader: getExtensions()→ For each extension: loadExtension()→ Execute extension code in sandbox→ Extension calls activate(context)→ context.registerPanel() / context.registerMessageConverter()→ Zustand state updated with new contributions
Extension Sandbox
Extensions run in a restricted context built by buildContributionPoints.ts:
- Source executed via
new Function("module", "require", source)—requireonly resolves
react and react-dom
- No direct DOM access (panels render via a React component)
- Limited to the
ExtensionContextAPI surface:
``typescript interface ExtensionContext { mode: "production" | "test" | "development"; registerPanel(registration: ExtensionPanelRegistration): void; registerMessageConverter<Src>(args: RegisterMessageConverterArgs<Src>): void; registerTopicAliases(aliasFunction: TopicAliasFunction): void; registerCameraModel(args: RegisterCameraModelArgs): void; } ``
Panel Registration
// Inside extension's activate():export function activate(context: ExtensionContext) {context.registerPanel({name: "MyPanel",initPanel: (panelAPI: PanelExtensionContext) => {panelAPI.onRender = (renderState, done) => {// Render panel contentdone();};panelAPI.subscribe([{ topic: "/my_topic" }]);},});}
Conflict Resolution
Priority order when same contribution exists in multiple sources:
- Local namespace (user-installed) — highest priority
- Org namespace (organization-managed)
- Built-in panels (always lowest priority)
Debugging Extensions
- Check browser DevTools console for extension load errors
- IndexedDB inspector shows cached extension data
- Extension catalog zustand devtools shows registration state
- Common issues: missing
mainfield, incorrect contribution format, version string format