@polargold/pg-frontend-core
    Preparing search index...

    @polargold/pg-frontend-core

    pg-frontend-core

    Framework-agnostic utility classes, error types, and an Axios client factory shared across polargold's frontend apps.



    Beschreibung

    Prerequisites

    Getting started

    Useful commands

    Useful links

    Advanced topics



    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.


    • See NVM for installation.

    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:

    • Static utility classes (src/utils)
    • A typed error hierarchy (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.

    Replaces 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 rejected 0, false, and "". LocalStorage.write() only rejects undefined — 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();

    Replaces 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"

    Replaces 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"

    Replaces 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);

    Replaces 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"]

    Replaces the identical copies in luv-autoq-frontend and bws-checkout-service-spa.

    import { removeWhiteSpace } from "@polargold/pg-frontend-core";

    removeWhiteSpace(" hello world "); // "helloworld"

    Replaces 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 }

    New — 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.

    Replaces 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();

    Replaces 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.

    Unifies 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..."

    New (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 });
    }

    Unifies 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: setUrlParam only removes a param for undefined/null/""/an empty array. luv-autoq-frontend's UrlQueryHelper used a bare !value check, which would also have deleted a legitimate 0 (e.g. resetting a page-number filter to the first page).

    Replaces 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 }

    Replaces 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");

    Replaces 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

    Replaces 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));

    Replaces 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
    });

    Ported 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

    Ported from data-sheet-portal.

    import { convertBytesToMb } from "@polargold/pg-frontend-core";

    convertBytesToMb(2_621_440); // "2.50"

    Generalizes 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" });

    Ported 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