Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Spur Copilot Instructions
# Narro Copilot Instructions

## Rules

Expand Down
15 changes: 9 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,11 @@ jobs:
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 24
registry-url: 'https://registry.npmjs.org'

- name: Install dependencies
run: pnpm install --frozen-lockfile

- run: pnpx changelogithub
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Build
run: pnpm -r build

Expand All @@ -41,5 +38,11 @@ jobs:
- name: Update npm
run: npm install -g npm@latest

- name: Publish to NPM
run: pnpm -r publish --access public --no-git-checks
- run: pnpm dlx changelogithub
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Publish narro to NPM
run: pnpm --filter ./packages/narro publish --access public --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,6 @@
],
"cSpell.words": [
"Checkables",
"leitplanken"
"narro"
]
}
63 changes: 46 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,61 @@
## Spur
# Narro

Spur is a lightweight TypeScript schema validation library with a familiar, Zod-inspired API. It keeps bundle size tiny by loading constraint logic asynchronously only when a schema is built, so you get a fluent, chainable experience without paying for unused checks.
Lightweight TypeScript schema validation library with a Zod-like API that keeps bundle sizes minimal.

> Note: Spur is in early development with no official release yet. The API is not stable and may change in future builds.
> **⚠️ Development Status:** Narro is in early development with no stable release yet. The API may change in future builds.

### Why Spur
- **Tiny bundles** – constraint logic is lazy-loaded on first build, so unused checks never ship.
- **Ergonomic chains** – compose schemas with a straightforward, Zod-like API.
- **Heuristic insights** – built-in scoring highlights the most plausible schema branch and pinpoints exactly what failed, without losing data when branches overlap.
## Why Narro

### Runtime modes
- **`safeParse` (lazy)** – call `schema.safeParse(input)` and Spur will build the schema and run validation in one step. This keeps bundles minimal thanks to on-demand dynamic imports.
- **`build()` (eager)** – build the schema once to load the required checks up front, then reuse the returned evaluator for hot paths.
- **`spur/inline` (fully bundled)** – import from `spur/inline` when throughput beats bundle size; all checks ship together, so there are no dynamic imports at runtime.
- **Tiny bundles** – constraint logic lazy-loads on first build, so unused checks never ship
- **Zod-like API** – familiar, chainable schema composition
- **Heuristic insights** – built-in scoring highlights the most plausible schema branch and pinpoints failures
- **Performance options** – choose between minimal bundles or maximum throughput

## Runtime Modes

- **`safeParse` (lazy)** – validates with dynamic imports for minimal bundles
- **`build()` (eager)** – preload checks upfront for reuse in hot paths
- **`narro/inline`** – all checks bundled together for maximum throughput

## Quick Start

### Quick taste
```ts
import { number } from 'spur'
import * as n from 'narro'

const ageSchema = number().min(0).max(130)
const ageSchema = n.number().min(0).max(130)
const report = await ageSchema.safeParse(input)

if (report.passed) {
if (report.success) {
// use report.value
}
else {
console.log(report) // heuristics explain the most likely mismatch
// see why validation failed
}
```

For more details, explore the `packages/spur` directory or run the playground in `packages/playground`.
## Project Structure

This is a monorepo containing:

- **`packages/narro`** – the main validation library
- **`packages/bench`** – performance benchmarks

## Development

```bash
# Install dependencies
pnpm install

# Run tests
pnpm -r test

# Lint
pnpm lint

# Release narro package
pnpm release:narro
```

## License

MIT
5 changes: 0 additions & 5 deletions TODOS.md

This file was deleted.

