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

@scayle/storefront

v0.1.0-alpha.1

Published

SCAYLE Storefront Runtime for Inertia based storefront applications

Readme

@scayle/storefront

V3 runtime framework for SCAYLE Storefront Applications.

Package entrypoints

| Subpath | Use | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | @scayle/storefront | Server runtime values: middleware factories (createRedirectMiddleware), routers, Inertia, Session, config, ShopConfigService, server utilities (generateKey, HttpStatusCode, price helpers, renderErrorPage, createStorefrontErrorHandler), shop switcher URL helpers (getCountryUrl, getAvailableCountries, getAvailableLanguages), and static asset helpers. | | @scayle/storefront/config | defineStorefrontConfig only. Use this in storefront.config.ts so the module graph does not pull virtual:storefront-config before your config file finishes loading. | | @scayle/storefront/client | Client-safe auth helpers (clearCheckoutAuthStorage, CHECKOUT_AUTH_STORAGE_KEYS, getCheckoutAuthApi), browser oRPC setup (createBrowserOrpcClient), and getCurrentCountryFromTimezone. | | @scayle/storefront/shared | createLogger, link or canonical URL helpers, isomorphic listing helpers (buildPagination, filter query utils, resolveSorting), currency/percentage/reduction formatting (formatCurrency, formatPercentage, getReduction), country redirect helpers (flattenRedirectShops, getRedirectShopsForRegion, evaluateShopRedirectSuggestion), and CHECKOUT_DEFERRED_PROP_KEYS. Use this in client code. | | @scayle/storefront/types | Type-only re-exports: StorefrontContext, StorefrontVariables, config and country types, country detection types (AvailableCountry, AvailableLanguage, RedirectShop, StorefrontCountryWithUrl), Inertia contracts, SEO types, session types, GetReductionInput (the parameter type of getReduction and of the formatReduction mapper hooks), and related API types. |

Deferred prop resolver failures

Inertia partial reloads in @scayle/storefront resolve every deferred prop with Promise.allSettled, so one rejected resolver does not poison sibling props in the same group. Each rejection is logged once with inertia.prop.name, the failed key is omitted from the response, and the Inertia client keeps the <Deferred> fallback visible for that key. Full page renders and full Inertia navigations still fail fast when a prop resolver throws.

This is a safety net, not a recovery mechanism. Resolvers for non-critical props should wrap their work in try/catch and return a sensible fallback (an empty basket, an empty recommendations list) so the UI degrades gracefully instead of staying on a loading skeleton:

basket: Inertia.defer(async () => {
  try {
    return await basketService.fetchBasket(...)
  } catch (err) {
    log.error({ message: 'basket fetch failed', err })
    return basketService.emptyBasket()
  }
}, { group: 'user-data' })

For critical props (the ones whose absence should fail the page render), let the resolver throw and the framework will propagate the error.

Server utilities (@scayle/storefront)

