@weipertda/sigiljs 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ---
9
+
10
+ ## [0.0.1] — 2026-03-10
11
+
12
+ ### Added
13
+ - **`realType(value, opts?)`** — cross-realm-safe runtime type detection fixing all `typeof` gaps (`null`, `array`, `nan`, `map`, `set`, `date`, `regexp`, `promise`, `asyncfunction`, `generatorfunction`, `asyncgeneratorfunction`, `weakmap`, `weakset`); extensible via hook array.
14
+ - **`Sigil` / `T` / `S` tagged template** — compile a schema string into a fast, memoized validator object exposing `.check()` and `.assert()`.
15
+ - **Schema language** — primitives, literals, unions (`|`), arrays (`[]`), optionals (`?`), and nested object schemas.
16
+ - **`SigilValidationError`** — structured error with `code`, `path`, `expected`, and `actual` properties.
17
+ - **Two-level memoization** — Sigil-level cache (identical schema strings share one object reference) and compiled-validator cache (structural AST identity).
18
+ - **Partial evaluation** — literal unions optimize to `Set` membership checks; pure primitive unions skip `realType` entirely; object hints pre-compute required/optional key sets.
19
+ - **CLI playground** — `bun run src/playground.js '<json>' '<schema>'` for shell-level validation.
20
+
21
+ ### Engineering
22
+ - Single-source-of-truth `realType` implementation in `src/core/realType.js`.
23
+ - Module-scope frozen `Set` for O(1) tokenizer punctuation lookup.
24
+ - Array-join accumulation in tokenizer hot string loops (no `+=` reallocation).
25
+ - Module-scope frozen `Set` for parser primitive name lookup (not rebuilt per call).
26
+ - Sigil objects are `Object.freeze()`-d — immutable public API surface.
27
+ - `Error.captureStackTrace` on `SigilValidationError` for clean V8/Bun stack traces.
28
+ - Full `package.json` with `exports`, `files`, `sideEffects: false`, `engines`, `keywords`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SigilJS Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,261 @@
1
+ # SigilJS
2
+
3
+ Write types. Validate reality.
4
+
5
+ ## What is SigilJS?
6
+
7
+ SigilJS is a tiny JavaScript library for describing and validating data using **sigils**.
8
+
9
+ A sigil is a small expression (type expression) that describes what your data should look like.
10
+
11
+ Sigils are compiled into fast validators, so repeated checks stay efficient.
12
+
13
+
14
+ No TypeScript.
15
+
16
+ No dependencies.
17
+
18
+ Just JavaScript.
19
+
20
+ ---
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+
26
+ bun add @weipertda/sigiljs
27
+
28
+ ```
29
+
30
+ – or –
31
+
32
+ ```bash
33
+
34
+ npm install @weipertda/sigiljs
35
+
36
+ ```
37
+
38
+ ---
39
+
40
+ ### Create a Sigil
41
+
42
+ ```javascript
43
+
44
+ import { Sigil } from "@weipertda/sigiljs"
45
+
46
+ const Email = Sigil`string`
47
+
48
+ ```
49
+
50
+ ### Validate a Value
51
+
52
+ ```javascript
53
+
54
+ Email.check("hello@example.com")
55
+ // true
56
+
57
+ Email.check(42)
58
+ // false
59
+
60
+ ```
61
+
62
+ ### Optional Values
63
+
64
+ Use `?` to mark optional values.
65
+
66
+ ```javascript
67
+
68
+ const MaybeName = Sigil`string?`
69
+
70
+ ```
71
+
72
+ Matches:
73
+
74
+ ```javascript
75
+
76
+ string
77
+ undefined
78
+
79
+ ```
80
+
81
+ ---
82
+
83
+ ### Arrays
84
+
85
+ ```javascript
86
+
87
+ const Tags = Sigil`string[]`
88
+
89
+ Tags.check(["js", "bun"])
90
+ // true
91
+
92
+ ```
93
+
94
+ ---
95
+
96
+ ### Unions
97
+
98
+ ```javascript
99
+
100
+ const ID = Sigil`string | number`
101
+
102
+ ```
103
+
104
+ Matches either type.
105
+
106
+ ---
107
+
108
+ ### Object Validation
109
+
110
+ ```javascript
111
+
112
+ const User = Sigil`
113
+ {
114
+ name: string
115
+ age?: number
116
+ }
117
+ `
118
+
119
+ ```
120
+
121
+ Optional properties use ` ? `.
122
+
123
+ ---
124
+
125
+ ### Nested Objects
126
+
127
+ ```javascript
128
+
129
+ const Order = Sigil`
130
+ {
131
+ id: string
132
+ customer: {
133
+ name: string
134
+ email: string
135
+ }
136
+ items: {
137
+ name: string
138
+ price: number
139
+ }[]
140
+ }
141
+ `
142
+
143
+ ```
144
+
145
+ ---
146
+
147
+ ### Runtime Type Detection
148
+
149
+ SigilJS also provides a better ` typeof `.
150
+
151
+ ```javascript
152
+
153
+ import { realType } from "@weipertda/sigiljs"
154
+
155
+ realType([]) // "array"
156
+ realType(null) // "null"
157
+ realType(new Map()) // "map"
158
+
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Why SigilJS?
164
+
165
+ Sigil cleanly solves four problems:
166
+
167
+ 1. JavaScript's native ` typeof ` is inconsistent and too weak for real type work.
168
+
169
+ ```javascript
170
+
171
+ typeof []
172
+ // "object" 😬
173
+
174
+ ```
175
+
176
+ 2. TypeScript solves mostly compile-time problems, often adds friction, and disappears at runtime.
177
+
178
+ * TypeScript helps during development, but once your program runs the types are gone.
179
+
180
+ * SigilJS solves the runtime side of the problem.
181
+
182
+ * Describe your data once, then validate it anywhere.
183
+
184
+ ```typescript
185
+
186
+ // TypeScript disappears at runtime
187
+ const x: string = 123;
188
+
189
+ // No runtime error
190
+
191
+ ```
192
+
193
+ 3. Existing runtime validation libraries are dependency-heavy, allocation-happy, or ergonomically off.
194
+
195
+ 4. JavaScript lacks a native-feeling type expression system for runtime truth.
196
+
197
+ ---
198
+
199
+ ### Accurate Runtime Types
200
+
201
+ Replacing the gaps in ` typeof ` — ` realType ` correctly identifies ` null `, ` NaN `, arrays, async and generator functions, maps, sets, and arbitrary custom classes through hooks.
202
+
203
+ ```javascript
204
+
205
+ import { realType } from '@weipertda/sigiljs';
206
+
207
+ realType('x'); // "string"
208
+ realType(null); // "null"
209
+ realType(NaN); // "nan"
210
+ realType([]); // "array"
211
+ realType(new Map()); // "map"
212
+ realType(async function() {}); // "asyncfunction"
213
+
214
+ ```
215
+
216
+ You can even provide custom override hooks to map instances directly back to nominal strings:
217
+
218
+ ```javascript
219
+
220
+ realType(myThing, {
221
+ hooks: [ v => v instanceof MyThing ? 'mything' : null ]
222
+ }); // "mything"
223
+
224
+ ```
225
+
226
+ SigilJS solves runtime type validation with a tiny, dependency-free, runtime-native type system.
227
+
228
+ ---
229
+
230
+ ## Documentation
231
+
232
+ See the [docs/](docs/README.md) folder for full, detailed documentation __(WIP)__.
233
+
234
+ ---
235
+
236
+ ## Examples
237
+
238
+ See the [examples/](examples/) folder for runnable examples __(WIP)__.
239
+
240
+ ---
241
+
242
+ ## License
243
+
244
+ MIT
245
+
246
+ ---
247
+
248
+ ## CLI Playground
249
+
250
+ You can securely test out Sigil validator schemas against JSON inputs directly from your shell:
251
+
252
+ ```bash
253
+
254
+ bun run src/playground.js '{"name": "Doug"}' '{name: string, age?: number}'
255
+ # ✅ Validation passed
256
+
257
+ ```
258
+
259
+ ## Performance Philosophy
260
+
261
+ SigilJS embraces a Functional Core / Imperative Shell architecture. It takes your schema string, turns it into a typed token stream, drops parse grouping artifacts, flattens branches, optimizes primitive unions, and finally generates a blazingly fast validator closure mapped dynamically from the ground up to minimize allocations on the hot path. Repeated tagged template passes are thoroughly memoized.
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@weipertda/sigiljs",
3
+ "version": "0.0.1",
4
+ "description": "Runtime type sigils for JavaScript",
5
+ "keywords": [
6
+ "types",
7
+ "validation",
8
+ "runtime",
9
+ "schema",
10
+ "type-checking",
11
+ "zero-dependency",
12
+ "sigil",
13
+ "bun",
14
+ "javascript"
15
+ ],
16
+ "author": {
17
+ "name": "Daniel Weipert",
18
+ "email": "weipertda@gmail.com"
19
+ },
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/weipertda/sigiljs.git"
24
+ },
25
+ "sideEffects": false,
26
+ "type": "module",
27
+ "exports": {
28
+ ".": "./src/index.js"
29
+ },
30
+ "main": "./src/index.js",
31
+ "files": [
32
+ "src/",
33
+ "README.md",
34
+ "LICENSE",
35
+ "CHANGELOG.md"
36
+ ],
37
+ "scripts": {
38
+ "format": "prettier --write src/",
39
+ "lint": "eslint src/",
40
+ "test": "bun test"
41
+ },
42
+ "engines": {
43
+ "bun": ">=1",
44
+ "node": ">=18"
45
+ }
46
+ }
@@ -0,0 +1,191 @@
1
+ import { validate } from './validate.js';
2
+ import { SigilValidationError } from './errors.js';
3
+ import { realType } from './realType.js';
4
+
5
+ /**
6
+ * Validates a value against a schema and throws a structured `SigilValidationError`
7
+ * if validation fails. Returns `true` if validation passes.
8
+ *
9
+ * @param {object} astOrSigil - Raw AST, or a Sigil object with `.normalized` / `.ast`
10
+ * @param {*} value - The value to validate
11
+ * @param {object} [opts] - Optional hooks / options
12
+ * @returns {true}
13
+ * @throws {SigilValidationError}
14
+ */
15
+ export function assert(astOrSigil, value, opts) {
16
+ if (validate(astOrSigil, value, opts)) return true;
17
+
18
+ const ast = astOrSigil.normalized ?? astOrSigil.ast ?? astOrSigil;
19
+
20
+ // Walk the AST to find the precise failure point for a rich error
21
+ const err = findError(ast, value, opts, []);
22
+ if (err)
23
+ throw new SigilValidationError(
24
+ err.message,
25
+ err.path,
26
+ err.expected,
27
+ err.actual,
28
+ );
29
+
30
+ // Generic fallback (should rarely be reached)
31
+ throw new SigilValidationError(
32
+ 'Validation failed',
33
+ [],
34
+ 'match',
35
+ realType(value, opts),
36
+ );
37
+ }
38
+
39
+ /**
40
+ * Recursively walks the AST to find the deepest node that fails for `value`,
41
+ * returning a structured error descriptor or null if no specific failure is found.
42
+ *
43
+ * @param {object} ast
44
+ * @param {*} value
45
+ * @param {object} opts
46
+ * @param {string[]} path - Current property path (accumulated for error reporting)
47
+ * @returns {{ message: string, path: string[], expected: string, actual: string } | null}
48
+ */
49
+ function findError(ast, value, opts, path) {
50
+ if (!ast) return null;
51
+
52
+ switch (ast.kind) {
53
+ case 'primitive': {
54
+ const p = ast.name;
55
+ if (p === 'any' || p === 'unknown') return null;
56
+ if (p === 'never') {
57
+ const actual = realType(value, opts);
58
+ return {
59
+ message: `Expected never, got ${actual}`,
60
+ path,
61
+ expected: 'never',
62
+ actual,
63
+ };
64
+ }
65
+ // typeof-checkable primitives
66
+ if (
67
+ p === 'string' ||
68
+ p === 'number' ||
69
+ p === 'boolean' ||
70
+ p === 'symbol' ||
71
+ p === 'bigint' ||
72
+ p === 'undefined'
73
+ ) {
74
+ const t = typeof value;
75
+ return t !== p ?
76
+ { message: `Expected ${p}, got ${t}`, path, expected: p, actual: t }
77
+ : null;
78
+ }
79
+ const actual = realType(value, opts);
80
+ return actual !== p ?
81
+ { message: `Expected ${p}, got ${actual}`, path, expected: p, actual }
82
+ : null;
83
+ }
84
+
85
+ case 'literal': {
86
+ if (value !== ast.value) {
87
+ return {
88
+ message: `Expected literal ${JSON.stringify(ast.value)}, got ${JSON.stringify(value)}`,
89
+ path,
90
+ expected: String(ast.value),
91
+ actual: String(value),
92
+ };
93
+ }
94
+ return null;
95
+ }
96
+
97
+ case 'literal_union': {
98
+ if (!ast.values.includes(value)) {
99
+ const expected = ast.values.map((v) => JSON.stringify(v)).join(' | ');
100
+ return {
101
+ message: `Expected one of [${expected}], got ${JSON.stringify(value)}`,
102
+ path,
103
+ expected,
104
+ actual: String(value),
105
+ };
106
+ }
107
+ return null;
108
+ }
109
+
110
+ case 'primitive_union': {
111
+ const actual = realType(value, opts);
112
+ if (!ast.names.includes(actual)) {
113
+ const expected = ast.names.join(' | ');
114
+ return {
115
+ message: `Expected ${expected}, got ${actual}`,
116
+ path,
117
+ expected,
118
+ actual,
119
+ };
120
+ }
121
+ return null;
122
+ }
123
+
124
+ case 'union': {
125
+ // For unions we report the full set of expected types
126
+ const expected = ast.members.map((m) => m.name ?? m.kind).join(' | ');
127
+ return {
128
+ message: `Expected ${expected}, got ${realType(value, opts)}`,
129
+ path,
130
+ expected,
131
+ actual: realType(value, opts),
132
+ };
133
+ }
134
+
135
+ case 'optional': {
136
+ return value === undefined ? null : (
137
+ findError(ast.inner, value, opts, path)
138
+ );
139
+ }
140
+
141
+ case 'array': {
142
+ if (!Array.isArray(value)) {
143
+ const actual = realType(value, opts);
144
+ return {
145
+ message: `Expected array, got ${actual}`,
146
+ path,
147
+ expected: 'array',
148
+ actual,
149
+ };
150
+ }
151
+ for (let i = 0; i < value.length; i++) {
152
+ const err = findError(ast.element, value[i], opts, [
153
+ ...path,
154
+ String(i),
155
+ ]);
156
+ if (err) return err;
157
+ }
158
+ return null;
159
+ }
160
+
161
+ case 'object': {
162
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
163
+ const actual = realType(value, opts);
164
+ return {
165
+ message: `Expected object, got ${actual}`,
166
+ path,
167
+ expected: 'object',
168
+ actual,
169
+ };
170
+ }
171
+ for (const p of ast.properties) {
172
+ if (!p.optional && !(p.key in value)) {
173
+ return {
174
+ message: `Missing required property "${p.key}"`,
175
+ path: [...path, p.key],
176
+ expected: p.value.kind,
177
+ actual: 'undefined',
178
+ };
179
+ }
180
+ if (p.key in value && value[p.key] !== undefined) {
181
+ const err = findError(p.value, value[p.key], opts, [...path, p.key]);
182
+ if (err) return err;
183
+ }
184
+ }
185
+ return null;
186
+ }
187
+
188
+ default:
189
+ return null;
190
+ }
191
+ }
@@ -0,0 +1,7 @@
1
+ export const validatorCache = new Map();
2
+
3
+ export function canonicalize(ast) {
4
+ // A fast and structurally stable canonicalization for caching compiled validators.
5
+ // Because JSON.stringify preserves key order from our normalization phase.
6
+ return JSON.stringify(ast);
7
+ }
@@ -0,0 +1,154 @@
1
+ import { realType } from '../core/realType.js';
2
+ import { validatorCache, canonicalize } from './cache.js';
3
+
4
+ /**
5
+ * Compiles a normalized+partially-evaluated SigilJS AST into a fast validator function.
6
+ *
7
+ * Validators are memoized by structural identity (via JSON canonicalization).
8
+ * A validator function has the signature: (value, opts?) => boolean
9
+ *
10
+ * @param {object} ast - Normalized & partially-evaluated AST node
11
+ * @returns {(value: *, opts?: object) => boolean}
12
+ */
13
+ export function compile(ast) {
14
+ if (!ast) return () => true;
15
+
16
+ const key = canonicalize(ast);
17
+ const cached = validatorCache.get(key);
18
+ if (cached !== undefined) return cached;
19
+
20
+ const validator = build(ast);
21
+ validatorCache.set(key, validator);
22
+ return validator;
23
+ }
24
+
25
+ /**
26
+ * Recursively builds a validator closure from an AST node.
27
+ * @param {object} ast
28
+ * @returns {Function}
29
+ */
30
+ function build(ast) {
31
+ switch (ast.kind) {
32
+ case 'primitive': {
33
+ const p = ast.name;
34
+ if (p === 'any' || p === 'unknown') return () => true;
35
+ if (p === 'never') return () => false;
36
+ // typeof-based fast-path for JS primitives (no realType overhead)
37
+ if (
38
+ p === 'string' ||
39
+ p === 'number' ||
40
+ p === 'boolean' ||
41
+ p === 'symbol' ||
42
+ p === 'bigint' ||
43
+ p === 'undefined'
44
+ ) {
45
+ return (v) => typeof v === p;
46
+ }
47
+ return (v, opts) => realType(v, opts) === p;
48
+ }
49
+
50
+ case 'literal': {
51
+ const val = ast.value;
52
+ return (v) => v === val;
53
+ }
54
+
55
+ case 'literal_union': {
56
+ // Fast-path: Set membership for 5+ literals, Array.includes for fewer
57
+ if (ast.values.length < 5) {
58
+ const arr = ast.values;
59
+ return (v) => arr.includes(v);
60
+ }
61
+ const set = new Set(ast.values);
62
+ return (v) => set.has(v);
63
+ }
64
+
65
+ case 'primitive_union': {
66
+ const names = ast.names;
67
+ // If all names are typeof-checkable, skip realType entirely
68
+ const allSimple = names.every(
69
+ (n) =>
70
+ n === 'string' ||
71
+ n === 'number' ||
72
+ n === 'boolean' ||
73
+ n === 'symbol' ||
74
+ n === 'bigint' ||
75
+ n === 'undefined',
76
+ );
77
+ return allSimple ?
78
+ (v) => names.includes(typeof v)
79
+ : (v, opts) => names.includes(realType(v, opts));
80
+ }
81
+
82
+ case 'union': {
83
+ const fns = ast.members.map(build);
84
+ const len = fns.length;
85
+ return (v, opts) => {
86
+ for (let i = 0; i < len; i++) {
87
+ if (fns[i](v, opts)) return true;
88
+ }
89
+ return false;
90
+ };
91
+ }
92
+
93
+ case 'array': {
94
+ const el = build(ast.element);
95
+ return (v, opts) => {
96
+ if (!Array.isArray(v)) return false;
97
+ for (let i = 0; i < v.length; i++) {
98
+ if (!el(v[i], opts)) return false;
99
+ }
100
+ return true;
101
+ };
102
+ }
103
+
104
+ case 'optional': {
105
+ const inner = build(ast.inner);
106
+ return (v, opts) => v === undefined || inner(v, opts);
107
+ }
108
+
109
+ case 'object': {
110
+ // Use pre-computed hint arrays from partial evaluation to avoid
111
+ // re-filtering on every validator invocation (pure win — zero cost at compile time)
112
+ const { hints, properties } = ast;
113
+ const req =
114
+ hints ?
115
+ hints.requiredKeys.map((k) => ({
116
+ key: k,
117
+ check: build(properties.find((p) => p.key === k).value),
118
+ }))
119
+ : properties
120
+ .filter((p) => !p.optional)
121
+ .map((p) => ({ key: p.key, check: build(p.value) }));
122
+ const opt =
123
+ hints ?
124
+ hints.optionalKeys.map((k) => ({
125
+ key: k,
126
+ check: build(properties.find((p) => p.key === k).value),
127
+ }))
128
+ : properties
129
+ .filter((p) => p.optional)
130
+ .map((p) => ({ key: p.key, check: build(p.value) }));
131
+
132
+ const reqLen = req.length;
133
+ const optLen = opt.length;
134
+
135
+ return (v, opts) => {
136
+ if (typeof v !== 'object' || v === null || Array.isArray(v))
137
+ return false;
138
+ for (let i = 0; i < reqLen; i++) {
139
+ const p = req[i];
140
+ if (!(p.key in v) || !p.check(v[p.key], opts)) return false;
141
+ }
142
+ for (let i = 0; i < optLen; i++) {
143
+ const p = opt[i];
144
+ if (p.key in v && v[p.key] !== undefined && !p.check(v[p.key], opts))
145
+ return false;
146
+ }
147
+ return true;
148
+ };
149
+ }
150
+
151
+ default:
152
+ return () => false;
153
+ }
154
+ }