@ata-project/valibot 0.1.0 → 0.2.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.
Files changed (4) hide show
  1. package/README.md +48 -9
  2. package/build.d.ts +39 -0
  3. package/build.js +102 -0
  4. package/package.json +16 -3
package/README.md CHANGED
@@ -51,22 +51,23 @@ rejected value, and a rejection that might be Infinity-caused is handed to
51
51
  valibot for the final word. And `v.record` runs on arrays too, so records stay
52
52
  valibot's entirely.
53
53
 
54
- The package is differential-tested against valibot on 10,249 generated values
55
- across all three modes, including NaN and Infinity corners, and the whole
56
- suite runs a second time with code generation blocked.
54
+ The package is differential-tested against valibot on 11,788 generated values
55
+ across all three modes, including NaN and Infinity corners and the compiled
56
+ modules the build entry emits, and the whole suite runs a second time with
57
+ code generation blocked.
57
58
 
58
59
  ## What it costs, measured
59
60
 
60
61
  One representative API-boundary object schema (nine fields, nested arrays of
61
62
  objects, picklist, nullable), interleaved medians of 7 rounds on an M-series
62
- Mac, Node 25, valibot 1.4.2, ata-validator 1.13.2:
63
+ Mac, Node 25, valibot 1.5.0, ata-validator 1.25.0:
63
64
 
64
65
  | | valibot `safeParse` | this package |
65
66
  |---|---|---|
66
- | accept, verdict only | 1,051 ns | **20 ns** |
67
- | reject, verdict only | 1,117 ns | **83 ns** |
68
- | reject, `safeParse` | 1,117 ns | **84 ns** |
69
- | accept, `safeParse` | 1,051 ns | 1,062 ns |
67
+ | accept, verdict only | 740 ns | **21 ns** |
68
+ | reject, verdict only | 818 ns | **82 ns** |
69
+ | reject, `safeParse` | 818 ns | **83 ns** |
70
+ | accept, `safeParse` | 740 ns | 763 ns |
70
71
 
71
72
  The last row is by design, not a gap: an accepted value's output is valibot's
72
73
  to make. Plain `v.object` strips unknown keys, defaults fill, transforms
@@ -80,9 +81,47 @@ number constraints they drop to the bare engine verdict.
80
81
 
81
82
  With code generation blocked, the way a strict CSP or a locked-down edge
82
83
  runtime blocks it: valibot stays at its usual speed, and the bridge falls back
83
- to ata's interpreted engine at 624 ns for accepts and 187 ns for rejects,
84
+ to ata's interpreted engine at 627 ns for accepts and 184 ns for rejects,
84
85
  still ahead on both.
85
86
 
87
+ ## Ahead of time, for the browser
88
+
89
+ The runtime bridge above carries a general engine that can validate any schema
90
+ handed to it. In a browser that is the wrong trade, and valibot users of all
91
+ people know why: a bundle is not the place for a compiler.
92
+
93
+ So compile the schema instead, at build time:
94
+
95
+ ```js
96
+ import { compileToModule } from '@ata-project/valibot/build'
97
+ import { writeFileSync } from 'node:fs'
98
+
99
+ writeFileSync('validate-product.js', compileToModule(productSchema))
100
+ ```
101
+
102
+ The emitted module imports nothing, not valibot and not ata. It exports
103
+ `isValid(data)` and `validate(data)`. Measured on a four-field product schema,
104
+ esbuild, minified and gzipped:
105
+
106
+ | what ships | gz |
107
+ |---|---|
108
+ | valibot, tree-shaken for that schema | 1.7 KB |
109
+ | this package's runtime bridge (the engine) | 66.2 KB |
110
+ | **the compiled module** | **1.67 KB** |
111
+
112
+ Both the compiled module and valibot's 1.7 KB include error reporting, so
113
+ that is a fair pairing: the same size, no runtime dependency at all, and a
114
+ function that was compiled rather than walked.
115
+
116
+ Only schemas the classifier calls exact can be compiled. Anything valibot
117
+ checks at runtime (transforms, custom checks, formats, native types) throws
118
+ `NotExactError` rather than being quietly compiled without those checks; use
119
+ the runtime bridge for those. `canCompile(schema)` tells you which you have
120
+ before you build.
121
+
122
+ Emitting a module uses code generation, so run it at build time in Node. What
123
+ it emits is plain code and runs anywhere, a strict CSP included.
124
+
86
125
  ## Raw bytes
