npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@alwith/module-extension

v0.6.0

Published

Portable application extension host and native package storage

Readme

@alwith/module-extension

Reusable application extensions for ALwith desktop hosts. This package is separate from Codex plugins and skills. It contains no Desktop or U application imports and does not own agent sessions.

Delivery

Maintained packages use the 0.6.x release line. extensionApiVersion follows the package version; extensionApiCompatibilityVersions lists the earlier versions the current API still serves (0.5.4, unchanged in 0.6.0), and the default Tauri runtime accepts them. The optional native adapter and JavaScript peers use Tauri 3 alpha.

The npm package includes compiled JavaScript, declarations, schemas, the generated command bindings (/commands) and the native extension libraries. The Tauri plugin is the alwith-extension crate. Ordinary extension bundles contain only JavaScript and static assets. See LICENSE for redistribution terms.

[dependencies]
alwith-extension = "0.6"
tauri-plugin-dialog = "3.0.0-alpha.4"
tauri::Builder::default()
    .plugin(tauri_plugin_dialog::init())
    .plugin(alwith_extension::init(Config { development_directory, ..Default::default() }));

Config { root, native_directory } overrides the default app_local_data_dir()/extensions root and the bundled resource_dir()/native libraries.

Grant alwith-extension:default only to participating windows in Tauri capabilities. Each such window creates one createTauriExtensionRuntime from /tauri, supplying application capability versions, actual service objects, author module mappings and an evaluator from /host. Call start() once per WebView; React StrictMode and page navigation are not instance ownership boundaries. Native destruction/navigation observation releases the old document's tokens. Do not release tokens merely because a window is hidden.

Main and settings windows can advertise the same application contributions while mounting different views. /react provides useHostSnapshot and ExtensionView; callers supply a container and error handler. Most applications use the default runtime and the management UI instead of assembling these parts. Commands and surfaces are explicit host UI outlets. There are no automatic global listeners or DOM prototype patches. Native capabilities are opt-in; no filesystem capability is registered automatically.

The root SDK and /host can be imported without DOM, React, or Tauri. /testing supplies in-memory data for independent hosts. /marketplace is an optional validated catalogue, not a second installer. /build is a Bun author tool. React and Tauri are optional peers; the selected adapter requires the corresponding peer.

Default Tauri runtime (0.5.2)

/tauri exports the runtime ALwith U uses, without U's product parts. A generated application only supplies its identity, language, toast and policies:

import { defineDefaultExtensionRuntime } from "@alwith/module-extension/tauri"

