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

@budiauktioner/buildi-primitives

v0.2.1

Published

Swedish, EU and international value-object primitives — validation, normalization, masking, and text scanning. TypeScript port of Buildi.Primitives (.NET).

Readme

@budiauktioner/buildi-primitives

TypeScript primitives for Swedish, EU, and international value-object validation, normalization, masking, and text scanning.

This is the TypeScript port of the .NET package Buildi.Primitives, whose source remains the behavioural specification for both languages.

Status: early release, versioned from 0.1.0. All thirteen .NET namespaces are ported and track the .NET package closely. See PARITY.md for the exact coverage and the handful of intentional divergences.

Install

pnpm add @budiauktioner/buildi-primitives
# or
npm install @budiauktioner/buildi-primitives
# or
yarn add @budiauktioner/buildi-primitives
# or
bun add @budiauktioner/buildi-primitives

Quick start

import { SwedishOrganizationNumber } from "@budiauktioner/buildi-primitives/organization";

SwedishOrganizationNumber.isValid("559246-0421"); // true
SwedishOrganizationNumber.isValid("559246-0420"); // false — bad check digit

// Static helpers accept messy input and return null when it is not valid.
SwedishOrganizationNumber.format("5592460421"); // "559246-0421"
SwedishOrganizationNumber.normalize("559246-0421"); // "165592460421"
SwedishOrganizationNumber.format("nonsense"); // null

// Or parse once and keep the value object around.
const org = SwedishOrganizationNumber.parse("5592460421");
org.toString(); // "559246-0421"  — display form
org.toNormalizedString(); // "165592460421" — storage form
org.isPerson; // false — this is a legal entity, not a sole trader

format() produces the display form and normalize() the canonical form for storage and comparison. For roughly half the types these are the same string; for the rest they deliberately differ, as above. Store the normalized value and render the formatted one.

API conventions

Every value-object type follows the same shape:

class TypeName {
  static readonly typeInfo: PrimitiveTypeInfo;

  static tryParse(input: string | null | undefined): TypeName | null;
  static parse(input: string): TypeName; // throws on invalid input
  static isValid(input: string | null | undefined): boolean;
  static format(
    input: string | null | undefined,
    options?: { fallbackToTrimmedInputWhenInvalid?: boolean },
  ): string | null;
  static normalize(
    input: string | null | undefined,
    options?: { fallbackToTrimmedInputWhenInvalid?: boolean },
  ): string | null;
  static isNormalized(input: string | null | undefined): boolean;

  toNormalizedString(): string;
  toString(): string;

  equals(other: TypeName | null | undefined): boolean;
  compareTo(other: TypeName | null | undefined): number;
}

Constructors are private: a value object can only come from parse or tryParse, so an instance is always valid. Every static helper accepts string | null | undefined, so unvalidated input can be passed straight in.

Pass { fallbackToTrimmedInputWhenInvalid: true } to format or normalize to get the trimmed original input back instead of null when the input is not valid — useful when rendering user-entered values you do not want to drop.

Validation

Types with more than one meaningful failure mode add validate(), which reports why the input was rejected rather than just that it was:

import { Iban } from "@budiauktioner/buildi-primitives/banking";

const result = Iban.validate("SE45 5000 0000 0583 9825 7460");

result.isValid; // false
result.issues.map((i) => i.reason); // ["InvalidCheckDigit"]
result.issues[0].description; // "Ogiltiga IBAN-kontrollsiffror."

reason is a stable ValidationErrorReason enum value safe to branch on. description is human-readable and follows the display language described under Defaults and display language; each issue also exposes englishDescription and localizedDescription directly.

Masking

Masking is done by module-level maskX functions, exported from the same subpath as the type they mask:

import { Iban, maskIban } from "@budiauktioner/buildi-primitives/banking";
import { EmailAddress, maskEmailAddress } from "@budiauktioner/buildi-primitives/web";
import { PhoneNumber, maskPhoneNumber } from "@budiauktioner/buildi-primitives/contact";

maskIban(Iban.parse("SE4550000000058398257466")); // "SE45 **** **** **** **** ****"
maskEmailAddress(EmailAddress.parse("[email protected]")); // "i***@example.com"
maskPhoneNumber(PhoneNumber.parse("070-174 06 33")); // "+46*****0633"

Some take options, for example maskEmailAddress(email, { maskDomain: true }).

Text scanning

Types that can plausibly appear in prose expose findCandidatesInText, and defaultTextScanner runs all of them at once:

import {
  TextCandidateCategory,
  defaultTextScanner,
} from "@budiauktioner/buildi-primitives/text-scanning";

const text = "Hör av dig till [email protected] eller ring 070-174 06 33.";
const result = defaultTextScanner.scan(text);

result.emails.map((c) => c.normalizedForm); // ["[email protected]"]
result.phoneNumbers.map((c) => c.normalizedForm); // ["0046701740633"]

result.maskAll(text); // "Hör av dig till i***@example.com eller ring +46*****0633."
result.redactAll(text); // "Hör av dig till [REDACTED] eller ring [REDACTED]."

