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

@360crewing/marketplace-sdk

v0.3.0

Published

Public SDK contract for 360crewing marketplace extensions: defineExtension, hooks, slot types.

Readme

@360crewing/marketplace-sdk

Public SDK contract for 360crewing marketplace extensions. This is a contract: after 0.1.0, breaking API changes ship only in a major version.

Install

npm i @360crewing/marketplace-sdk

Peer dependencies (provided by the host via Module Federation shared — an extension does NOT bundle its own copies): react, axios, react-i18next, @360crewing/ui.

Quick start

import {defineExtension, useUser, useI18n, Card, Button, Slot} from "@360crewing/marketplace-sdk";
import en from "./locales/en.json";

// dashboard.widget — a card on the dashboard (bare component).
function KpiWidget() {
    const user = useUser();
    const {t} = useI18n();
    return (
        <Card title={t("title")}>
            <p>{t("greeting", {name: user.full_name})}</p>
            <Button title={t("refresh")} onClick={() => {}}/>
        </Card>
    );
}

// seafarer.menu — a menu item on the seafarer card + the page it opens.
function CrewAnalyticsPage(props: {seafarer: {uuid: string}}) {
    return <div>Analytics for seafarer {props.seafarer.uuid}</div>;
}

export default defineExtension({
    id: "com.acme.crew-analytics",            // reverse-DNS, unique
    locales: {en},                            // >=1 locale; files in locales/*.json
    slots: {
        [Slot.Dashboard.Widget]: KpiWidget,                       // widget: a component
        [Slot.Seafarer.Menu]: {                                   // menu: {title,icon?,page}
            title: "Crew Analytics",
            page: CrewAnalyticsPage,
        },
    },
});

Slot model

A slot is a named mount point in the host. The slot name = where the extension appears. Two archetypes:

| Archetype | Slots | What you register | |---|---|---| | widget | Slot.Dashboard.Widget | a bare React component (a card in the dashboard grid) | | menu | Slot.Seafarer.Menu, Slot.Vessel.Menu, Slot.Contract.Menu, Slot.Nav.{Admin,Data,Finance,Fleet,Reports}.Menu | MenuRegistration { title, icon?, page } |

  • Slot is a nested namespace (one import): Slot.Seafarer.Menu, Slot.Nav.Data.Menu, Slot.Dashboard.Widget. A raw string key ("seafarer.menu") is also accepted and type-safe.
  • menu slot: title is the item label, page is the page component the host renders on click (the route is derived from the extension id).
  • One extension can occupy several slots; several extensions in one slot → several menu items.
  • Context in props: widget → { widget:{id,settings}, sdk }; entity-menu → { seafarer|vessel|contract:{uuid}, sdk }; nav-menu → { sdk }.

defineExtension validates: reverse-DNS id, >=1 locale, >=1 slot, and that a menu slot is { title, page }.

i18n

  • Text lives in locales/<lang>.json; defineExtension({ locales: { en, ru } }).
  • The host registers them as the i18next namespace app.<id> on load.
  • useI18n() is react-i18next bound to that namespace: const {t} = useI18n(); t("title"). Content re-renders when the user changes language (no reload).
  • No translation for the active language → falls back to the host default.

Storage

The platform gives an extension two primitives (no backend of your own):

useUserSettings() / sdk.userSettings — per-user settings (saved filters, UI prefs). Small JSON, last-write-wins, scoped per user.

const us = useUserSettings<{ visaFilter?: object }>();
await us.set({ visaFilter });             // shallow-merge
const { visaFilter } = await us.get();    // whole document

useDb() / sdk.db — the extension's own database: one isolated SQLite file per (tenant, installation). Full SQL, the extension owns its schema. query (SELECT), exec (INSERT/UPDATE/DELETE/DDL), tx.

const db = useDb();

// schema — once (on install / migration)
await db.exec(`CREATE TABLE IF NOT EXISTS visa(
  id INTEGER PRIMARY KEY, seafarer_uuid TEXT, country TEXT,
  status TEXT, applied_at TEXT, docs JSON)`);

// CREATE
await db.exec(
  `INSERT INTO visa(seafarer_uuid,country,status,applied_at,docs) VALUES(?,?,?,?,?)`,
  [uuid, "US", "pending", "2026-05-18", JSON.stringify(["passport","photo"])]);

// READ + filter, including an array column (JSON1)
const rows = await db.query(`
  SELECT * FROM visa
  WHERE status IN ('pending','submitted') AND country = ?
    AND EXISTS (SELECT 1 FROM json_each(docs) WHERE value = 'passport')
  ORDER BY applied_at DESC LIMIT 50`, ["US"]);

// UPDATE / DELETE / transaction
await db.exec(`UPDATE visa SET status=? WHERE id=?`, ["approved", 1]);
await db.tx(async (t) => {
  await t.exec(`DELETE FROM visa WHERE status='rejected'`);
  await t.exec(`INSERT INTO audit(ts) VALUES(?)`, [Date.now()]);
});

Isolation: the server derives tenant/installation from the app-JWT, not the request body; SQL is pinned to that file — no cross-extension data. Host guards: SQLite authorizer denylist (no ATTACH/load_extension/file pragmas), per-query timeout, result cap, file quota, write serialization. The SQLite dialect is a stable public contract.

What's included

| Group | API | |---|---| | Entry point | defineExtension({ id, locales, slots, onActivate?, onDeactivate? }) | | Hooks | usePlatform, useUser, useTenant, useNotify, useModal, useConfirm, useForm, useTheme, useI18n, useNavigate, useUsage, useDb, useUserSettings | | Slots | Slot.* + MenuRegistration + context types (DashboardWidgetContext, SeafarerMenuContext, VesselMenuContext, ContractMenuContext, NavMenuContext) | | UI | re-exports all @360crewing/ui primitives (Button, Card, TextField, …) |

Boundaries

  • Hooks are the only way to use host services (Modal / Toast / Confirm / Form / platform data / i18n / navigation). Extensions have no direct access to host managers.
  • usePlatform() is a typed client for platform data (platform.seafarers/vessels/contracts.list() / .search(params) / .get(uuid)). No hand-written URLs or JSON; the host injects an app-scoped JWT and the server checks scope. No raw axios.
  • useNavigate() navigates the host app (host-absolute paths).
  • Hooks work only inside a component the host mounted into a slot. Calling them at module scope throws a clear error.

Building & shipping an extension

An extension is a Module Federation remote, built with the reference MF tooling (Rsbuild/Rspack, @module-federation/rsbuild-plugin), that exposes ./extension = the default export of defineExtension(...). @360crewing/* and react are declared shared (import:false) so the extension always uses the host's instance (deduped across the MF boundary). A working reference lives in this package's example/ directory (rsbuild.config.ts + locales/).