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

@ujl-framework/core

v0.0.3

Published

UJL Core - The Heart of the UJL Framework

Readme

@ujl-framework/core

The Heart of the UJL Framework - A modular, type-safe system for building dynamic web layouts using an Abstract Syntax Tree (AST) architecture.

UJL Core provides the foundational building blocks for creating reusable, composable web layouts from JSON documents. Design rules are enforced at the schema level, making it impossible for content editors to break brand guidelines or accessibility standards.

Key features:

  • Modular architecture - Reusable modules with fields and slots
  • Full TypeScript support - Compile-time validation
  • Extensibility - Add custom modules and field types
  • Multiple outputs - Via adapters (HTML, Svelte, Web Components)
  • AI-native JSON - Optimized for LLMs, validated against schemas
  • Brand-Compliance by Design - Schema validation ensures valid layouts
  • Accessibility - Built-in through semantic HTML and WCAG-compliant structures

Installation

pnpm add @ujl-framework/core

Quick Start

import { Composer } from "@ujl-framework/core";

const ujlcDocument = {
	ujlc: {
		meta: { title: "Example" /* ... */ },
		root: [
			{
				type: "text",
				meta: { id: "text-001" /* ... */ },
				fields: { content: "Welcome to UJL Framework" },
				slots: {},
			},
		],
	},
};

const composer = new Composer();
const ast = await composer.compose(ujlcDocument);

Architecture

Data Flow

UJL Document (.ujlc.json)
    ↓
Composer (with Module Registry)
    ↓
AST (UJLAbstractNode with IDs)
    ↓
Adapter (HTML, Svelte, etc.)
    ↓
Output

Modules vs. AST Nodes

Modules are building blocks in .ujlc.json documents. They exist at the document level and are editable.

AST Nodes are created by the Composer during composition. Every node has:

  • node.id - Unique, randomly generated identifier
  • meta.moduleId - Which module this node belongs to
  • meta.isModuleRoot - true for editable module nodes

Child nodes (like grid-item wrappers) have meta.moduleId but isModuleRoot: false.

Extensibility

Creating Custom Fields

class EmailField extends FieldBase<string, EmailFieldConfig> {
	protected readonly defaultConfig = {
		label: "Email Address",
		default: "",
	};

	validate(raw: UJLCFieldObject): raw is string {
		return typeof raw === "string" && raw.includes("@");
	}

	fit(value: string): string {
		return value.toLowerCase().trim();
	}
}

Creating Custom Modules

class CustomModule extends ModuleBase {
	readonly name = "custom-module";

	readonly fields = [{ key: "title", field: new TextField({ label: "Title", default: "" }) }];

	readonly slots = [{ key: "content", slot: new Slot({ label: "Content", max: 5 }) }];

	async compose(
		moduleData: UJLCModuleObject,
		composer: Composer,
		doc: UJLCDocument,
	): Promise<UJLAbstractNode> {
		const title = this.parseField(moduleData, "title", "");
		const children: UJLAbstractNode[] = [];

		for (const child of moduleData.slots.content ?? []) {
			children.push(await composer.composeModule(child, doc));
		}

		return this.createNode("wrapper", { children }, moduleData);
	}
}

Registry Management

// Default registry
const composer = new Composer();

// Add custom modules
composer.registerModule(new CustomModule());

// Custom registry
const registry = new ModuleRegistry();
registry.registerModule(new CustomModule());
const composer = new Composer(registry);

Access Module Metadata

const composer = new Composer();
const modules = composer.getRegistry().getAllModules();

const module = composer.getRegistry().getModule("text");
if (module) {
	console.log(module.label, module.category);
	for (const field of module.fields) {
		console.log(field.key, field.field.config.label);
	}
}

Built-in Modules & Fields

Modules

| Module | Purpose | Fields | Slots | | ---------------- | -------------- | ------------------------- | --------- | | container | Layout wrapper | - | body | | text | Text content | content | - | | button | Buttons | label, href | - | | card | Content cards | title, description | content | | grid | Grid layout | - | items | | call-to-action | Action blocks | headline, description | - | | image | Image display | image, alt | - |

Fields

| Field | Purpose | Config | | --------------- | --------------- | -------------------------------------- | | TextField | Text input | maxLength, default | | NumberField | Numeric input | min, max, default | | RichTextField | Rich text | default (ProseMirror) | | ImageField | Image reference | default (string \| number \| null) |

Library System

The Composer is stateless. Modules access assets directly from doc.ujlc.library during composition.

Key principle: Rendering uses inline data; the LibraryProvider is only used for Crafter operations (upload, list, metadata).

// In a module's compose() method:
async compose(moduleData, composer, doc) {
	const imageId = this.parseField(moduleData, "image", null);
	const asset = imageId ? doc.ujlc.library[imageId] : null;
	// Use asset.img.src, asset.meta.alt, etc.
}

Assets are stored as LibraryAssetImage objects with:

  • img.src - Required fallback URL
  • img.srcset - Responsive images
  • meta - Alt, caption, credits
  • sources - Art direction

Providers (Crafter concern):

  • InlineLibraryProvider is provided by @ujl-framework/crafter
  • Custom providers implement the LibraryProvider interface from @ujl-framework/types

See @ujl-framework/types for complete type definitions.

Rich Text Schema

import { ujlRichTextExtensions } from "@ujl-framework/core";
import { Editor } from "@tiptap/core";

const editor = new Editor({
	extensions: ujlRichTextExtensions,
});

Supported: Paragraph, Text, Headings (h1-h6), Bold, Italic, Code, HardBreak, Blockquote, HorizontalRule, Lists (Bullet, Ordered).

API Reference

Core Classes

class Composer {
	constructor(registry?: ModuleRegistry);
	compose(doc: UJLCDocument): Promise<UJLAbstractNode>;
	composeModule(moduleData: UJLCModuleObject, doc: UJLCDocument): Promise<UJLAbstractNode>;
	registerModule(module: AnyModule): void;
	getRegistry(): ModuleRegistry;
}

abstract class ModuleBase {
	abstract readonly name: string;
	abstract readonly fields: FieldSet;
	abstract readonly slots: SlotSet;
	abstract compose(
		moduleData: UJLCModuleObject,
		composer: Composer,
		doc: UJLCDocument
	): UJLAbstractNode | Promise<UJLAbstractNode>;

	// Helpers
	protected parseField<T>(moduleData: UJLCModuleObject, key: string, fallback: T): T;
	protected escapeHtml(value: string): string;
	protected createNode<T>(type: T, props: ..., moduleData: UJLCModuleObject, isModuleRoot?: boolean): ...;
}

abstract class FieldBase<ValueT, ConfigT> {
	parse(raw: UJLCFieldObject): ValueT;
	abstract validate(raw: UJLCFieldObject): raw is ValueT;
	abstract fit(value: ValueT): ValueT;
}

Development

# Build
pnpm run build

# Type check
pnpm run check

# Format and lint
pnpm run format
pnpm run lint

Project Structure

src/
├── fields/
│   ├── base.ts           # FieldBase
│   └── concretes/        # Built-in fields
├── modules/
│   ├── base.ts           # ModuleBase
│   ├── concretes/        # Built-in modules
│   └── registry.ts       # ModuleRegistry
├── composer.ts           # Composer
└── index.ts