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

@attrx/core

v0.1.0

Published

Semantic attribute metadata system - schema, validation, presentation, and transformations for data keys

Downloads

14

Readme

@attrx/core

Sistema de atributos semânticos para chaves de dados.

Transforme chaves JSON em metadados ricos com tipo, semântica, validação e apresentação.

Instalação

pnpm add @attrx/core @attrx/role-morphic

Por que @attrx/core?

// Sem attrx: só sei que é número
const data = { price: 99.90 };

// Com attrx: sei que é DINHEIRO em BRL
const attrs = new AttrCollection([
  { key: 'price', type: 'number', role: 'currency:brl', label: 'Preço' }
]);

O que você ganha:

  • Semântica: type + role = significado completo
  • Slots: organização hierárquica (pictures, edges, address fields)
  • 4 Pilares: cast, validate, convert, format
  • State Tracking: dirty, pristine, commit, reset (por slot)
  • Eventos: reatividade para UI
  • JSON Schema: validação automática

Quick Start

import { AttrCollection } from '@attrx/core';
import { RoleMorphic, areaRole, colorRole } from '@attrx/role-morphic';

// Setup RoleMorphic
const morph = new RoleMorphic();
morph.register('area', areaRole.toSpec());
morph.register('color', colorRole.toSpec());

// Cria collection
const attrs = new AttrCollection([
  { key: 'name', type: 'string', label: 'Nome' },
  { key: 'area', type: 'number', role: 'area:hectare' },
  { key: 'color', type: 'string', role: 'color:hex' },
], morph);

// Define valores
attrs.setValue('name', 'Fazenda Sol');
attrs.setValue('area', 100);
attrs.setValue('color', '#ff0000');

// 4 Pilares
attrs.cast('area', '150 ha');              // Parse: "150 ha" → 150
attrs.validate('area');                     // { valid: true, errors: [] }
attrs.convert('color', 'color:rgb_object'); // "#ff0000" → { r: 255, g: 0, b: 0 }
attrs.format('area');                       // "100 ha"

API

CRUD

// Criar
attrs.add({ key: 'email', type: 'string', role: 'email' });

// Ler
attrs.get('email');           // Attribute | undefined
attrs.getOrThrow('email');    // Attribute (throws se não existe)
attrs.has('email');           // boolean
attrs.getAll();               // Attribute[] (só slot '#' por padrão)
attrs.getAll({ slot: '*' });  // Attribute[] (todos os slots)
attrs.keys();                 // string[]
attrs.length;                 // number

// Atualizar
attrs.update('email', { label: 'E-mail corporativo' });

// Deletar (virtual até commit)
attrs.remove('email');
attrs.remove('fotos', { cascade: true }); // Remove filhos também

Values

// Individual
attrs.setValue('name', 'João');
attrs.getValue('name');  // 'João'

// Múltiplos
attrs.setValues({ name: 'João', age: 30 });
attrs.getValues();  // { name: 'João', age: 30 }

4 Pilares

// CAST - Normaliza input sujo
attrs.cast('area', '100 hectares');  // → 100 (number)
attrs.cast('area', '100 ha');        // → 100
attrs.tryCast('area', 'invalid');    // { ok: false, error: '...' }

// VALIDATE - Verifica regras
attrs.validate('area');              // { valid: true, errors: [] }
attrs.validate('area', -5);          // { valid: false, errors: ['...'] }
attrs.isValid('area');               // boolean
attrs.isAllValid();                  // boolean

// CONVERT - Transforma variantes
attrs.convert('color', 'color:rgb_object');     // { r: 255, g: 0, b: 0 }
attrs.convertVariant('color', 'hsl_object');    // { h: 0, s: 100, l: 50 }
attrs.getConvertibleVariants('color');          // ['color:rgb_object', ...]

// FORMAT - Apresentação
attrs.format('area');                           // "100 ha"
attrs.format('area', { verbose: true });        // "100 hectares"
attrs.format('area', { locale: 'pt-BR' });      // "1.000 ha"

State Tracking

// Status de campos
attrs.getStatus('name');     // 'pristine' | 'dirty' | 'added' | 'removed'
attrs.getState('name');      // { status, original? }