29 changes: 23 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
{
"name": "spur-monorepo",
"name": "narro-monorepo",
"type": "module",
"version": "0.0.0",
"packageManager": "pnpm@10.15.1",
"description": "Monorepo for Spur",
"author": "",
"license": "ISC",
"keywords": [],
"packageManager": "pnpm@10.22.0",
"description": "Monorepo for Narro",
"author": "schplitt",
"license": "MIT",
"homepage": "https://github.com/schplitt/narro#readme",
"repository": {
"type": "git",
"url": "https://github.com/schplitt/narro.git"
},
"bugs": {
"url": "https://github.com/schplitt/narro/issues"
},
"keywords": [
"narro",
"typescript",
"schema",
"validation",
"monorepo"
],
"engines": {
"node": ">=20.0.0"
},
Expand All @@ -16,6 +30,9 @@
"release": "bumpp -r",
"prerelease": "eslint . && tsc --noEmit && vitest run"
},
"dependencies": {
"bumpp": "^10.3.1"
},
"devDependencies": {
"@antfu/eslint-config": "^5.2.2",
"changelogithub": "^13.16.0",
Expand Down
16 changes: 8 additions & 8 deletions packages/bench/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Spur Benchmarks
# Narro Benchmarks

Performance benchmarks comparing Spur with other popular validation libraries: Zod, Valibot, and ArkType.
Performance benchmarks comparing Narro with other popular validation libraries: Zod, Valibot, and ArkType.

## Benchmark Suites

Expand Down Expand Up @@ -32,13 +32,13 @@ pnpm bench

## Benchmark Variants

Each suite tests 4 Spur configurations + 3 competitors:
Each suite tests 4 Narro configurations + 3 competitors:

### Spur Variants
1. **spur unbuild async** - Schema built on each validation call
2. **spur async** - Pre-built schema (recommended for production)
3. **spur inline unbuild** - Inline export, built on each call
4. **spur inline** - Inline export, pre-built
### Narro Variants
1. **narro unbuild async** - Schema built on each validation call
2. **narro async** - Pre-built schema (recommended for production)
3. **narro inline unbuild** - Inline export, built on each call
4. **narro inline** - Inline export, pre-built

### Competitors
- **zod** - Synchronous validation
Expand Down
48 changes: 24 additions & 24 deletions packages/bench/bench/complex-backend.bench.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type } from 'arktype'
import { array, literal, number, object, string, union } from 'spur'
import { array as arrayInline, literal as literalInline, number as numberInline, object as objectInline, string as stringInline, union as unionInline } from 'spur/inline'
import { array, literal, number, object, string, union } from 'narro'
import { array as arrayInline, literal as literalInline, number as numberInline, object as objectInline, string as stringInline, union as unionInline } from 'narro/inline'
import * as v from 'valibot'
import { bench, describe } from 'vitest'
import { z } from 'zod'
Expand Down Expand Up @@ -105,8 +105,8 @@ const invalidOrder = {
tags: ['priority', 'gift-wrap'],
}