87
126
 
88
127
  `isValidBytes` answers from a `Buffer`, `Uint8Array` or JSON string. On an
package/build.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ import type { GenericSchema } from 'valibot'
2
+
3
+ export interface CompileToModuleOptions {
4
+ /** Module format of the emitted source. Default 'esm'. */
5
+ format?: 'esm' | 'cjs'
6
+ }
7
+
8
+ export interface CanCompileResult {
9
+ /** Whether the schema converts exactly and can be compiled. */
10
+ ok: boolean
11
+ mode: 'ata' | 'hybrid' | 'valibot'
12
+ /** Why not, when it cannot. */
13
+ reasons: string[]
14
+ }
15
+
16
+ export declare class NotExactError extends Error {
17
+ name: 'NotExactError'
18
+ mode: 'hybrid' | 'valibot'
19
+ reasons: string[]
20
+ }
21
+
22
+ /**
23
+ * Compile a valibot schema to standalone module source that imports nothing,
24
+ * not valibot and not ata. The module exports `isValid(data)` and
25
+ * `validate(data)`.
26
+ *
27
+ * Only schemas the classifier calls exact can be compiled; anything valibot
28
+ * checks at runtime throws NotExactError rather than being silently dropped.
29
+ *
30
+ * Emitting needs code generation, so run this at build time in Node. What it
31
+ * emits is plain code and runs anywhere, including under a strict CSP.
32
+ */
33
+ export declare function compileToModule(schema: GenericSchema, opts?: CompileToModuleOptions): string
34
+
35
+ /** Whether a schema can be compiled ahead of time, and why not when it cannot. */
36
+ export declare function canCompile(schema: GenericSchema): CanCompileResult
37
+
38
+ /** Whether this runtime allows the code generation the emitter needs. */
39
+ export declare function codegenAvailable(): boolean
package/build.js ADDED
@@ -0,0 +1,102 @@
1
+ 'use strict'
2
+
3
+ // Build-time compilation: a valibot schema becomes a standalone JavaScript
4
+ // module that imports nothing, not even this package or ata.
5
+ //
6
+ // const { compileToModule } = require('@ata-project/valibot/build')
7
+ // fs.writeFileSync('validate-user.js', compileToModule(userSchema))
8
+ //
9
+ // The emitted module is a plain function. Nothing about valibot or the ata
10
+ // engine ships with it, which is the point: the runtime bridge carries a
11
+ // general engine that can validate any schema handed to it, while this
12
+ // carries only the code for the one schema you compiled.
13
+ //
14
+ // Only schemas the classifier calls exact (`engine: 'ata'`) can be compiled.
15
+ // A hybrid schema is one whose JSON Schema conversion is deliberately looser
16
+ // than valibot, with valibot confirming the acceptances at runtime; emitting
17
+ // a standalone module for it would drop those checks silently and accept
18
+ // documents valibot rejects. This refuses instead. Declining to compile is
19
+ // recoverable; a validator that wrongly accepts is not.
20
+
21
+ const { analyze, toJSONSchema } = require('./index.js')
22
+
23
+ // The emitter builds the module source through the code generator, so this
24
+ // runs at build time in Node. What it emits is plain code with no generation
25
+ // of its own, so the compiled module runs anywhere, CSP included.
26
+ function codegenAvailable () {
27
+ try {
28
+ // eslint-disable-next-line no-new-func
29
+ new Function('return 1')
30
+ return true
31
+ } catch {
32
+ return false
33
+ }
34
+ }
35
+
36
+ class NotExactError extends Error {
37
+ constructor (analysis) {
38
+ super(
39
+ 'this schema cannot be compiled ahead of time: ' + analysis.reasons.join(', ') +
40
+ '. Those features are checked by valibot at runtime, so a standalone module ' +
41
+ 'would accept documents valibot rejects. Use compile() from the package root instead.',
42
+ )
43
+ this.name = 'NotExactError'
44
+ this.mode = analysis.mode
45
+ this.reasons = analysis.reasons
46
+ }
47
+ }
48
+
49
+ function header () {
50
+ return '// Generated by @ata-project/valibot from a valibot schema.\n' +
51
+ '// Standalone: imports nothing, including valibot and ata.\n' +
52
+ '//\n' +
53
+ '// This follows JSON Schema semantics, which is what the wire carries.\n' +
54
+ '// One documented difference from valibot in this direction: valibot\'s\n' +
55
+ '// number() accepts Infinity, a value JSON cannot express, and this\n' +
56
+ '// rejects it. Any document that arrived as JSON behaves identically.\n' +
57
+ '//\n' + '// Exports: isValid(data) and validate(data).\n'
58
+ }
59
+
60
+ // Compile a valibot schema to standalone module source.
61
+ //
62
+ // opts.format: 'esm' (default) or 'cjs'
63
+ //
64
+ // The module exports `isValid(data)` for the verdict and `validate(data)` for
65
+ // the verdict with an error report. Rename at the import site if you are
66
+ // compiling several schemas into one place; this deliberately does not
67
+ // rewrite the generated source to rename them, because rewriting emitted
68
+ // code is how validators start silently accepting things.
69
+ function compileToModule (schema, opts) {
70
+ const options = opts || {}
71
+ const analysis = analyze(schema)
72
+ if (analysis.mode !== 'ata') throw new NotExactError(analysis)
73
+
74
+ const { Validator } = require('ata-validator')
75
+ const { toStandaloneModule } = require('ata-validator/aot')
76
+ const jsonSchema = toJSONSchema(schema)
77
+ const validator = new Validator(jsonSchema, { assertFormat: false })
78
+ const src = toStandaloneModule(validator, { format: options.format || 'esm' })
79
+ if (!src) {
80
+ throw new Error(
81
+ codegenAvailable()
82
+ ? 'the ata engine declined to emit a standalone module for this schema. ' +
83
+ 'Use compile() from the package root instead.'
84
+ : 'compiling to a module needs code generation, which this runtime blocks. ' +
85
+ 'Run the compile step at build time in Node; the module it emits is plain ' +
86
+ 'code and runs anywhere, including under a strict CSP.',
87
+ )
88
+ }
89
+ return header() + src
90
+ }
91
+
92
+ // Whether a schema can be compiled ahead of time, and why not when it cannot.
93
+ function canCompile (schema) {
94
+ const analysis = analyze(schema)
95
+ return {
96
+ ok: analysis.mode === 'ata',
97
+ mode: analysis.mode,
98
+ reasons: analysis.reasons,
99
+ }
100
+ }
101
+
102
+ module.exports = { compileToModule, canCompile, codegenAvailable, NotExactError }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ata-project/valibot",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Run valibot schemas on the ata engine: same answers, verdicts in nanoseconds",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
@@ -8,6 +8,8 @@
8
8
  "files": [
9
9
  "index.js",
10
10
  "index.d.ts",
11
+ "build.js",
12
+ "build.d.ts",
11
13
  "README.md",
12
14
  "LICENSE"
13
15
  ],
@@ -25,7 +27,7 @@
25
27
  },
26
28
  "dependencies": {
27
29
  "@valibot/to-json-schema": "^1.3.0",
28
- "ata-validator": "^1.13.2"
30
+ "ata-validator": "^1.25.0"
29
31
  },
30
32
  "peerDependencies": {
31
33
  "valibot": "^1.0.0"
@@ -33,6 +35,17 @@
33
35
  "devDependencies": {
34
36
  "@types/node": "^26.5.0",
35
37
  "typescript": "^5.6.0",
36
- "valibot": "^1.0.0"
38
+ "valibot": "^1.5.0"
39
+ },
40
+ "exports": {
41
+ ".": {
42
+ "types": "./index.d.ts",
43
+ "default": "./index.js"
44
+ },
45
+ "./build": {
46
+ "types": "./build.d.ts",
47
+ "default": "./build.js"
48
+ },
49
+ "./package.json": "./package.json"
37
50
  }
38
51
  }