Pure helpers consumed by boilerplate and feature-package server code. Tree-shakable; safe to import in any file under src/server/**.

import {
  generateKey,
  calculateOriginalFromReductions,
  hasCampaignReductionFromStrikeThrough,
} from '@scayle/storefront'

| Export | Purpose | | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | generateKey | Stable basket/wishlist cache key derived from shop, session, and (optional) user identifiers. | | calculateOriginalFromReductions | Sums appliedReductions on top of withTax to recover the pre-reduction price (cents). | | hasCampaignReductionFromStrikeThrough | Predicate over a strike-through ladder; true when at least one entry has category === 'campaign'. | | renderErrorPage | Renders the boilerplate Error page for an uncaught request error. In dev, attaches a Youch technical-details report for 5xx; never leaks the cause or stack to the client in production. | | createStorefrontErrorHandler | Builds the Hono onError handler tenants wire up via app.onError(...). Logs uncaught errors as one structured entry, passes HTTPExceptions through, and renders the Error page. |

Product helpers (mapSellableTimeframe, SellableTimeframe, getPrimaryCategory, low-stock and attribute utilities) are exported directly from @scayle/storefront (values from the package root, types from @scayle/storefront/types).

Redirect middleware

Register after the storefront runtime middleware so ctx.get('storefront') is available:

import { createRedirectMiddleware } from '@scayle/storefront'

app.use(createRedirectMiddleware())

| Export | Purpose | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | createRedirectMiddleware | Hono middleware that runs after downstream handlers. On 404 only, normalizes the request URL and looks up panel-managed redirects via SAPI POST /v1/redirects. Responds with the configured status and Location when a match is found. Caches hits and misses for five minutes. Preserves the original 404 if lookup fails. |

Optional queryParamAllowlist controls which query params participate in the lookup URL. Params not in the list are stripped from the lookup but forwarded to the redirect target.

Server-side cache (CacheService)

The runtime exposes ctx.get('storefront').cache as a CacheService backed by the shared unstorage instance. See the boilerplate Storage and Caching guide for wiring and examples.

In production SSR bundles, import.meta.env.SCAYLE_BUILD_ID is injected by @scayle/storefront-build and is included in every physical cache key after the cache prefix and before the shop id. After a deployment the build id changes, so lookups no longer hit KV entries from the previous build. That avoids returning stale data when mapper output or serialization changes. The first request for a logical key after a deploy misses and repopulates the cache (for example via getOrSet). Entries written under an older build id are not deleted immediately. They remain until their TTL expires, then drop off on their own.

In development, SCAYLE_BUILD_ID is usually unset, so keys match the historical shape cache:{shopId}:{key}.

Logger

Logging uses tslog behind @scayle/storefront/shared. The root logger name is storefront. Use createLogger with domain-based namespaces, for example auth, search, or checkout. Do not repeat a storefront prefix in your namespace string.

Imports

import { createLogger } from '@scayle/storefront/shared'

Namespace usage

Use createLogger with stable, domain-oriented names so logs stay easy to query across the app. Namespace depth is limited to 2 segments in the final logger name. Since tslog already uses storefront as root, pass only one segment: domain.

const log = createLogger('redirects')

log.debug({ message: 'Cache miss', path })

Example:

const log = createLogger('search')

Do not add extra segments with getSubLogger, because it would exceed storefront:domain.

Log payload shape (JSON mode)

tslog merges a log call into a JSON object. From the lifecycle docs: if there is only one argument and it is an object, it merges with the default log object. If there is more than one argument, they become "0", "1", and so on, which is awkward for Datadog and similar tools.

Convention: pass one object per call. Put the human-readable line in message, errors in err, and extra context as sibling fields (do not interpolate identifiers into message when you want them filterable).

log.error({ message: 'Redirect lookup failed', err: error })
log.error({ message: 'Failed to fetch navigation', err: error, referenceKey })
log.error({ message: 'Failed to warm order summary cache', err: error })
log.error({ message: 'Failed to invalidate order summary cache', err: error })

The custom JSON transport overrides tslog's default to match the OTel log collector contract used across the storefront fleet. Each line carries:

  • timestamp — ISO 8601 string from the log entry's date.
  • severity — lowercase severity text (debug, info, warn, error).
  • logger — colon-joined namespace (for example storefront:redirects), omitted when empty.
  • All other payload fields keep their position. tslog's _logMeta envelope is dropped.

Error instances are serialized as { name, message, stack, cause, ...ownProps } so stack traces survive the wire (the default JSON.stringify returns {} because Error properties are non-enumerable). cause chains are walked recursively; self-referential or mutually referential causes short-circuit with [Circular].

Edge cases the replacer handles so structured payloads do not throw on serialization:

  • bigint becomes its decimal string (7n"7").
  • undefined becomes "[undefined]".
  • True cycles in plain objects become "[Circular]". Shared (non-cyclic) references are preserved on every visit, so logging the same object twice in one payload still serializes both copies.

Environment variables

Configuration is read from Node process.env and, when the app exposes them, from Vite import.meta.env (for example with envPrefix: 'STOREFRONT_' so the same keys work in the browser).

| Variable | Purpose | Values | | ---------------------------- | -------------------------------- | -------------------------------------------------------------------------------------- | | STOREFRONT_LOG_FORMAT | tslog output shape | Optional. pretty for readable lines. Otherwise json (default). | | STOREFRONT_LOG_LEVEL | Minimum log level | Optional. debug, info, warn, or error. Invalid or unset defaults to debug. | | STOREFRONT_LOG_REDACT_KEYS | Extra keys to redact in payloads | Optional. Comma-separated list of key names, merged with the default denylist. |

Examples:

STOREFRONT_LOG_FORMAT=json
STOREFRONT_LOG_LEVEL=info
STOREFRONT_LOG_REDACT_KEYS=idToken,customSecret

In a Vite app, expose the same STOREFRONT_* keys on import.meta.env if you want client-side reads (for example using Vite envPrefix: 'STOREFRONT_'). See readEnvString in src/logger/tslog.ts.

Sensitive data redaction

The root logger is configured with tslog's mask.keys, so values attached under known sensitive keys are replaced with [REDACTED] before output.

Default keys (see DEFAULT_REDACT_KEYS in src/logger/tslog.ts):

password, token, secret, jwt, code, authorization, cookie,
apiKey, apiToken, clientSecret, accessToken, refreshToken,
checkoutJwt, sessionId, access_token, refresh_token, setCookie

Extend the list per deployment via STOREFRONT_LOG_REDACT_KEYS (comma-separated). Entries merge with the defaults, duplicates are ignored.

For redaction to actually kick in, pass log data as a structured object, not a pre-stringified JSON blob. tslog walks the object and masks matching keys, it cannot reach into a string.

log.error({ message: 'onCheckoutError', userId, checkoutJwt, payload })
log.error({
  message: 'onCheckoutError',
  detail: JSON.stringify({ checkoutJwt }),
})

The first call logs checkoutJwt: "[REDACTED]". The second leaks the token inside detail.

Redaction applies to the OpenTelemetry log pipeline as well: TslogLogRecordExporter forwards each record through the same root logger, so logRecord.attributes and logRecord.body are masked on the way out.

Observability

Render-pipeline span shape

A full SSR render produces this span tree:

http.server.request                 [instrumentation-http, entry]
└─ Internal: GET /<route> [ssr]     [@scayle/opentelemetry middleware, route-named]
   ├─ inertia_resolve_prop × N      [@scayle/storefront]
   │  └─ http.client.request        [instrumentation-undici]
   │
   ├─ inertia_render                [@scayle/storefront]
   │  └─ vue_ssr_render             [@scayle/opentelemetry helper, via boilerplate ssr.ts]
   │
   └─ inertia_compose_html          [@scayle/storefront]

inertia_compose_html is a sibling of inertia_render because the work runs after serverRenderer.render() returns. Sequential, not nested.

Span attribute reference

| Span | Attributes | | ------------------------------------------------------ | -------------------------------------------------------------------------------------------- | | inertia_resolve_prop | inertia.prop.kind, inertia.prop.name | | inertia_render | inertia.component | | inertia_compose_html | inertia.html_bytes | | vue_ssr_render (helper from @scayle/opentelemetry) | vue.html_bytes | | http.client.request (undici) | http.request.body.size, http.response.body.size, http.response.header.content_encoding |

*_bytes attributes are read from already-existing strings (rendered HTML, composed page, wire headers). No extra serialization.

Boilerplate traceSsrRender integration

The vue_ssr_render span is emitted by the boilerplate's SSR entry, not by this package. The boilerplate's src/client/ssr.ts wraps Vue's renderToString with traceSsrRender from @scayle/opentelemetry:

import { renderToString } from '@vue/server-renderer'
import { traceSsrRender } from '@scayle/opentelemetry'

createInertiaApp({
  render: (app) => traceSsrRender(() => renderToString(app)),
  // ...
})

The helper lives in @scayle/opentelemetry so this package does not gain a vue dependency.