export const extensions = defineDefaultExtensionRuntime({
  host: { id: "my-app", version },
  language: () => i18n.language,
  // The host toast; returns { hide, setMessage } and calls onClose when dismissed
  notices: { show: (message, duration, onClose) => showToast(message, duration, onClose) },
  // apiCompatibilityVersions, http: { allowUrl }, externalLinks: { allowUrl }, clipboard,
  // capabilities, modules, contributions are optional
})
if (import.meta.hot) import.meta.hot.dispose(() => void extensions.dispose())
  • createDefaultExtensionRuntime(options) composes createCommonCapabilities over the native plugin (files, directories, binary HTTP, clipboard, notices), createTauriHttp, createTauriNotifications, createTauriExternalLinks and createTauriDialogs, then the host's own capabilities. HTTP denies every URL unless http.allowUrl allows it; external links allow every HTTP(S) URL unless externalLinks.allowUrl narrows them; the clipboard defaults to navigator.clipboard.writeText. All seven contribution kinds are supported at 1.0.0 (DEFAULT_CONTRIBUTIONS) unless contributions replaces them.
  • Extensions may require the SDK, /dom, /plugin, /react (mountReact only), react, react/jsx-runtime, react/jsx-dev-runtime (mapped onto the production runtime) and react-dom/client, plus the host's modules. React loads on the first evaluation, so /tauri itself does not import the React peer.
  • defaultRuntimeOptions(options) returns the same runtime options without the Tauri transport, for hosts that build the ExtensionRuntime themselves. wrapEvaluate?: (evaluate) => evaluate receives the standard CommonJS evaluator and returns the one the runtime uses, so a host can decorate specific extension factories (U binds yup-kb's Drive data this way); default: the standard evaluator unchanged.
  • defineDefaultExtensionRuntime(options, afterStart?) returns { getRuntime, start, dispose }: one runtime per WebView, created on first use; start is createExtensionStartup(getRuntime, afterStart) (refresh, start(), then afterStart, shared by concurrent callers; a failed startup is not kept). The caller wires dispose to HMR disposal or window teardown.
  • pickLocalExtension() invokes plugin:alwith-extension|pick_local. The native plugin still opens the folder dialog through tauri-plugin-dialog, so the host registers tauri_plugin_dialog::init() before alwith_extension::init(..) and grants alwith-extension:default (which includes allow-pick-local) to participating windows. No JS dialog permission is needed for the picker; createTauriDialogs needs dialog:allow-message.
  • /plugin's pluginHostCapability has no default provider: class-based plugins need a host-supplied PluginHost in capabilities.

/host exports the install flow the UI uses: installLocalExtension(runtime, path, { update?, source?, signal? }) installs or updates a picked folder, waits for every window to release the old package, enables new installations and reports activation; executeReload(runtime, id, signal) performs a coordinated reload; executeStagedInstall verifies a package the host staged itself.

React management UI (0.5.2)

/react ships U's extension management and outlets. Wrap them in ExtensionsHostProvider with the application's host:

import { ExtensionsHostProvider, ExtensionsSection } from "@alwith/module-extension/react"
import { pickLocalExtension } from "@alwith/module-extension/tauri"

<ExtensionsHostProvider
  value={{
    i18n: { language },
    openLink,
    reportError,
    confirm: (message, title) => ask(message, { title, kind: "warning" }),
    notify: (message) => toast.success(message)
  }}
>
  <ExtensionsSection
    runtime={extensions.getRuntime()}
    start={extensions.start}
    pickLocal={pickLocalExtension}
    onOpenSurface={openSurface}
  />
</ExtensionsHostProvider>
  • ExtensionsSection / ExtensionsManager: search, install from a folder, enable/disable, update from a folder, reload, uninstall after confirm, pending transitions with cancel, errors and service retry, contributed pages per extension and each extension's settings views. settings selects the compact settings-window variant.
  • Host hooks on ExtensionsSection, all optional; omitted, the section behaves as above:
    • install(runtime, { update?, onPhase }) replaces pickLocal + installLocalExtension (for example a host's own prepare/staging install). The section shows "preparing" first; the hook calls onPhase("installing") once a package is chosen and resolves an InstallResult, or null when the user cancels. With install, pickLocal may be omitted.
    • onEnabled(installation) runs after the enable switch, once runtime.settled() resolved without an error for that extension (for example a first-use notice).
    • reload(runtime, id, signal) replaces executeReload and resolves "reloaded" or "updated"; the notice follows the outcome.
    • onUninstalled(installation) runs after the uninstall transition completed in every window (grant or import cleanup). If the transition ends with the extension still installed, the hook is skipped and an error is reported.
    • refresh() replaces runtime.refresh() for the service Retry button and window focus.
    • renderIcon(installation) and notices(installation) pass through to ExtensionsManager.
  • ExtensionsManager also takes renderIcon?(installation) => ReactNode (replaces the manifest icon; undefined/null keeps it) and notices?(installation) => ExtensionNotice[] ({ id, content }, rendered under the row with a close button; a dismissal lasts until the installation's package revision changes).
  • useExtensions(runtime, start?, onError?, { refresh? }?): snapshot and contributions as React state; runs start on mount and refresh (default runtime.refresh()) on window focus.
  • Outlets: ExtensionNavigation (the leading rail's app buttons), ExtensionActions (topBar / navigation placements), ExtensionStatusBar, ExtensionSettingsNavigation + ExtensionSettingsContent, ExtensionMount, ExtensionViewContent (local error and retry) and ExtensionPage (a surface or settings page as a full page). ExtensionViewContent and ExtensionMount take onOpen?(view) => boolean | Promise<boolean>, a gate that runs once per view id before a surfaces or settingsPages view renders (U's legacy consent), and onCancel?(); false or a rejection (reported through onError) keeps the view closed. ExtensionPage takes onOpen and closes itself through onClose when the gate cancels. ExtensionIcon and EXTENSION_ICONS resolve manifest and contribution icons; ExtensionSettingsControls presents DOM-based extension settings through the host's select and switch controls.
  • Strings come from extensionLocales (en and zh-CN complete, other U languages partial, falling back to en). A host with its own i18n passes t; it then receives the keys prefixed with extensions. and must provide every key of extensionLocales.en.
  • extensionActions(installation) decides whether an installation offers update and uninstall; the default hides both for bundled:* sources.
  • The components use Tailwind classes and the shadcn tokens of the shared U presentation; the application's Tailwind setup must scan this package's dist. The catalog layout stylesheet (react.css) is imported by the components themselves.

Host-owned parts

The package does not decide authorization or product behavior. The application keeps:

  • Policy: which origins HTTP may reach, which links may open, which installations may be updated or removed, and any capability authorization.
  • Bundled, legacy and imported extensions: seeding bundled packages after startup (afterStart), legacy Desktop conversion, startup imports, grant cleanup after uninstall, and their notices and consent flows.
  • Product capabilities and modules: application-specific providers (for example a Drive bridge or wallpaper), the PluginHost provider, and a host UI kit offered to extensions through modules.

These plug into the package's flows through the host hooks above (wrapEvaluate, ExtensionsSection's install, onEnabled, reload, onUninstalled, refresh, renderIcon, notices, and the views' onOpen) rather than through a copy of the management UI.

Manifest format 3

{
  "id": "my-extension",
  "name": "My extension",
  "version": "1.0.0",
  "dependencies": { "@alwith/module-extension": "^0.6.0" },
  "hosts": { "alwith-u": ">=0.1.1" }
}

Optional display metadata: description and author are nonempty strings; authorUrl is an absolute HTTP(S) URL without credentials. icon accepts an HTTP(S) image URL or a local glyph ID: lucide:blocks, lucide:play, lucide:panel, lucide:settings, lucide:clock, lucide:chart, lucide:globe. Glyph IDs are exact lowercase names matching the existing contribution icon vocabulary. Unknown names are rejected at build/install. authorUrl cannot use glyph IDs. Omit unused fields, not null or blank strings.

parseManifestIcon(icon) returns { type: "image", url } or { type: "lucide", name } without React/DOM/Lucide dependencies. Hosts map glyphs to local components; U shares the map with its extension entrypoints. Remote images retain their existing error fallback. Local glyphs work while offline or disabled. Package-relative paths and data URLs remain unsupported. Metadata does not affect activation or permissions.

Optional locales translates only name and description, keeping all metadata in one manifest:

{
  "id": "localized-example",
  "name": "Notes",
  "description": "Personal notes",
  "version": "1.0.0",
  "dependencies": { "@alwith/module-extension": "^0.6.0" },
  "locales": {
    "zh": { "name": "笔记", "description": "个人笔记" },
    "zh-CN": { "name": "我的笔记" }
  }
}

resolveManifestText(manifest, language) is a pure helper independent of React, DOM, Tauri and i18next. Each field resolves by exact language, primary language, then the top-level default, ignoring language case. Missing descriptions remain absent; a translation may provide a description even when the default omits it. The manifest and author language keys remain unchanged.

The public package validates translations at build/install boundaries and persists them inside Installation.manifest.locales. Hosts supply their current language and refresh the display on language changes, including search, accessibility labels and management prompts. Disabled extensions work without activation. Tauri notification/dialog providers accept { language: () => currentLanguage }, evaluating each operation; omitted callbacks use default text. Extension page content remains the extension's responsibility.

Language keys require 2–8 ASCII letters followed by optional hyphen-separated 1–8 ASCII alphanumeric subtags (en, zh-CN, zh-Hant-TW, es-419). Case-insensitive duplicates are rejected. Each locale entry must contain at least one nonblank string name or description; unknown fields, null, arrays and empty entries are rejected. locales may be omitted or {}. No extra translation files, placeholders, default-locale field or host i18n dependency are needed.

The current snapshot uses locales exclusively. NLS files are ordinary assets with no special parsing; %key% strings remain literal. All author examples use extension version 1.0.0 and SDK range ^0.5.0. The U host constraint remains independent at >=0.1.1.

manifestVersion defaults to 3, entry to main.js, dataSchemaVersion to 1, and hosts to an empty unrestricted map. Only the SDK dependency is supported; this is a compatibility constraint, not an instruction to install npm packages. A nonempty hosts map is an allowlist of alternative host IDs with independent version ranges. extensionApiVersion is generated from this package's package.json.version before tests, type checking and distribution builds. Hosts pass apiVersion: extensionApiVersion and their own host: { id, version } to the runtime. Use parseManifest(raw) to normalize author JSON before passing it to a standalone JS host; native snapshots already contain defaults.

A host that has verified support for an earlier SDK API can supply additional concrete versions:

apiVersion: extensionApiVersion,
apiCompatibilityVersions: ["0.1.0-rc.20"]

The SDK dependency must match the current version or one explicitly supported version. No older versions are accepted by default. The current version, every compatibility version and the requested range must all be valid; an alias cannot hide an invalid current version. Normal semver prerelease rules apply: 0.1.0-rc.20 satisfies ^0.1.0-rc.11, but not ^0.1.0-rc.21, ^0.1.0 or ^0.2.0. This declaration changes only SDK dependency matching. Manifest versions, host constraints, capabilities and native authorization remain independent. Installed packages, certificates, data and enable state are not rewritten.

There is no v2 compatibility path. Old fields (engines, capabilities, contributions, externals) are rejected. Rebuild old development extensions; existing v2 native state is not automatically converted or deleted. Back up development data before manually replacing an old store. Storage-layout migration is a separate mechanism and does not make v2 manifests compatible.

SDK and host versions are checked before module evaluation. Missing modules fail at require, missing capabilities fail when requested, and unsupported contributions fail on registration. Activation failure releases managed resources; later callback failures must be reported by the caller. Side effects already sent outside the process cannot be rolled back. The evaluator uses only the host's explicit author module map and still denies private host/tauri/testing entrypoints. Private capability authorization remains the host's responsibility.

dataSchemaVersion remains optional author metadata with default 1. It is a format marker, not JSON structure validation. Data format changes still require migration support; the current service rejects such updates. The maintained SDK package, runtime API and Rust crate use the 0.5.x release line.

Build an extension

import { buildExtension } from "@alwith/module-extension/build"

await buildExtension({ sourceDirectory: "./my-extension", outputDirectory: "./build/my-extension" })

The source contains manifest.json, main.tsx and optional assets. The default export is a defineExtension(context => ({ onload, onunload })) factory. Register commands with context.command and views with context.view("settings" | "surfaces" | "statusBar" | "settingsPages", view). Start rendering and background work in a view's mount hook. Register cancellation listeners and all cleanup with the provided owner.

The tool produces one CommonJS entry (main.js by default), checks its imports, and emits a compact manifest without an externals list. It rejects unknown/dynamic requires, private host entrypoints, links and output overwrites. Only author SDK, DOM helpers, mountReact, shared React and JSX modules belong in the evaluator map. This execution model is for trusted same-context UI code, not a security sandbox.

examples/portable exercises commands, settings, a React surface and compare-and-swap data. Its manifest depends on @alwith/module-extension: ^0.5.0. examples/host is an independent two-window Tauri consumer; it installs the packed tarball and never imports the U or Desktop source trees.

State and errors

Native storage uses the application's local data directory under extensions/; hosts and development identifiers remain isolated. Native state selects an immutable package, installation identity and data generation. The service owns all writes; data writes require the expected revision and live instance token. Conflict, corruption, revoked instance and absent data are distinct results.

Installed layout: extensions/<extensionId>/packages/<packageRevision>/ contains the read-only code/resources; extensions/<extensionId>/data/data.json holds mutable configuration. A bounded .previous.json supports interrupted-write recovery. extensions/state.json remains the shared authority for installation identity and selected revisions. Existing package-layout v1 stores migrate on open, preserving the original trees under .legacy-v1/; older binaries cannot open the new layout. Public SDK/data APIs are unchanged. See native persistence in the plugin crate's documentation.

Activation awaits asynchronous hooks. Deactivation revokes new calls immediately, then awaits hooks and LIFO resource cleanup. Cleanup errors are aggregated. A hung hook remains hung; no timeout reports a false successful stop. Update, disable and uninstall commit only after every participating window releases its instance. Events carry full sequenced snapshots and a service identity; data changes notify subscribers without reloading code. Call refresh() on transport reconnection or window focus.

Use native plugin:alwith-extension|pick_local (commands.pickLocal()) to grant a user-selected directory to the calling window, then request installLocal or an update transition through the runtime. Package resources use a scoped read-only URI scheme with real URL directories for relative CSS assets; no home-directory asset scope is required.

Uninstall retains installation source and data. Reinstall from a different source is rejected. The current native installer supports local directories; it deliberately rejects data schema changes with migrationRequired. ZIP/network installation, explicit data migrations/rollback, data clearing and package garbage collection are not yet implemented. Immutable old content is retained. Do not present this initial package as completion of the architecture document's full stability phase.

Development and verification

From this directory in alwith-modules:

bun install
bun run test
bun run check
bunx tsc -p tsconfig.core.json
bun run build
bun pm pack
cargo test --manifest-path rust/Cargo.toml
cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings

The plugin's Rust types are the source of src/generated/commands.ts (UPDATE_BINDINGS=1 cargo test --manifest-path rust/Cargo.toml); src/tauri/protocol.ts names its types by direction. The JSON schemas come from cargo run -p alwith-extension-impl --bin generate-protocol. Keep generated changes under review with their Rust source. Tests cover delayed activation, cancellation, cleanup failures, real bundled JSX, multiwindow CAS and stop barriers, content validation and storage failure recovery. Target-platform WebView behavior and power-loss behavior still require platform validation before a stable release or replacing Desktop.

Optional native capabilities

The root SDK exports httpCapability, notificationsCapability, externalLinksCapability and dialogsCapability, plus DOM-free contracts. Each capability is version 1.0.0. Extensions request these providers directly through context.capability(key). Missing providers throw; host authorization still applies. context.supportsCapability(key) checks provider availability, not authorization. No capability declarations belong in the manifest.

Hosts explicitly register providers from @alwith/module-extension/tauri/capabilities:

import { httpCapability, dialogsCapability } from "@alwith/module-extension"
import { createTauriHttp, createTauriDialogs } from "@alwith/module-extension/tauri/capabilities"

const capabilities = {
  [httpCapability.id]: createTauriHttp({
    allowUrl: (url, _extensionId) => url.origin === "https://example.com",
    timeoutMs: 30_000,
    maxResponseBytes: 8 * 1024 * 1024
  }),
  [dialogsCapability.id]: createTauriDialogs()
}
// Supply capabilities to createTauriExtensionRuntime's options.

HTTP additionally requires the optional @tauri-apps/plugin-http JS peer (3.0.0-alpha.1+) and the host's matching Rust plugin. The HTTP peer loads only when a request is made. Other adapters use the existing Tauri API peer and await native commands directly. The host initializes the selected Rust plugins and configures native ACLs:

| Provider | Rust plugin | Permissions | | -------------------------- | ------------------------------------ | -------------------------------------------------------------------- | | createTauriHttp | tauri-plugin-http | http:default with explicit URL allow scopes | | createTauriNotifications | tauri-plugin-notification | notification:default | | createTauriExternalLinks | tauri-plugin-opener | opener:allow-open-url with allowed URL scopes, or opener:default | | createTauriDialogs | tauri-plugin-dialog 3.0.0-alpha.1+ | dialog:allow-message |

The extension plugin is the alwith-extension crate; its implementation ships as a prebuilt library in this package. These adapters reuse host-linked native plugins; business extensions never gain a private Rust build. A non-Tauri host can implement the same contracts through a provider's create(binding) callback. Existing { version, value } capability registrations remain supported, but a shared value does not automatically acquire lifecycle guards: use a binding factory for native or stateful services.

Providers are cached per activation, receive the manifest identity and cancellation, and register one completion barrier with the owner. Retained methods reject after revocation, including after reactivation of the same extension. Every pending operation is joined before normal owner cleanup finishes. Rejections remain observable on the original call; disposal does not report the same operation error a second time.

HTTP accepts text request bodies and returns status, final URL, headers and a UTF-8 text body. The default response limit is 8 MiB of bytes, with a 30-second timeout including reading. HTTP errors such as 404 remain responses; transport/ACL/limit errors reject. Automatic redirects are disabled: a 3xx response requires a new explicit request, checked against host policy again. Only HTTP(S) URLs without embedded credentials are accepted; the adapter does not inject host login tokens or expose unsafe TLS/proxy options. To avoid implicit shared authentication, hosts MUST disable the HTTP plugin's cookies Cargo feature (including defaults enabled transitively). The plugin ignores JS credentials and otherwise shares a cookie jar between the application and every extension. Check the resolved feature graph; a host that needs automatic cookies must supply a separate isolated transport instead. A caller may pass a view's Cancellation to end work when the view closes. Instance revocation, caller cancellation and timeout propagate to the plugin's AbortSignal. The upstream HTTP plugin handles native resource cancellation; its JS API does not provide an independently awaited cancellation acknowledgement, so this is not a hard guarantee of native network quiescence.

Notification permission requests are explicit; send never opens a permission prompt. Permission values reflect the plugin's report, not independently verified OS authorization: the current desktop backend reports granted without an OS permission query. send resolves when the native command accepts the request, not when the OS delivers or displays it. IPC and returned native-command errors propagate, but the upstream desktop backend may discard later OS delivery errors. Notifications prefix their title with the extension name. Link opening accepts only HTTP(S) URLs allowed by host policy and offers no executable or file-path option. Dialogs expose message and confirmation only, using the extension name as the title; they do not grant filesystem access. Native dialogs, already submitted notifications and browser launches cannot be undone by cancellation. Revocation rejects late successful results, and an open dialog keeps cleanup pending until the user closes it.

The evaluator and capability facade are lifecycle/convention boundaries for trusted extensions, not isolation from other JavaScript in the same WebView. Tauri ACLs remain window-scoped. Filesystem access, subprocesses, credentials, application login, Runtime sessions and business-specific native services belong to separately authorized host providers.

For a host that does not need automatic HTTP cookies (including U), preserve the other default transport features explicitly:

tauri-plugin-http = { version = "3.0.0-alpha.1", default-features = false, features = ["rustls-tls", "http2", "charset", "system-proxy"] }

Verify cargo tree -e features -i tauri-plugin-http contains neither cookies nor default. Cargo features unify across dependencies; setting default-features = false in only one dependency declaration is insufficient if another dependency re-enables cookies.

Host UI outlets

In addition to commands and surfaces, hosts can advertise these 1.0.0 contributions:

| Contribution | Registration | Host responsibility | | --------------- | -------------------------------------- | -------------------------------------------------------------------- | | topBar | context.action("topBar", action) | Render a compact toolbar action | | navigation | context.action("navigation", action) | Render a navigation entry | | statusBar | context.view("statusBar", view) | Mount a compact persistent widget | | settingsPages | context.view("settingsPages", view) | Render a settings column entry and mount its full page when selected |

Actions have id, title, optional icon and order, and target: { type: "command" | "surface", id }. Both action and target IDs must be local nonempty ASCII IDs containing only letters, digits, ., _, -. The host prefixes both with the extension ID, so an action cannot name another extension's contribution. Register the referenced command or surface as well; registration order is free. Targets may disappear independently, so host UIs must disable or hide actions without a current target. Execute commands through their guarded run() function; route surface targets through the host's navigation.

HostSnapshot.actions publishes actions only for active instances; stop/failure removes them along with views. Existing consumers of commands/views keep working. Main and settings windows may advertise all supported application contributions but only mount their actual local outlets. Lifecycle compatibility does not imply a view is rendered in every window.

Presentation metadata is optional on views and actions. order must be finite (host default 0); icon names are blocks, play, panel, settings, clock, chart, globe. mountReact preserves both fields. The package owns no application layout or UI toolkit. Dimensions, ordering, scrolling, empty-state behavior and placement remain host decisions. Put timers and subscriptions in mount scope and release them on unmount.

The top-bar, navigation, status-bar, and settings-page examples require extension API ^0.1.0; build them along with the four existing entry examples using bun run build:entry-examples <new-output-directory>.

Bar alignment

ContributionPresentation.alignment accepts "left" | "center" | "right" for topBar actions and statusBar views. mountReact forwards it unchanged. Invalid values are rejected on registration. Navigation and settings/page outlets ignore alignment. Existing contributions remain compatible: U defaults omitted alignment to left for both top-bar actions and status-bar views. Explicit center/right alignment is preserved. Legacy ribbon actions retain the reviewed business icons (bar-chart-3, file-text, book-open, shield-check) and explicitly request left alignment.

U uses three equal-width groups in each bar's available space. Center is relative to that available space (after native controls, sidebar and version-info clearances), independent of side content width. Within each group, order is ascending from left to right, ties use the full contribution ID. A crowded group scrolls horizontally without displacing another group. Unoccupied toolbar space stays available for dragging the native window.

Alignment is included in SDK ^0.1.0. The top-bar and status-bar examples (extension version 1.0.0) demonstrate all three positions.

Contribution lifecycle and host helpers

  • context.supportsContribution(kind) checks the application's supported outlets. Revoked instances throw; unsupported outlets return false and attempts to register them throw. No contribution declarations belong in the manifest. This is application support, not whether the current window has mounted an outlet.
  • The disposer returned by context.view() synchronously removes that registration and cancels all its mounts. Await it to wait for every mount's asynchronous cleanup. Other views and the extension remain active. A new registration with the same ID has a separate lifetime.
  • /host exports resolveActionTarget(snapshot, action) for ownership-aware command/surface lookup and sortContributions(items) for default order/ID sorting without mutating the input. Layout, routing and disabled/hidden presentation remain host responsibilities.
  • mountReact() catches component rendering failures inside the extension root. host.mount(id, container, cancellation?, onError?) reports them as ViewRenderError with extensionId, viewId and cause; the instance diagnostic snapshot records the error without stopping unrelated views. ExtensionView.onError receives these errors too. The host renders its own local error/retry UI; remounting recreates the root. Errors reported after cancellation are ignored. This does not catch arbitrary asynchronous tasks or event-handler errors; extension code must handle or report those explicitly through the mount context's optional reportError.

The top-bar example requires SDK ^0.1.0 and checks whether the host supports topBar. Hosts without a top bar can still expose its required surface through their page navigation.

Class-based Plugin SDK

The optional @alwith/module-extension/plugin author entrypoint exports concrete Plugin, Component, ItemView, WorkspaceLeaf, PluginSettingTab, Setting and control classes. createPluginExtension(context, { manifest, styles?, create }) adapts a typed plugin to the same runtime, contribution registry and revisioned context.data service as a factory extension. Hosts explicitly provide pluginHostCapability (plugin.host); this is independent of the opt-in legacy host policy. Authors using this entrypoint should declare SDK dependency ^0.1.8 or newer. SDK and host compatibility checks still occur before activation.

import { defineExtension } from "@alwith/module-extension"
import { createPluginExtension, Plugin } from "@alwith/module-extension/plugin"

class CounterPlugin extends Plugin {
  async onload(): Promise<void> {
    await this.saveData({ count: 0 })
    this.addCommand({
      id: "read",
      name: "Read counter",
      callback: async (): Promise<void> => {
        const data = await this.loadData()
        await this.app.chat.sendMessage(JSON.stringify(data))
      }
    })
  }
}

export default defineExtension(context =>
  createPluginExtension(context, {
    manifest: context.manifest,
    create(_api, app, manifest): Plugin {
      return new CounterPlugin(app, manifest)
    }
  })
)

The create callback receives bound constructors, the explicit PluginApp and its manifest. api.Plugin can be constructed without arguments; public Plugin(app, manifest) and WorkspaceLeaf(app, location?) also accept the explicit app. ItemView(leaf) inherits its leaf's owner. Each activation retains its own data queue, workspace, callbacks and cleanup; no mutable default app is used. loadData() returns JSON or null, and saveData(value) uses the current revision and manifest data schema version. Existing legacy bundles consume these same implementations through their historical @alwith/extension binding.

For an idle empty leaf, setViewState creates the view and starts onOpen synchronously before returning its promise. Initial state is applied as soon as onOpen completes, before managed component loading adds further asynchronous work. Replacements remain serialized and wait for the previous view's close and cleanup. Callers should await the returned promise before delivering fetched content to a view whose onOpen performs asynchronous initialization, and to observe initialization failures and completion.

Legacy CommonJS compatibility

The optional @alwith/module-extension/legacy entrypoint provides createLegacyExtension(context, { manifest, source, styles?, modules? }) and legacyHostCapability. styles is an installed resource path. Hosts must explicitly provide legacy.host; importing the main SDK does not install DOM helpers or activate this compatibility layer.

This generic API does not recognize business extension IDs. Hosts can layer optional application-specific compatibility policies over Plugin/Component lifetime, ItemView/WorkspaceLeaf, workspace tabs, commands and top-bar actions, settings controls, Modal, Notice, and requestUrl. The workspace surface retains view instances across outlet mounts; settings is a settingsPages contribution. Unsupported external CommonJS imports fail explicitly. Package-relative JS/JSON modules, directory entries, caching and circular references are supported through modules. app.vault.adapter.getResourcePath() and relative fetch map to activation-bound package resources. registerVivariumSurface registers independent surfaces with local IDs vivarium-<id>. The host chooses their page outlet; U uses its leading screen. Mount cleanup owns surface-created leaves without adding them to workspace tabs. The workspace surface remains available for older view-based entry points.

LegacyHost supplies fetch, restricted invoke, vault write/remove, notices, language, external links, view navigation, and chat sending. Chat context must be captured once per activation at a user interaction and remain immutable for later asynchronous sends. The host enforces native command/path authorization. Optional loadInitialData and acknowledgeInitialData migrate a legacy snapshot only after the portable data write succeeds. Existing data subscriptions update live settings objects without replacing references captured by business code; removed keys are removed from the live object.

PluginHost.presentModal(modal, present) optionally controls when a compatibility modal is displayed. Call present() to show it; return a disposer to remove a queued presentation on close or unload. Repeated or stale presentation callbacks do not reopen a closed modal. Hosts without this hook display modals immediately. Host policies must preserve explicit consent and the modal callbacks.

Optional bindStorage(storage) receives {read, write, exists, resource} before entry evaluation and returns a disposer. Hosts route direct legacy data.json file aliases to this port so Plugin and file APIs share initialization and the write queue. exists() distinguishes absent data from stored JSON null. manifest.dir is a virtual own-extension directory; hosts map it without writing back into the old application.

Native hosts use validate_manifest, Service::installed_package, and the Tauri plugin::installed_package/plugin::snapshot wrappers instead of reading private service state or storage layout. Package snapshots validate the enabled installation, source, transition state, selected manifest and complete revision. These host-only APIs do not replace activation tokens for extension-facing calls.

The evaluated module gets per-instance window/document facades. Timers, window and document listeners, directly appended body/head nodes, views, styles, and modals are released with the activation. Created elements remain native DOM objects. The finite DOM convenience methods are installed additively and idempotently on HTMLElement. These facades provide compatibility and resource ownership; they are not a security sandbox. Hosts should only evaluate reviewed, trusted sources, and review package-specific background behavior before admission. Session archive/export and unrestricted Tauri access are not part of this API.

Common host capabilities (0.1.5)

The SDK exports filesCapability, directoriesCapability, binaryHttpCapability, clipboardCapability, and noticesCapability. Hosts register providers from createCommonCapabilities (the /host export), injecting native command dispatch, clipboard writing, and in-app notice presentation. Providers own their activation lifecycle; revocation rejects further use and teardown waits for accepted native work.

Files use bytes (Uint8Array). Directory selection returns an opaque handle with id, display name, and scoped files; the native host must persist and check grants for each extension. Private root data.json is reserved for context.data. Neither these providers nor the legacy facade constitute a JavaScript sandbox.

Rust filesystem supplies bounded, relative-path file operations. The optional native-http feature supplies bounded binary HTTP without automatic redirects or shared cookies. The host still authorizes installation identity, paths and URLs. Native HTTP has no cancellation handle yet: abort suppresses delivery, teardown waits, and transport timeouts bound execution. Legacy hosts can override command names and reuse createScopedFileOperations, createBinaryHttp, and createBinaryFetch without exposing old command syntax to modern extensions.

noticesCapability is an in-app message with hide and setMessage, distinct from OS notificationsCapability. Closed notices cannot be revived and activation cleanup hides them. Clipboard currently supports text writes only.

Chat drafts

With SDK 0.1.9 or newer, plugins can call await app.chat.setDraft(text) to fill an empty composer for user review. This never sends a message. The host must implement the optional PluginHost.setDraft method; unsupported hosts reject the call. Existing app.chat.sendMessage(text) calls still send directly.

U targets the currently selected writable chat. Existing text, attachments, or file mentions cause draft placement to reject instead of overwriting user input. Select a chat first; the operation does not silently create or choose a conversation. Both methods return promises: handle rejection when subsequent plugin actions require successful delivery. Sending success means acceptance, not turn completion.

Coordinated reload (0.1.0-rc.11)

beginTransition with action: "reload" stops all instances without changing the selected package, data generation, source or persistent enabled intent. Disabled installations reject reload. The native barrier requires every lease to release before commit; cleanup failure keeps the transition pending. Canceling an uncommitted transition preserves the old selection.

A committed reload increments the installation's optional activationRevision. Runtime identity includes this value so a failed activation can be retried even when a window misses the pending snapshot. It is separate from package content and data revisions. Hosts must confirm transition completion and local activation rather than treating request acceptance as successful reload. This does not fetch packages, migrate data or provide automatic rollback.