Framework-agnostic utility classes, error types, and an Axios client factory shared across polargold's frontend apps.
Framework-agnostic (no Vue) TypeScript utility library for polargold frontend apps: date/currency formatting, a
namespaced localStorage wrapper, an Axios client factory, an OIDC auth wrapper, Sentry config resolution, and
typed error classes.
Extracted from the near-duplicate composables/utilities found across luv-ui-tools, bws-ui-tools, and four app
repos (luv-autoq-frontend, ama-deep, data-sheet-portal, bws-checkout-service-spa) during the September 2026
frontend tooling audit. Vue-specific composables built on top of this package live in the sibling
@polargold/pg-frontend-vue package, not here — see What belongs here.
nvm use && npm install
# Configure access to the polargold registry (once, globally). Token in Bitwarden under "[NPM Registry] pg-admin"
npm config set -- //npm.white-lan.de/:_authToken=${token}
cd your-project/
echo "@polargold:registry=https://npm.white-lan.de" >> .npmrc
npm install -S @polargold/pg-frontend-core
Peer dependencies (installed automatically by npm ≥7, but listed explicitly since this is a library):
{
"axios": "^1.17.0",
"dayjs": "^1.11.21"
}
One more is an optional peer dependency, only needed if you actually call the method that uses it — install it yourself if so, npm won't pull it in automatically:
{
"@sentry/browser": "^10.69.0"
}
It's needed only for InitializeSentry.initialize(), not resolveConfig() (the documented Vue path — see
InitializeSentry) — .initialize() loads it via a dynamic import() at call time, so
resolveConfig()-only usage never needs it installed at all.
oidc-client-ts (used by AuthService) is a regular hard dependency instead, even though only AuthService
uses it — unlike @sentry/browser, its import in AuthService is a static top-level one (the whole class
needs UserManager unconditionally), and this package ships as a single bundled file, so a static import
gets evaluated the moment anything is imported from the package, not just when AuthService is used. Making
it an optional peer without also making it lazy would break every other export for any consumer that doesn't
happen to have oidc-client-ts installed — found the hard way in 0.3.0, fixed in 0.3.1.
npm run lint
npm run lint:fix
npm run vitest:run
npm run vitest:coverage
npm run docs:build
npm run docs:serve
npm run vite:build
| Service | URL |
|---|---|
| Package registry | Package registry |
| TypeDoc | pg-frontend-core.polargold.dev |
Framework-light, component-agnostic code only:
src/utils)src/errors)Anything that needs Vue (composables, anything using ref/computed/useRouter) belongs in
@polargold/pg-frontend-vue, which depends on this package. Anything specific to one brand (colors, Tailwind
config, useSeminarType, useHomeLink) belongs in that brand's own *-ui-library package, not here.
LocalStorageReplaces the StorageHelper class duplicated in luv-autoq-frontend, ama-deep, data-sheet-portal, and
bws-checkout-service-spa.
Behavior change: the original
StorageHelper.write()rejected every falsy value (if (!value) throw), which incorrectly rejected0,false, and"".LocalStorage.write()only rejectsundefined— check any code that relied on the old (buggy) rejection of falsy values when migrating.
import { LocalStorage } from "@polargold/pg-frontend-core";
import { version } from "../package.json";
// Most keys: versioned, so stale state doesn't carry across a deploy.
const filterStore = new LocalStorage("productSearchFilterParamsStore", {
mode: import.meta.env.MODE,
version,
});
// Access/session tokens: omit `version` so a deploy doesn't force everyone to
// sign in again - matches luv-autoq-frontend's `new StorageHelper(StorageName.AccessToken, false)`.
const accessTokenStore = new LocalStorage("accessToken", { mode: import.meta.env.MODE });
accessTokenStore.write("some-token");
accessTokenStore.read<string>(); // "some-token"
accessTokenStore.remove();
DateFormatterReplaces luv-ui-tools'/bws-ui-tools' DateFormatter (unchanged) — used internally by useDateTime in
@polargold/pg-frontend-vue, or directly for non-Vue formatting needs.
import { DateFormatter } from "@polargold/pg-frontend-core";
DateFormatter.date(new Date()); // "04.09.26"
DateFormatter.internationalDate(new Date()); // "2026-09-04"
CurrencyFormatterReplaces luv-ui-tools'/bws-ui-tools' CurrencyFormatter and bws-checkout-service-spa's standalone
currencyHelper (constructor now takes optional currency/locale params, matching what currencyHelper
already exposed).
import { CurrencyFormatter } from "@polargold/pg-frontend-core";
new CurrencyFormatter().formatCurrency(19.9); // "19,90 €"
new CurrencyFormatter("USD", "en-US").formatCurrency(19.9); // "$19.90"
ColorGradientReplaces luv-ui-tools'/bws-ui-tools' ColorGradient (brand-neutral ColorGradientDirectionName instead of a
Luv/Bws-prefixed enum).
import { ColorGradient, ColorGradientDirectionName } from "@polargold/pg-frontend-core";
new ColorGradient().generate("#FFFFFF", "#000000", ColorGradientDirectionName.Vertical);
TypeCastUtilsReplaces the copies in luv-autoq-frontend and bws-checkout-service-spa. Ships with the regex fix
luv-autoq-frontend already had (see the class's own doc comment) — bws-checkout-service-spa's copy was still
affected by the bug this fixes.
import { TypeCastUtils } from "@polargold/pg-frontend-core";
new TypeCastUtils("42").typedValue; // 42
new TypeCastUtils("true").typedValue; // true
new TypeCastUtils("a,b,c").stringToArray; // ["a", "b", "c"]
removeWhiteSpaceReplaces the identical copies in luv-autoq-frontend and bws-checkout-service-spa.
import { removeWhiteSpace } from "@polargold/pg-frontend-core";
removeWhiteSpace(" hello world "); // "helloworld"
cleanseObjectReplaces ama-deep's hand-rolled cleanseObject and bws-checkout-service-spa's removeEmptyProperties.
import { cleanseObject } from "@polargold/pg-frontend-core";
cleanseObject({ name: "x", note: "", count: 0, tags: [] }); // { name: "x", count: 0 }
createApiClientNew — generalizes the near-identical axios.create() + interceptor blocks in luv-autoq-frontend's, ama-deep's
and data-sheet-portal's api/modules/base files.
import { createApiClient } from "@polargold/pg-frontend-core";
import { accessTokenStore, appLanguageStore } from "./storage";
import router from "@/router";
import { ROUTE } from "@/router/routeDefinitions";
export const apiClient = createApiClient({
baseURL: import.meta.env.VITE_API_URL,
getAccessToken: () => accessTokenStore.read<string>(),
getAcceptLanguage: () => appLanguageStore.read<string>() ?? "en",
onForbidden: () => router.push({ name: ROUTE.PUBLIC_PAGE_NOT_ALLOWED.NAME }),
onServerError: () => router.push({ name: ROUTE.PUBLIC_ERROR_PAGE.NAME }),
});
A request can opt out of the forbidden handler with { suppressForbiddenRedirect: true } in its own Axios config
— same escape hatch data-sheet-portal already relies on for downloads that handle a 403 locally.
AuthServiceReplaces the copies in ama-deep and data-sheet-portal. Configuration is passed in explicitly (see the class's
own doc comment for why), instead of being read from import.meta.env inside the class.
import { AuthService } from "@polargold/pg-frontend-core";
const authService = new AuthService({
authority: import.meta.env.VITE_OPEN_ID_REALM_URL,
clientId: import.meta.env.VITE_OPEN_ID_CLIENT_ID,
redirectUri: import.meta.env.VITE_OPEN_ID_REDIRECT_URL,
postLogoutRedirectUri: import.meta.env.VITE_OPEN_ID_POST_LOGOUT_REDIRECT_URL,
});
await authService.signInRedirect();
InitializeSentryReplaces the copies in luv-autoq-frontend, ama-deep and data-sheet-portal — but narrower on purpose. The
source copies call @sentry/vue's Sentry.init({ app, ... }) with browserTracingIntegration({ router }), which
needs a Vue App/Router — genuinely Vue-specific, so it doesn't belong in this framework-agnostic package. Here,
InitializeSentry only resolves configuration (DSN gating, sample rates); a Vue app calls resolveConfig() and
passes the result into @sentry/vue's own Sentry.init together with its own integrations
(@polargold/pg-frontend-vue is expected to wrap this pattern).
import { InitializeSentry } from "@polargold/pg-frontend-core";
import * as Sentry from "@sentry/vue";
import { version } from "../package.json";
const sentry = new InitializeSentry({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.VITE_SENTRY_ENVIRONMENT,
release: version,
validAppDomains: ["https://app.example.polargold.dev"],
});
const config = sentry.resolveConfig();
if (config) {
Sentry.init({
app,
...config,
integrations: [Sentry.browserTracingIntegration({ router }), Sentry.replayIntegration()],
});
}
A non-Vue consumer can instead call await sentry.initialize() directly for plain @sentry/browser error
capture with no tracing integration. @sentry/browser is only ever loaded (dynamically) if this method is
actually called — importing InitializeSentry for resolveConfig() alone never pulls it in.
truncateStringUnifies luv-autoq-frontend's truncateWordByLetterCount and dspace-relaunch's getShortenedString — same
character-count-based truncation, found duplicated during a follow-up scan of dspace-relaunch.
import { truncateString } from "@polargold/pg-frontend-core";
truncateString("A very long product description", 20); // "A very long product..."
isStorageAvailableNew (ported from an unused dspace-relaunch helper) — feature-detects whether localStorage/sessionStorage
actually works, e.g. in private-browsing modes that disable storage.
import { isStorageAvailable, LocalStorage } from "@polargold/pg-frontend-core";
if (isStorageAvailable("localStorage")) {
const store = new LocalStorage("accessToken", { mode: import.meta.env.MODE });
}
getUrlParam / getTypedUrlParam / setUrlParam / removeUrlParam / clearUrlParamsUnifies dspace-relaunch's getUrlParam/updateUrlParam and luv-autoq-frontend's
UrlQueryHelper/UrlQueryHelperGlobal class — sync a value to the URL query string via history.pushState,
without a page reload.
import { getTypedUrlParam, setUrlParam, clearUrlParams } from "@polargold/pg-frontend-core";
setUrlParam("page", 2); // ?page=2
getTypedUrlParam<number>("page"); // 2 (typed via TypeCastUtils, not the raw "2" string)
clearUrlParams(); // strips every query param
Behavior change:
setUrlParamonly removes a param forundefined/null/""/an empty array.luv-autoq-frontend'sUrlQueryHelperused a bare!valuecheck, which would also have deleted a legitimate0(e.g. resetting a page-number filter to the first page).
buildI18nMessageReplaces a copy byte-identical between ama-deep and data-sheet-portal — found in the original audit but
missed at the time. Generalized to accept the target key instead of hardcoding "$vuetify".
import { buildI18nMessage } from "@polargold/pg-frontend-core";
import deLocale from "vuetify/locale/de";
const messages = buildI18nMessage(appTranslations.de, deLocale); // { ...appTranslations.de, $vuetify: deLocale }
triggerBlobDownloadReplaces the copy in luv-autoq-frontend. The same anchor-click-and-revoke sub-routine is hand-rolled again
inside data-sheet-portal's useAuthenticatedFileDownload composable — that composable should call this
instead.
import { triggerBlobDownload } from "@polargold/pg-frontend-core";
const { data } = await apiClient.get("/documents/123", { responseType: "blob" });
triggerBlobDownload(data, "certificate.pdf");
createBroadcastChannelReplaces the copy in luv-autoq-frontend. Returns undefined instead of throwing when the BroadcastChannel
API isn't available.
import { createBroadcastChannel } from "@polargold/pg-frontend-core";
const logoutChannel = createBroadcastChannel("auth-logout");
logoutChannel?.postMessage("logged-out"); // in the tab that logged out
logoutChannel?.addEventListener("message", () => signOutLocally()); // in every other tab
convertFileToBase64 / convertFilesToBase64Replaces luv-autoq-frontend's FileHelper class — simplified to plain functions (see the function's own doc
comment for why).
import { convertFileToBase64, convertFilesToBase64 } from "@polargold/pg-frontend-core";
const base64 = await convertFileToBase64(fileInput.files[0]);
const allBase64 = await convertFilesToBase64(Array.from(fileInput.files));
safeLocalStorage / safeSessionStorageReplaces data-sheet-portal's safeLocalStorage — a full Storage-interface wrapper that silently no-ops
(instead of throwing) when the underlying storage is unavailable (SSR, private-browsing modes, a disabled-storage
policy). Complements isStorageAvailable (a boolean the caller branches on) with a drop-in object that never
needs that check.
import { safeLocalStorage, AuthService } from "@polargold/pg-frontend-core";
const authService = new AuthService({
authority: "...",
clientId: "...",
redirectUri: "...",
postLogoutRedirectUri: "...",
userStore: safeLocalStorage, // instead of the raw window.localStorage global
});
DebouncedIdBufferPorted unchanged from luv-autoq-frontend — already framework-agnostic. Batches ids enqueued within a debounce
window into a single call, so several rapid actions (e.g. "mark as read" clicks) become one API call instead of
one per click.
import { DebouncedIdBuffer } from "@polargold/pg-frontend-core";
const buffer = new DebouncedIdBuffer(
(ids) => apiClient.post("/mark-read", { ids }),
{ wait: 1500, maxWait: 4000 },
);
await buffer.enqueue(42); // resolves once the whole batch it was part of has been sent
convertBytesToMb / convertKbToMbPorted from data-sheet-portal.
import { convertBytesToMb } from "@polargold/pg-frontend-core";
convertBytesToMb(2_621_440); // "2.50"
injectGtmScript / pushDataLayerEventGeneralizes data-sheet-portal's useGTM (hardcoded two import.meta.env var names) and the generic part of
bws-checkout-service-spa's GoogleAnalyticsTracker.trackEvent — its cart-specific event-shape mapping
(trackCartEvent) stays app-local, that's e-commerce domain logic, not a shared concern.
import { injectGtmScript, pushDataLayerEvent } from "@polargold/pg-frontend-core";
injectGtmScript({ gtmId: import.meta.env.VITE_GTM_GTAG_ID, gtmUrl: import.meta.env.VITE_GTM_SCRIPT_URL });
pushDataLayerEvent("purchase", { value: 49.9, currency: "EUR" });
ApiCallError / LocalStorageErrorPorted unchanged from luv-ui-tools/bws-ui-tools. ApiCallError is thrown by API-calling wrappers such as
@polargold/pg-frontend-vue's useApi; LocalStorageError (with a typed reason: "NO_KEY" | "NO_VALUE") is
thrown by LocalStorage above.
Every symbol is re-exported through its category's own index.ts (src/utils/index.ts, src/errors/index.ts)
and through src/main.ts — nothing is auto-discovered. Follow this for anything new.
Everything pushed as a tag is linted, tested, has its TypeDoc built, and is published to the internal
package registry automatically — see bitbucket-pipelines.yml. Versioning itself is manual:
npm version [major | minor | patch | x.x.x]
| Release type | Description |
|---|---|
| Major | Breaking change to an exported class/function's public shape |
| Minor | A new exported utility |
| Patch | Bugfixes/improvements to existing utilities |
polargold GmbH, Lilienstraße 5-9 / Semperhaus C, 20095 Hamburg