// Checks (suporta slots)
attrs.isDirty();                      // Algum campo alterado no slot '#'?
attrs.isDirty('name');                // Este campo alterado?
attrs.isDirty({ slot: 'pictures' });  // Slot pictures alterado?
attrs.isDirty({ slot: '*' });         // Qualquer slot alterado?
attrs.isPristine();                   // Todos pristine?
attrs.isClean();                      // Sem mudanças pendentes?
attrs.hasChanges({ slot: '*' });      // Mudanças em qualquer slot?

// Checks específicos
attrs.isAdded('newField');   // Foi adicionado?
attrs.isRemoved('oldField'); // Foi removido?

// Listas (suporta slots)
attrs.getDirtyKeys();                     // Alterados no slot '#'
attrs.getDirtyKeys({ slot: 'pictures' }); // Alterados no slot pictures
attrs.getDirtyKeys({ slot: '*' });        // Alterados em qualquer slot
attrs.getModifiedKeys();     // Só dirty (não added/removed)
attrs.getAddedKeys();        // Só added
attrs.getRemovedKeys();      // Só removed
attrs.getPristineKeys();     // Não alterados

Commit & Reset

// Edita
attrs.setValue('name', 'João');
attrs.add({ key: 'phone', type: 'string' });
attrs.remove('oldField');

// Commit: salva como novo baseline
attrs.commit();
// - dirty → pristine
// - added → pristine (persiste)
// - removed → deletado de verdade

// Commit por slot
attrs.commit({ slot: 'pictures' });  // Só slot pictures
attrs.commit({ slot: '*' });         // Todos os slots

// Edita novamente
attrs.setValue('name', 'Maria');

// Reset: volta ao último commit
attrs.reset();
attrs.getValue('name');  // 'João'

// Reset parcial
attrs.reset('name');                   // Só este campo
attrs.reset(['name', 'age']);          // Só estes campos
attrs.reset({ slot: 'pictures' });     // Só slot pictures

Batch Operations

// Agrupa operações (1 evento no final)
attrs.beginBatch();
attrs.setValue('a', 1);
attrs.setValue('b', 2);
attrs.setValue('c', 3);
attrs.endBatch();  // Emite batchEnd

// Ou com transaction
attrs.transaction(() => {
  attrs.setValue('a', 1);
  attrs.setValue('b', 2);
  attrs.setValue('c', 3);
});

Snapshot

// Salva estado completo
const snapshot = attrs.toSnapshot();

// Faz alterações
attrs.setValue('name', 'Outro');
attrs.add({ key: 'temp', type: 'string' });

// Restaura
attrs.restore(snapshot);

Eventos

attrs.on('valueChanged', ({ key, value, previous }) => {
  console.log(`${key}: ${previous} → ${value}`);
});

attrs.on('stateChanged', ({ key, status, previous }) => {
  console.log(`${key}: ${previous} → ${status}`);
});

attrs.on('committed', ({ keys }) => {
  console.log('Committed:', keys);
  // Salvar no backend...
});

attrs.on('reset', ({ keys }) => {
  console.log('Reset:', keys);
});

// Todos os eventos
// added, updated, removed, valueChanged, converted,
// stateChanged, committed, reset, batchStart, batchEnd

Filter & Find

attrs.filter(a => a.type === 'number');  // Attribute[]
attrs.find(a => a.key === 'name');       // Attribute | undefined
attrs.filterByType('number');            // Attribute[]
attrs.filterByRole('area');              // Attribute[]

Slots

Slots são namespaces lógicos para organizar atributos hierarquicamente:

const property = new AttrCollection([
  // Root (#) - dados principais
  { key: 'titulo', type: 'string', value: 'Apartamento Centro' },
  { key: 'preco', type: 'number', role: 'currency', value: 450000 },

  // Parent com children (aponta para slot)
  { key: 'fotos', type: 'array', children: 'pictures' },

  // Slot pictures (filho de 'fotos')
  { key: 'pictures:1', slot: 'pictures', type: 'object', value: { url: '/sala.jpg' } },
  { key: 'pictures:2', slot: 'pictures', type: 'object', value: { url: '/quarto.jpg' } },

  // Slot orphan (sem parent) - dados auxiliares
  { key: 'edges:a->b', slot: 'edges', type: 'object', value: { from: 'a', to: 'b' } },
]);