TextScanResult has a typed accessor per scannable type (emails, ibans, organizationNumbers, and so on). Every candidate carries its span in the source text, its normalized, formatted and masked forms, and a TextMatchConfidence. Types with a checksum report High; bare digit sequences report Low. Narrow a scan with scan(text, { minimumConfidence, includeCategories, excludeCategories }).

Version 0.2.0 expands defaultTextScanner with the Measurement, Vehicle, and Product scanners, so an unfiltered scan may now find or mask more values than 0.1.x. To preserve the previous category scope during migration, exclude the new categories:

const result = defaultTextScanner.scan(text, {
  excludeCategories: new Set([
    TextCandidateCategory.Measurement,
    TextCandidateCategory.Vehicle,
    TextCandidateCategory.Product,
  ]),
});

Scanning is the one part of the package with import-time behaviour: importing @budiauktioner/buildi-primitives/text-scanning (or the root entry) registers all built-in scanners on defaultTextScanner. Importing only a value-object subpath deliberately does not. Construct your own TextScanner if you want an isolated registry.

Defaults and display language

PrimitivesDefaults holds the process-wide culture, UI culture, default country and default calling code. It affects display names, localized validation messages, and how ambiguous input such as a national phone number is interpreted.

import { PrimitivesDefaults, Country } from "@budiauktioner/buildi-primitives";

PrimitivesDefaults.culture; // "sv-SE"
PrimitivesDefaults.countryAlpha2Code; // "SE"

Country.parse("SE").displayName; // "Sverige"

PrimitivesDefaults.uiCulture = "en-US";
Country.parse("SE").displayName; // "Sweden"

PrimitivesDefaults.reset(); // back to the defaults

useLocalizedDisplayNames is a read-only getter derived from uiCulture — it is true when uiCulture is Swedish. Set uiCulture to change the display language; assigning to useLocalizedDisplayNames throws a TypeError. Types that expose a displayName also expose englishName and localizedName, so a mixed-locale app can bypass the global entirely.

Sample data

Valid, publicly known values for tests, fixtures and documentation. Everything here is a real parsed value object, so it stays valid as the rules evolve.

import { OrganizationSampleData } from "@budiauktioner/buildi-primitives/sample-data";

OrganizationSampleData.OrganizationNumber.all[0].toString(); // "559246-0421"

Only publicly known organizations and reserved test values are used, such as RFC 2606 domains for email and the standard international test IBANs.

Subpath exports and bundle size

The root entry re-exports everything for convenience. Import from a namespace subpath instead when bundle size matters. These figures bundle and minify the complete consumers shown below with esbuild for an ES2022 browser target:

// 439,333 bytes minified
import { SwedishOrganizationNumber } from "@budiauktioner/buildi-primitives";

console.log(SwedishOrganizationNumber.parse("559246-0421").toNormalizedString());
// 22,024 bytes minified
import { SwedishOrganizationNumber } from "@budiauktioner/buildi-primitives/organization";

console.log(SwedishOrganizationNumber.parse("559246-0421").toNormalizedString());
// 15,502 bytes minified
import { Length } from "@budiauktioner/buildi-primitives/measurement";

console.log(Length.fromMeters(1).toNormalizedString());

The root named import remains substantially larger because importing the root entry registers all built-in scanners as an import-time side effect. Namespace imports avoid that registration graph, and tree-shaking drops unrelated Measurement dimensions from the Length consumer.

Available subpaths: contact, person, organization, geography, web, finance, property, banking, measurement, vehicle, product, validation, text-scanning, and sample-data.

import { SwedishPersonalIdentityNumber } from "@budiauktioner/buildi-primitives/person";
import { Country, Continent, Language } from "@budiauktioner/buildi-primitives/geography";
import { AddressCity, PhoneNumber, SwedishAddress } from "@budiauktioner/buildi-primitives/contact";
import { Currency, MoneyAmount, ExchangeRates } from "@budiauktioner/buildi-primitives/finance";
import {
  Length,
  Weight,
  Temperature,
  Percentage,
} from "@budiauktioner/buildi-primitives/measurement";
import {
  FuelType,
  TireDimension,
  VehicleIdentificationNumber,
} from "@budiauktioner/buildi-primitives/vehicle";
import { Gtin13, ScreenSize, ClothingSize } from "@budiauktioner/buildi-primitives/product";
import { ValidationErrorReason } from "@budiauktioner/buildi-primitives/validation";

Use the root entry when you want the batteries-included behaviour, in particular a defaultTextScanner that already knows every scannable type.

Runtime targets

  • Node.js 20.10 or newer (see .nvmrc).
  • Bun 1.1 or newer.
  • Modern browsers (current Chromium, Firefox, Safari).
  • No Node-only runtime APIs and no runtime dependencies; ES2022 plus standard Intl only.
  • Ships ESM and CJS builds with type definitions for both.

Development

All commands run from the repository root:

pnpm install
pnpm typecheck
pnpm lint
pnpm test
pnpm build

See AGENTS.md for conventions and the per-type pattern, PARITY.md for the current state of .NET parity, and CHANGELOG.md for release notes.

License

MIT. See LICENSE and THIRD_PARTY_NOTICES.md.