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

@ryadavwebdev/core

v0.2.0

Published

Shared services, interceptors, guards, theming and models for the portfolio ecosystem.

Readme

@ryadavwebdev/core

Cross-app Angular services shared across the portfolio ecosystem (portfolio, folio, design-system): auth client, HTTP interceptors, route guards, theming and the shared models.

Cross-app only — anything that knows about a specific domain ("forms", "boards") belongs in the app, not here. Everything public is exported from src/public-api.ts; nothing is deep-imported.

Angular 22, standalone, signal-first, zoneless-safe.


1. Install

npm i @ryadavwebdev/core

Peer dependencies (you almost certainly have them already): @angular/common, @angular/core, @angular/router, rxjs.

2. Wire it up

One call in app.config.ts supplies the API base URL, and the interceptors are registered on provideHttpClient:

import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideCore, authInterceptor, errorInterceptor } from '@ryadavwebdev/core';
import { environment } from '../environments/environment';

export const appConfig: ApplicationConfig = {
  providers: [
    provideCore({ apiBaseUrl: environment.apiBaseUrl }),
    provideHttpClient(withInterceptors([errorInterceptor, authInterceptor])),
  ],
};

The interceptor order is not cosmetic. errorInterceptor must come first, so it sits outermost and is the last to see a failure. Reverse them and authInterceptor never sees the raw HttpErrorResponse behind a 401 — it gets an already-normalised ApiError and the silent token refresh stops working.

provideCore options:

| Option | Required | Default | Meaning | | --------------- | -------- | --------- | --------------------------------------------------------------------- | | apiBaseUrl | yes | — | API root, e.g. http://localhost:3000/api. Trailing / stripped. | | storagePrefix | no | 'folio' | Namespaces the localStorage keys so apps on one origin don't clash. |

3. Auth

AuthService holds the session as signals and returns observables from the HTTP calls, because callers compose them (switchMap, retry, cancel).

const auth = inject(AuthService);

auth.currentUser(); // Signal<User | null>
auth.isAuthenticated(); // Signal<boolean>
auth.roles(); // Signal<readonly string[]>
auth.hasRole('admin');

auth.login({ email, password }).subscribe(/* … */);
auth.register({ name, email, password });
auth.refresh();
auth.logout();

Two behaviours worth knowing:

  • Concurrent 401s trigger exactly one refresh. The in-flight request is shared via shareReplay, so ten parallel calls failing at once produce one /auth/refresh, not ten.
  • A rejected refresh token clears the session locally, so the app can't sit in a half-authenticated state.

TokenStorage persists the session in localStorage behind a service, so tests can swap it and a non-browser context degrades to an in-memory session rather than throwing. localStorage is also wrapped in try/catch — it throws in some privacy modes and sandboxed iframes.

Guards

{ path: 'dashboard', canActivate: [authGuard], /* … */ }
{ path: 'admin', canActivate: [authGuard, roleGuard('admin')], /* … */ }

authGuard redirects to /login with a returnUrl query param so the app can send the user back. roleGuard(...roles) is a factory and redirects to /forbidden.

4. Errors

errorInterceptor normalises every failure into an ApiError, so components and stores never branch on HttpErrorResponse internals:

class ApiError extends Error {
  readonly code: string; // 'network_error' | 'unexpected_error' | whatever the API sent
  readonly status: number; // 0 for network/CORS/offline
  readonly details?: unknown;
  get isNetworkError(): boolean; // status === 0
}

It reads the ecosystem's standard failure shape, { error: { code, message } }, and falls back to unexpected_error for anything that doesn't match.

5. Theming

The visual layer lives in @ryadavwebdev/ui/styles/tokens.css; this package owns the state. Every themed token is declared with CSS light-dark(), so flipping data-theme on <html> re-resolves all of them at once.

const theme = inject(ThemeService);

theme.theme(); // Signal<'light' | 'dark' | 'system'> — what the user chose
theme.resolved(); // Signal<'light' | 'dark'>          — what will actually paint
theme.isDark(); // Signal<boolean>
theme.toggle();
theme.set('dark');
theme.useSystem();

ThemeService tracks the OS preference live via matchMedia, and tears the listener down through DestroyRef.

For use before Angular boots, the same logic is available as plain framework-free functions: setTheme, getStoredTheme, applyStoredTheme, resolveTheme, THEME_STORAGE_KEY.

Preventing a flash of the wrong theme. setTheme sets color-scheme as an inline style as well as the data-theme attribute, and both are required. Angular's critical-CSS inliner drops :root[data-theme="dark"] rules from the inlined block — they match nothing in the served HTML, because Angular hasn't run yet — so they arrive with the deferred stylesheet, after first paint. The attribute alone therefore still flashes. See the @ryadavwebdev/ui README for the index.html snippet.

6. Public API

| Export | Kind | | ---------------------------------------------------------------------------------------------- | ------------------------ | | provideCore, CORE_CONFIG, CoreConfig | Setup | | AuthService, TokenStorage | Auth | | authInterceptor, errorInterceptor | HTTP | | authGuard, roleGuard | Routing | | ThemeService | Theming | | setTheme, getStoredTheme, applyStoredTheme, resolveTheme, THEME_STORAGE_KEY, Theme | Theming (framework-free) | | ApiError, isApiErrorBody, ApiErrorBody | Models | | AuthSession, AuthTokens, LoginRequest, RegisterRequest, User | Models |

Deep imports are not supported. The package exports map exposes the root entry point only, so @ryadavwebdev/core/lib/... fails at build time by design.

7. Versioning

The public API is a semver contract:

| Change | Bump | | ----------------------------------------------------------------------------------------------- | --------- | | Adding an export, an optional input, a new optional config field | minor | | Removing or renaming an export; changing a type incompatibly; making an optional thing required | major | | Bug fix, docs, internal refactor with no API change | patch |

While the package is 0.x, a breaking change bumps the minor — that is the standard 0.x convention, and it is why the API is not yet declared stable.

Bump projects/core/package.json before every publish. npm versions are immutable: a published version can never be re-uploaded, only deprecated.

8. Build, test, publish

ng build core          # → dist/core
ng test core --watch=false
cd dist/core && npm publish --access public

Publish from dist/core, never from projects/core — the built folder is the real package (it has the FESM bundle, the flattened .d.ts and the generated exports map).