// Spur async schema
const spurAsyncUnbuild = object({
// Narro async schema
const narroAsyncUnbuild = object({
customer: object({
id: string().minLength(10),
email: string().minLength(5),
Expand Down Expand Up @@ -149,9 +149,9 @@ const spurAsyncUnbuild = object({
tags: array(string()).optional(),
})

const spurAsyncBuilt = await spurAsyncUnbuild.build()
const narroAsyncBuilt = await narroAsyncUnbuild.build()

const spurInlineUnbuild = objectInline({
const narroInlineUnbuild = objectInline({
customer: objectInline({
id: stringInline().minLength(10),
email: stringInline().minLength(5),
Expand Down Expand Up @@ -194,8 +194,8 @@ const spurInlineUnbuild = objectInline({
tags: arrayInline(stringInline()).optional(),
})

// Spur inline schema
const spurInlineBuilt = await objectInline({
// Narro inline schema
const narroInlineBuilt = await objectInline({
customer: objectInline({
id: stringInline().minLength(10),
email: stringInline().minLength(5),
Expand Down Expand Up @@ -374,17 +374,17 @@ const arkTypeSchema = type({
})

describe('complex backend: valid parse', () => {
bench('spur unbuild async valid', async () => {
await spurAsyncUnbuild.safeParse(validOrder)
bench('narro unbuild async valid', async () => {
await narroAsyncUnbuild.safeParse(validOrder)
})
bench('spur async valid', () => {
spurAsyncBuilt.safeParse(validOrder)
bench('narro async valid', () => {
narroAsyncBuilt.safeParse(validOrder)
})
bench('spur inline unbuild valid', async () => {
await spurInlineUnbuild.safeParse(validOrder)
bench('narro inline unbuild valid', async () => {
await narroInlineUnbuild.safeParse(validOrder)
})
bench('spur inline valid', () => {
spurInlineBuilt.safeParse(validOrder)
bench('narro inline valid', () => {
narroInlineBuilt.safeParse(validOrder)
})
bench('zod valid', () => {
zodSchema.safeParse(validOrder)
Expand All @@ -398,17 +398,17 @@ describe('complex backend: valid parse', () => {
})

describe('complex backend: invalid parse', () => {
bench('spur unbuild async invalid', async () => {
await spurAsyncUnbuild.safeParse(invalidOrder)
bench('narro unbuild async invalid', async () => {
await narroAsyncUnbuild.safeParse(invalidOrder)
})
bench('spur async invalid', () => {
spurAsyncBuilt.safeParse(invalidOrder)
bench('narro async invalid', () => {
narroAsyncBuilt.safeParse(invalidOrder)
})
bench('spur inline unbuild invalid', async () => {
await spurInlineUnbuild.safeParse(invalidOrder)
bench('narro inline unbuild invalid', async () => {
await narroInlineUnbuild.safeParse(invalidOrder)
})
bench('spur inline invalid', () => {
spurInlineBuilt.safeParse(invalidOrder)
bench('narro inline invalid', () => {
narroInlineBuilt.safeParse(invalidOrder)
})
bench('zod invalid', () => {
zodSchema.safeParse(invalidOrder)
Expand Down
48 changes: 24 additions & 24 deletions packages/bench/bench/complex-frontend.bench.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type } from 'arktype'
import { array, boolean, literal, object, string, union } from 'spur'
import { array, boolean, literal, object, string, union } from 'narro'

import { array as arrayInline, boolean as booleanInline, literal as literalInline, object as objectInline, string as stringInline, union as unionInline } from 'spur/inline'
import { array as arrayInline, boolean as booleanInline, literal as literalInline, object as objectInline, string as stringInline, union as unionInline } from 'narro/inline'
import * as v from 'valibot'
import { bench, describe } from 'vitest'
import { z } from 'zod'
Expand Down Expand Up @@ -87,8 +87,8 @@ const invalidFormData = {
referralCode: null,
}

// Spur async schema
const spurAsyncUnbuild = object({
// Narro async schema
const narroAsyncUnbuild = object({
account: object({
username: string().minLength(3).maxLength(30),
email: string().minLength(5).maxLength(100),
Expand Down Expand Up @@ -128,7 +128,7 @@ const spurAsyncUnbuild = object({
referralCode: string().nullable(),
})

const spurAsyncBuilt = await object({
const narroAsyncBuilt = await object({
account: object({
username: string().minLength(3).maxLength(30),
email: string().minLength(5).maxLength(100),
Expand Down Expand Up @@ -168,7 +168,7 @@ const spurAsyncBuilt = await object({
referralCode: string().nullable(),
}).build()

const spurInlineUnbuild = objectInline({
const narroInlineUnbuild = objectInline({
account: objectInline({
username: stringInline().minLength(3).maxLength(30),
email: stringInline().minLength(5).maxLength(100),
Expand Down Expand Up @@ -208,8 +208,8 @@ const spurInlineUnbuild = objectInline({
referralCode: stringInline().nullable(),
})

// Spur inline schema
const spurInlineBuilt = await objectInline({
// Narro inline schema
const narroInlineBuilt = await objectInline({
account: objectInline({
username: stringInline().minLength(3).maxLength(30),
email: stringInline().minLength(5).maxLength(100),
Expand Down Expand Up @@ -374,18 +374,18 @@ const arkTypeSchema = type({
})

describe('complex frontend: valid parse', () => {
bench('spur async unbuild valid', async () => {
await spurAsyncUnbuild.safeParse(validFormData)
bench('narro async unbuild valid', async () => {
await narroAsyncUnbuild.safeParse(validFormData)
})
bench('spur async valid', () => {
spurAsyncBuilt.safeParse(validFormData)
bench('narro async valid', () => {
narroAsyncBuilt.safeParse(validFormData)
})

bench('spur inline unbuild valid', async () => {
await spurInlineUnbuild.safeParse(validFormData)
bench('narro inline unbuild valid', async () => {
await narroInlineUnbuild.safeParse(validFormData)
})
bench('spur inline valid', () => {
spurInlineBuilt.safeParse(validFormData)
bench('narro inline valid', () => {
narroInlineBuilt.safeParse(validFormData)
})
bench('zod valid', () => {
zodSchema.safeParse(validFormData)
Expand All @@ -399,17 +399,17 @@ describe('complex frontend: valid parse', () => {
})

describe('complex frontend: invalid parse', () => {
bench('spur unbuild async invalid', async () => {
await spurAsyncUnbuild.safeParse(invalidFormData)
bench('narro unbuild async invalid', async () => {
await narroAsyncUnbuild.safeParse(invalidFormData)
})
bench('spur async invalid', () => {
spurAsyncBuilt.safeParse(invalidFormData)
bench('narro async invalid', () => {
narroAsyncBuilt.safeParse(invalidFormData)
})
bench('spur inline unbuild invalid', async () => {
await spurInlineUnbuild.safeParse(invalidFormData)
bench('narro inline unbuild invalid', async () => {
await narroInlineUnbuild.safeParse(invalidFormData)
})
bench('spur inline invalid', () => {
spurInlineBuilt.safeParse(invalidFormData)
bench('narro inline invalid', () => {
narroInlineBuilt.safeParse(invalidFormData)
})
bench('zod invalid', () => {
zodSchema.safeParse(invalidFormData)
Expand Down
Loading