ptech-shell-dev
v2.9.0
Published
Standalone/mock shell service implementations for Module Federation apps.
Downloads
1,911
Maintainers
Readme
ptech-shell-dev
Standalone and development implementations for the contracts exported by ptech-shell-sdk.
Install
npm i ptech-shell-dev ptech-shell-sdkReact is a peer dependency through ptech-shell-sdk.
What This Package Owns
initStandaloneServices: registers a complete local shell service set.createTestServices: creates the standalone services without registration.createDevPreset: creates reusable standalone bootstrap defaults.- Standalone services for i18n, user, tenant, app settings, onboarding, config, permissions, shared state, navigation, realtime, observability, notification, analytics, request context, and API client.
- Adapters such as
createMsalUserServiceandcreateReactRouterNavigationAdapter. - API error mapping with
mapApiErrorToUiError.
Standalone Bootstrap
Use this in a remote that must run without a real host.
import { initStandaloneServices } from 'ptech-shell-dev';
initStandaloneServices();Default bootstrap values:
apiBase:http://localhost:4000apiBaseRoutes:{}lang:viuser:{ id: 'dev', name: 'Dev User' }navigationPath:/standaloneregisterMode:if-missing
Override defaults per remote:
import { initStandaloneServices } from 'ptech-shell-dev';
initStandaloneServices({
apiBase: 'http://localhost:5010',
apiBaseRoutes: {
audit: '/audit/api',
ats: { url: 'http://localhost:5020/api' },
},
originPolicy: {
trustedOrigins: ['http://localhost:5020'],
},
lang: 'en',
navigationPath: '/remote-a',
featureFlags: {
'ui.experimental': true,
},
});Use registerMode: 'always' only when intentionally replacing existing services. In host-composed mode, the default if-missing protects real host services from being overwritten by standalone mocks.
Creating Services Without Registration
import { createTestServices } from 'ptech-shell-dev';
const services = createTestServices({
apiBase: 'http://localhost:4000',
});
const result = await services.apiClient.request<{ id: string }>({
url: '/v1/items/1',
});Standalone onboarding accepts optional campaign seeds. With no campaign, a registered page tour remains available for manual replay but never auto-starts.
const services = createTestServices({
onboarding: {
campaigns: [{
campaignId: 'local-audit-intro',
tourKey: 'audit-tool.evidence',
appKey: 'audit-tool',
contentVersion: '3',
revision: 1,
scope: 'app',
enabled: true,
autoShow: true,
progress: 'pending',
}],
},
});API Client Behavior
createStandaloneApiClient implements the ApiClient contract from ptech-shell-sdk.
It provides:
- base URL resolution with optional
apiBaseRoutesand per-requestappKey, - JSON body handling for plain objects,
- request context headers (
traceparent,x-correlation-id), - tenant header propagation through
X-Tenant-Id, - optional Authorization header propagation from
UserService.acquireAccessToken, - destination trust enforcement from
ptech-shell-runtime: arbitrary cross-origin URLs receive no shell credentials, tenant, or trace context, - timeout and caller abort handling,
- retry for idempotent methods by default,
- ProblemDetails/error normalization into
ApiError, - invalid JSON handling for explicit
responseType: 'json', - POST failure notification side effect that never interrupts error propagation.
UI code should map normalized errors with mapApiErrorToUiError and translate the returned i18n key.
import { TOKENS, getService } from 'ptech-shell-sdk';
import { mapApiErrorToUiError } from 'ptech-shell-dev';
const api = getService(TOKENS.apiClient);
try {
await api?.request({ url: '/v1/items', method: 'POST', body: { name: 'A' } });
} catch (error) {
const uiError = mapApiErrorToUiError(error as Parameters<typeof mapApiErrorToUiError>[0]);
console.error(uiError.i18nKey, uiError.supportTraceId);
}MSAL Adapter
import { PublicClientApplication } from '@azure/msal-browser';
import { TOKENS, registerService } from 'ptech-shell-sdk';
import { createMsalUserService } from 'ptech-shell-dev';
const msal = new PublicClientApplication(msalConfig);
registerService(
TOKENS.userService,
createMsalUserService({
msal,
defaultScopes: ['User.Read'],
loginMode: 'popup',
}),
);React Router Navigation Adapter
import { TOKENS, registerService } from 'ptech-shell-sdk';
import { createReactRouterNavigationAdapter } from 'ptech-shell-dev';
registerService(
TOKENS.navigation,
createReactRouterNavigationAdapter({
navigate,
location,
createHref,
}),
);Host / standalone base-path (mount prefix)
The shell owns base-path so remotes never hand-roll it. Internal links work whether a
remote runs standalone (/settings) or is mounted under a host prefix (/audit-tool/settings).
Basename authority — exactly one source at a time:
- Bound to a real react-router → react-router's own
basenameis the sole authority.nav.navigate(to)receives the app-relativetounchanged (react-router applies the prefix);nav.createHref(to)is prefixed by the adapter's basename kept in sync with it. - Unbound / standalone → the adapter/mock applies its own static
basenameoption.
Never set both a bound react-router createHref and a static basename — a dev-mode
warning fires if you do. Invariants: getPath() / getSnapshot().location.pathname are
always app-relative (basename-stripped) in both modes; only createHref() returns the
host-prefixed form.
Batteries-included react-router integration (optional subpath)
react-router is an optional peer dependency, isolated behind the
ptech-shell-dev/react-router subpath — the main entry stays react-router-free.
// App owns its own top-level Router (standalone, or mounted at a fixed host prefix):
import { ShellRouterProvider } from 'ptech-shell-dev/react-router';
<ShellRouterProvider basename="/audit-tool">
<App />
</ShellRouterProvider>;
// Creates <BrowserRouter basename>, wires the shell NavigationService, and registers it.
// Plain <Link to="/settings"> and useNavigate() become host-prefix-correct for free.// Remote nested inside a Router the host already owns — do NOT nest a second Router:
import { ShellRouterBridge } from 'ptech-shell-dev/react-router';
<>
<ShellRouterBridge adapter={adapter} basename={hostBasename} />
<App />
</>;For imperative navigation / link generation from anywhere (including non-router code), use the sdk hooks:
import { useNavigation, useNavigationSnapshot } from 'ptech-shell-sdk';
const nav = useNavigation();
const snapshot = useNavigationSnapshot(); // location.pathname is always app-relative
nav.createHref('/settings'); // '/audit-tool/settings' when embedded, '/settings' standaloneTo rehearse a host prefix while running standalone, seed initStandaloneServices:
initStandaloneServices({ navigationBasename: '/audit-tool' });Module Federation note
ptech-preset shares react-router across remotes. Each adopting remote mounts its own
Router, so react-router does not need to be a true singleton for correctness. Keep the
default shared singleton when all consumers are on the same major; add
mf.shared: { 'react-router': { singleton: false } } in a remote's rsbuild config only if
its react-router major diverges from the rest.
Build and Test
npm run build -w ptech-shell-dev
npm run test -w ptech-shell-devnpm run test -w ptech-shell-dev builds the package and runs node --test tests/*.test.mjs.
