@ata-project/valibot 0.1.0 → 0.2.0
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/README.md +42 -3
- package/build.d.ts +39 -0
- package/build.js +102 -0
- package/package.json +14 -1
package/README.md
CHANGED
|
@@ -51,9 +51,10 @@ 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
|
|
55
|
-
across all three modes, including NaN and Infinity corners
|
|
56
|
-
suite runs a second time with
|
|
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
|
|
|
@@ -83,6 +84,44 @@ runtime blocks it: valibot stays at its usual speed, and the bridge falls back
|
|
|
83
84
|
to ata's interpreted engine at 624 ns for accepts and 187 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.
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
],
|
|
@@ -34,5 +36,16 @@
|
|
|
34
36
|
"@types/node": "^26.5.0",
|
|
35
37
|
"typescript": "^5.6.0",
|
|
36
38
|
"valibot": "^1.0.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
|
}
|