pointy-ts
v1.0.0
Published
Type-safe, chainable file and data manipulation library
Maintainers
Readme
🎯 Pointy
🤔 Why Pointy Exists
Manipulating config files, spreadsheets, and environment files is boring boilerplate. Every project has the same dance:
// Read file → parse → mutate → stringify → write
const fs = require('fs');
const raw = fs.readFileSync('config.json', 'utf-8');
const data = JSON.parse(raw);
data.database.host = process.env.DB_HOST;
data.database.ssl = true;
fs.writeFileSync('config.json', JSON.stringify(data, null, 2));Five lines for one change. Multiply by every format you touch (JSON, CSV, YAML, Env…) and every project you ship. That's the pain Pointy solves.
Pointy wraps all that in a fluent, chainable builder that feels like writing a sentence — not a script.
✨ The Pointy Way
import { PointyBuilder } from 'pointy';
// One chain. One intent. Done.
await PointyBuilder.fromFile('config.json')
.asJson()
.pointTo('database.host')
.update(process.env.DB_HOST)
.pointTo('database.ssl')
.update(true)
.apply();No manual parse. No manual stringify. No fs.writeFileSync. Just describe what you want to change, and Pointy handles how.
🚦 Without vs With Pointy
JSON Config
Without Pointy — 9 lines of ceremony:
import { readFileSync, writeFileSync } from 'fs';
const raw = readFileSync('config.json', 'utf-8');
const data = JSON.parse(raw);
data.app.version = '1.0.1';
data.features.cache = true;
data.users[0].role = 'superadmin';
data.features.notifications = { enabled: true, channel: 'email' };
writeFileSync('config.json', JSON.stringify(data, null, 2));With Pointy — 5 lines of intent:
await PointyBuilder.fromFile('config.json')
.asJson()
.pointTo('app.version').update('1.0.1')
.pointTo('features.cache').update(true)
.pointTo('users.0.role').update('superadmin')
.pointTo('features').append('notifications', { enabled: true, channel: 'email' })
.apply();CSV Spreadsheet
Without Pointy — manual PapaParse + filter + rewrite:
import Papa from 'papaparse';
import { readFileSync, writeFileSync } from 'fs';
const raw = readFileSync('users.csv', 'utf-8');
const { data: rows } = Papa.parse(raw, { header: true });
rows.forEach((r) => { if (r.name === 'Alice') r.city = 'Boston'; });
writeFileSync('users.csv', Papa.unparse(rows));With Pointy — declarative selection + mutation:
await PointyBuilder.fromFile('users.csv')
.asCSV()
.pointTo('name', 'Alice', 'all')
.update({ city: 'Boston' })
.apply();Environment Variables
Without Pointy — ad-hoc parsing, manual joins:
import { readFileSync, writeFileSync } from 'fs';
const text = readFileSync('.env', 'utf-8');
const lines = text.split('\n');
const result = lines
.map((line) =>
line.startsWith('DB_HOST=') ? 'DB_HOST=remote.host' : line
)
.join('\n');
writeFileSync('.env', result);With Pointy — key-based navigation:
await PointyBuilder.fromFile('.env')
.asEnv()
.pointTo('DB_HOST')
.update('remote.host')
.apply();🚀 Quick Start
import { PointyBuilder } from 'pointy';
// --- JSON ---
await PointyBuilder.fromFile('config.json')
.asJson()
.pointTo('database.host')
.update('localhost')
.apply();
// --- CSV ---
const csv = PointyBuilder.fromCSV('name,age\ntom,30')
.asCSV()
.pointTo('name', 'tom', 'all')
.update({ age: '31' })
.apply();
// --- YAML → JSON conversion ---
const json = PointyBuilder.fromFile('config.yaml')
.asYaml()
.pointTo('app.debug')
.update(false)
.toJson()
.apply();
// --- Text ---
const text = PointyBuilder.fromText('line1\nline2')
.asText()
.pointTo('0')
.update('header')
.apply();📦 Supported Formats
| Format | Start | Coerce | Key Paths |
|---|---|---|---|
| JSON | fromJson() / fromFile('x.json') | asJson() | Dot-path (a.b.c, users.0.email) |
| CSV | fromCSV() / fromFile('x.csv') | asCSV() | Row index (0), column match (name,Alice,all), START_ROWS.0.END_ROWS.10 |
| Text | fromText() / fromFile('x.txt') | asText() | Line index (0), START, END, ranges |
| Env | fromEnv() / fromFile('.env') | asEnv() | Key name (DB_HOST) |
| YAML | fromYaml() / fromFile('x.yaml') | asYaml() | Dot-path (same as JSON) |
| TOON | fromToon() / fromFile('x.toon') | asToon() | Nested section keys |
🔄 Format Conversions
Pointy can read one format and write another. Zero manual re-serialization.
// Read YAML, write JSON
const json = PointyBuilder.fromFile('config.yaml')
.asYaml()
.pointTo('server.port')
.update(3000)
.toJson()
.outputToFile('config.json')
.apply();| From ↓ / To → | JSON | YAML | CSV | Text | Env | TOON | |---|---|---|---|---|---|---| | JSON | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | | CSV | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | | Text | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | | Env | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | | YAML | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | | TOON | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ |
(❌ = semantically unsafe, blocked at type level)
🧩 Plugin System
Register your own format at runtime:
import { PointyBuilder, FormatStrategy } from 'pointy';
class TomlStrategy implements FormatStrategy<'toml'> {
readonly format = 'toml';
parse(source) { /* ... */ }
stringify(data) { /* ... */ }
navigate(data, path) { /* ... */ }
mutate(data, op) { /* ... */ }
}
PointyBuilder.registerFormat('toml', new TomlStrategy(), ['.toml']);
const result = PointyBuilder.fromFormat('toml', 'key = val')
.asFormat('toml')
.pointTo('key')
.update('new')
.apply();⚡ Benchmarks
Run: npm run bench
| Operation | ops/sec | vs. Raw JSON | |---|---|---| | Raw JSON parse/stringify (baseline) | 1,818,000 | 1.0× | | JsonStrategy.parse + mutate | 1,339,000 | 0.74× | | Pointy: 1 update | 958,000 | 0.53× | | Pointy: 3 chained updates | 801,000 | 0.44× |
Pointy adds ~2× overhead over raw JSON — the price for immutability, type safety, and format abstraction but probably we can improve it in the future.
🛡️ Type Safety
Pointy enforces method sequencing at compile-time through a type-level state machine. Invalid chains are caught by TypeScript before your code runs:
// ❌ Compile-time errors
cannot mutate before coercion: PointyBuilder.fromFile('x.json').update(1)
cannot coerce after active: PointyBuilder.fromJson('{}').asJson().asCSV()
could not call terminal then mutate: PointyBuilder.fromObject({}).outputToFile('out').update(1)Entry Points & Phases
| Entry Point | Initial Phase | Methods Available |
|---|---|---|
| fromFile | Coercing | asJson, asCSV, asText, asEnv, asYaml, asToon |
| fromJson, fromCSV, fromText, fromEnv, fromYaml, fromToon | Coercing | Same as above |
| fromObject | Active | pointTo, update, toJson, apply, outputToFile, … |
After any .asXxx() or .asFormat(), the builder transitions to Active, unlocking navigation, mutations, conversions, and terminal operations.
📁 Samples
Check the samples/ folder for real files you can run against:
// Update JSON config
await PointyBuilder.fromFile('samples/config.json')
.asJson()
.pointTo('database.host')
.update('remote.host')
.apply();
// Update CSV rows
await PointyBuilder.fromFile('samples/users.csv')
.asCSV()
.pointTo('name', 'Alice', 'all')
.update({ city: 'Remote' })
.apply();
// Update env variables
await PointyBuilder.fromFile('samples/.env')
.asEnv()
.pointTo('DEBUG')
.update('true')
.apply();📜 License
Apache 2.0
This license gives you the same broad freedoms as MIT (use, modify, distribute, even commercially), with the addition of a patent grant. Most importantly: if you use this library, you must include attribution — a copyright notice and a copy of the license — so anyone who uses your software knows Pointy is part of the picture.
MIT is simpler but offers no patent protection and no formal attribution requirement. We chose Apache 2.0 because it protects both users and contributors, and it ensures credit is given where it's due.