// Query por slot
property.getAll();                   // só '#' (root) - 3 items
property.getAll({ slot: '*' });      // todos - 6 items
property.getSlot('pictures');        // 2 items

// Children API
property.getChildren('fotos');       // [pictures:1, pictures:2]
property.getChildrenCount('fotos');  // 2
property.hasChildren('fotos');       // true

// Slot metadata
property.getSlots();                 // ['#', 'pictures', 'edges']
property.getSlotParent('pictures');  // 'fotos'
property.getOrphanSlots();           // ['#', 'edges']

// Collection helpers (auto-ID)
const key = property.addToSlot('pictures', {
  type: 'object',
  value: { url: '/cozinha.jpg' }
});
// key = 'pictures:pic_a7f3b2x9'

// Reorder
property.reorderSlot('pictures', ['pictures:2', 'pictures:1']);

// Move entre slots
property.moveToSlot('pictures:1', 'archived');

// Clear slot inteiro
property.clearSlot('pictures');

// Remove com cascade (deleta children)
property.remove('fotos', { cascade: true });

Convenções de key:

  • Collection items: slot:idpictures:a7f3b2
  • Structured fields: parent.fieldaddress.street

JSON Schema

import { toJsonSchema } from '@attrx/core';

const schema = toJsonSchema(attrs.getAll(), morph);
// {
//   $schema: "http://json-schema.org/draft-07/schema#",
//   type: "object",
//   properties: {
//     name: { type: "string" },
//     area: { type: "number", minimum: 0 }
//   },
//   required: ["name"]
// }

Form Workflow

// 1. Load
const attrs = new AttrCollection(dataFromApi, morph);
// Todos começam pristine

// 2. User edits
attrs.setValue('name', 'João');
attrs.setValue('age', 30);
// → dirty, eventos emitidos

// 3. Validate on blur
attrs.validate('age');
// { valid: true, errors: [] }

// 4. Check before submit
if (!attrs.isAllValid()) {
  // Show errors
  return;
}

// 5. Save
await api.save(attrs.getValues());
attrs.commit();
// → pristine, novo baseline

// 6. Cancel (em outro momento)
attrs.reset();
// → volta ao commit

Add/Remove Workflow

// Virtual delete
attrs.remove('email');
attrs.has('email');        // false (não aparece)
attrs.isRemoved('email');  // true (ainda existe internamente)

// Cancelar remoção
attrs.reset('email');
attrs.has('email');        // true (restaurado)

// Confirmar remoção
attrs.remove('email');
attrs.commit();
attrs.has('email');        // false (deletado de verdade)

Types

import type {
  Attribute,
  AttributeInput,
  AttributeType,
  FieldState,
  FieldStatus,
  AttrEvents,
  ValidationResult,
  SchemaProps,
  SlotOptions,
  SlotInfo,
} from '@attrx/core';

import { DEFAULT_SLOT } from '@attrx/core'; // '#'

Integração com React

function useAttrValue(attrs: AttrCollection, key: string) {
  const [value, setValue] = useState(attrs.getValue(key));

  useEffect(() => {
    const handler = ({ key: k, value }: { key: string; value: unknown }) => {
      if (k === key) setValue(value);
    };
    attrs.on('valueChanged', handler);
    return () => attrs.off('valueChanged', handler);
  }, [attrs, key]);

  return value;
}

function useAttrStatus(attrs: AttrCollection, key: string) {
  const [status, setStatus] = useState(attrs.getStatus(key));

  useEffect(() => {
    const handler = ({ key: k, status }: { key: string; status: FieldStatus }) => {
      if (k === key) setStatus(status);
    };
    attrs.on('stateChanged', handler);
    return () => attrs.off('stateChanged', handler);
  }, [attrs, key]);

  return status;
}

Performance

  • O(1) para operações por key (setValue, getStatus, etc)
  • O(1) para lookup de slot (índice interno bySlot)
  • O(n do slot) para listar attrs de um slot (não O(n total))
  • O(dirty) para listas de alterados (getDirtyKeys, commit, reset)
  • Lazy original: só guarda snapshot se campo foi alterado
  • Batch: agrupa eventos para evitar re-renders

Métricas

  • 192 testes passando
  • ~28KB bundle size
  • 0 dependências runtime extras

Licença

MIT