@c9up/rune 0.1.10 → 0.1.12
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/dist/Schema.d.ts +24 -3
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +62 -4
- package/dist/Schema.js.map +1 -1
- package/dist/formats.d.ts.map +1 -1
- package/dist/formats.js +5 -2
- package/dist/formats.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/native/generated.d.ts +3 -0
- package/dist/native/generated.d.ts.map +1 -0
- package/dist/native/generated.js +7 -0
- package/dist/native/generated.js.map +1 -0
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +3 -2
- package/scripts/build-napi-types.mjs +68 -0
- package/scripts/generate-napi-types.mjs +153 -0
- package/src/Schema.ts +66 -4
- package/src/formats.ts +5 -2
- package/src/native/generated.ts +9 -0
- package/src/native.ts +10 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generated.d.ts","sourceRoot":"","sources":["../../src/native/generated.ts"],"names":[],"mappings":"AAMA,oDAAoD;AAEpD,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// GENERATED FROM THE RUST — do not edit.
|
|
2
|
+
//
|
|
3
|
+
// Produced by scripts/generate-napi-types.mjs from napi-derive's type-def
|
|
4
|
+
// output. Editing this file by hand puts it back where it started: a
|
|
5
|
+
// description that can disagree with the code it describes.
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=generated.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generated.js","sourceRoot":"","sources":["../../src/native/generated.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,EAAE;AACF,0EAA0E;AAC1E,qEAAqE;AACrE,4DAA4D"}
|
package/dist/native.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAuCH;;GAEG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG;IACpD,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B,CAaA;AAED,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAaD,sEAAsE;AACtE,qBAAa,uBAAwB,SAAQ,KAAK;IACjD,QAAQ,CAAC,IAAI,EAAG,oBAAoB,CAAU;;CAQ9C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C"}
|
package/dist/native.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native.js","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAChD,MAAM,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE3D,MAAM,WAAW,GAA2B;IAC3C,WAAW,EAAE,eAAe;IAC5B,aAAa,EAAE,iBAAiB;IAChC,YAAY,EAAE,YAAY;IAC1B,cAAc,EAAE,cAAc;IAC9B,WAAW,EAAE,gBAAgB;CAC7B,CAAC;
|
|
1
|
+
{"version":3,"file":"native.js","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAChD,MAAM,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE3D,MAAM,WAAW,GAA2B;IAC3C,WAAW,EAAE,eAAe;IAC5B,aAAa,EAAE,iBAAiB;IAChC,YAAY,EAAE,YAAY;IAC1B,cAAc,EAAE,cAAc;IAC9B,WAAW,EAAE,gBAAgB;CAC7B,CAAC;AAWF,IAAI,MAA8B,CAAC;AACnC,IAAI,SAAkB,CAAC;AAEvB,IAAI,CAAC;IACJ,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,QAAQ,IAAI,IAAI,EAAE,CAAC,CAAC;IAClD,IAAI,MAAM,EAAE,CAAC;QACZ,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,UAAU,EAAE,YAAY,MAAM,OAAO,CAAC,CAAC,CAAC;IAChE,CAAC;AACF,CAAC;AAAC,OAAO,CAAC,EAAE,CAAC;IACZ,SAAS,GAAG,CAAC,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,WAAmB;IAKjD,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACd,+DAA+D,SAAS,IAAI,kBAAkB,EAAE,CAChG,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC;IACxD,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,KAAK,CACd,uEAAuE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAC/F,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AACf,CAAC;AAED,MAAM,UAAU,iBAAiB;IAChC,OAAO,MAAM,KAAK,SAAS,CAAC;AAC7B,CAAC;AAED,6EAA6E;AAC7E,SAAS,iBAAiB;IACzB,MAAM,MAAM,GAAG,GAAG,QAAQ,IAAI,IAAI,EAAE,CAAC;IACrC,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC7B,OAAO,mBAAmB,SAAS,YAAY,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC;IACjG,CAAC;IACD,OAAO,WAAW,CAAC,MAAM,CAAC,KAAK,SAAS;QACvC,CAAC,CAAC,kBAAkB;QACpB,CAAC,CAAC,0BAA0B,MAAM,EAAE,CAAC;AACvC,CAAC;AAED,sEAAsE;AACtE,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IACxC,IAAI,GAAG,oBAA6B,CAAC;IAC9C;QACC,KAAK,CACJ,gFAAgF,iBAAiB,EAAE,KAAK;YACvG,oFAAoF,CACrF,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACvC,CAAC;CACD;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,qBAAqB;IACpC,IAAI,MAAM,KAAK,SAAS;QAAE,MAAM,IAAI,uBAAuB,EAAE,CAAC;AAC/D,CAAC"}
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/rune",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Rune — Validation engine for the Ream framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
"prebuild": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
49
49
|
"build": "tsc -p tsconfig.build.json",
|
|
50
50
|
"build:rust": "cargo build --release -p rune-engine-napi",
|
|
51
|
-
"build:napi": "pnpm build:rust && node scripts/copy-napi.mjs",
|
|
51
|
+
"build:napi": "pnpm build:rust && pnpm build:napi-types && node scripts/copy-napi.mjs",
|
|
52
|
+
"build:napi-types": "node scripts/build-napi-types.mjs",
|
|
52
53
|
"test": "vitest run",
|
|
53
54
|
"test:rust": "cargo test -p rune-engine",
|
|
54
55
|
"lint": "biome check src/",
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Regenerate `src/native/generated.ts` from the Rust.
|
|
4
|
+
*
|
|
5
|
+
* Node rather than a shell script because this runs inside `build:napi`, and
|
|
6
|
+
* that runs on the Windows prebuild runner too: a bash `mktemp` there hands
|
|
7
|
+
* cargo a `/tmp/...` path the native proc-macro cannot write to, so the
|
|
8
|
+
* type-def file comes back empty and the build fails for no visible reason.
|
|
9
|
+
*
|
|
10
|
+
* One crate at a time on purpose: napi-derive APPENDS to `TYPE_DEF_TMP_PATH`
|
|
11
|
+
* while cargo compiles, and a parallel build interleaves the writes —
|
|
12
|
+
* definitions go missing, silently, and the generated file comes out short.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execFileSync } from 'node:child_process'
|
|
16
|
+
import { createRequire } from 'node:module'
|
|
17
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
18
|
+
import { tmpdir } from 'node:os'
|
|
19
|
+
import { dirname, join } from 'node:path'
|
|
20
|
+
import { fileURLToPath } from 'node:url'
|
|
21
|
+
|
|
22
|
+
const CRATES = ['rune-engine-napi']
|
|
23
|
+
|
|
24
|
+
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
25
|
+
const output = join('src', 'native', 'generated.ts')
|
|
26
|
+
const scratch = mkdtempSync(join(tmpdir(), 'napi-types-'))
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const lines = []
|
|
30
|
+
for (const crate of CRATES) {
|
|
31
|
+
const perCrate = join(scratch, `${crate}.jsonl`)
|
|
32
|
+
writeFileSync(perCrate, '')
|
|
33
|
+
execFileSync('cargo', ['build', '-p', crate], {
|
|
34
|
+
cwd: packageRoot,
|
|
35
|
+
env: { ...process.env, TYPE_DEF_TMP_PATH: perCrate },
|
|
36
|
+
stdio: ['ignore', 'ignore', 'inherit'],
|
|
37
|
+
})
|
|
38
|
+
const emitted = readFileSync(perCrate, 'utf8').split('\n').filter(Boolean)
|
|
39
|
+
if (emitted.length === 0) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`[napi-types] ${crate} emitted nothing — is napi-derive's "type-def" feature on?`,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
lines.push(...emitted)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const combined = join(scratch, 'combined.jsonl')
|
|
48
|
+
writeFileSync(combined, `${lines.join('\n')}\n`)
|
|
49
|
+
|
|
50
|
+
execFileSync(
|
|
51
|
+
process.execPath,
|
|
52
|
+
[join('scripts', 'generate-napi-types.mjs'), combined, output],
|
|
53
|
+
{ cwd: packageRoot, stdio: 'inherit' },
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// Formatted here rather than excluded from the linter: the file is checked
|
|
57
|
+
// in, so it should read like the rest of the tree — and formatting it at
|
|
58
|
+
// generation means it never shows up as a diff someone has to fix by hand.
|
|
59
|
+
// Resolved through the package entry rather than `node_modules/.bin`, whose
|
|
60
|
+
// shim is a shell script the Windows runner cannot execute directly.
|
|
61
|
+
const biome = createRequire(import.meta.url).resolve('@biomejs/biome/bin/biome')
|
|
62
|
+
execFileSync(process.execPath, [biome, 'format', '--write', output], {
|
|
63
|
+
cwd: packageRoot,
|
|
64
|
+
stdio: ['ignore', 'ignore', 'inherit'],
|
|
65
|
+
})
|
|
66
|
+
} finally {
|
|
67
|
+
rmSync(scratch, { recursive: true, force: true })
|
|
68
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate the TypeScript surface of the native module FROM the Rust.
|
|
4
|
+
*
|
|
5
|
+
* `napi-derive`'s `type-def` feature emits one JSON line per exported item
|
|
6
|
+
* while cargo compiles. Those lines are the source of truth: they are derived
|
|
7
|
+
* from the `#[napi]` items themselves, so a signature cannot drift from the
|
|
8
|
+
* Rust without this file changing.
|
|
9
|
+
*
|
|
10
|
+
* Hand-written interfaces are what this replaces, and they do drift — nothing
|
|
11
|
+
* on the TypeScript side notices when a `pub fn` gains a parameter or stops
|
|
12
|
+
* being `async`.
|
|
13
|
+
*
|
|
14
|
+
* One thing napi-rs cannot infer: the shape of a `JsFunction` callback. It
|
|
15
|
+
* emits `(...args: any[]) => any`. Any such parameter is refined below, by
|
|
16
|
+
* name, and the script fails if a refinement no longer matches anything — so
|
|
17
|
+
* the table cannot rot either.
|
|
18
|
+
*
|
|
19
|
+
* TYPE_DEF_TMP_PATH=<file> cargo build -p <crate>
|
|
20
|
+
* node scripts/generate-napi-types.mjs <file> <out.d.ts>
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Callback signatures napi-rs erases to `any`, restored here.
|
|
27
|
+
*
|
|
28
|
+
* Keyed by `<owner>.<method>`; the value replaces the whole parameter. Every
|
|
29
|
+
* entry must match, or the script fails: a stale refinement is how a hand
|
|
30
|
+
* annotation quietly stops describing the Rust.
|
|
31
|
+
*
|
|
32
|
+
* Empty while this crate exposes no `JsFunction` parameter — add an entry the
|
|
33
|
+
* day one appears, rather than letting an `any` through.
|
|
34
|
+
*/
|
|
35
|
+
const CALLBACK_REFINEMENTS = {}
|
|
36
|
+
|
|
37
|
+
const [input, output] = process.argv.slice(2)
|
|
38
|
+
if (!input || !output) {
|
|
39
|
+
console.error('usage: generate-napi-types.mjs <type-def file> <out.d.ts>')
|
|
40
|
+
process.exit(2)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const entries = readFileSync(input, 'utf8')
|
|
44
|
+
.split('\n')
|
|
45
|
+
.map((l) => l.trim())
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.map((l) => JSON.parse(l))
|
|
48
|
+
|
|
49
|
+
const used = new Set()
|
|
50
|
+
|
|
51
|
+
/** Apply the refinements that belong to `owner`, tracking which ones matched. */
|
|
52
|
+
function refine(owner, body) {
|
|
53
|
+
let out = body
|
|
54
|
+
for (const [key, { from, to }] of Object.entries(CALLBACK_REFINEMENTS)) {
|
|
55
|
+
const [entryOwner] = key.split('.')
|
|
56
|
+
if (entryOwner !== owner) continue
|
|
57
|
+
if (out.includes(from)) {
|
|
58
|
+
out = out.replace(from, to)
|
|
59
|
+
used.add(key)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* JSDoc as emitted by napi-derive, indented to sit above its member.
|
|
67
|
+
*
|
|
68
|
+
* `*` followed by `/` inside the text closes the comment early — a Rust doc
|
|
69
|
+
* example holding a cron expression (`0 */5 * * *`) is enough to do it, and
|
|
70
|
+
* the generated file then fails to parse. Escaped rather than stripped, so the
|
|
71
|
+
* example still reads correctly.
|
|
72
|
+
*/
|
|
73
|
+
function docBlock(doc, indent = '') {
|
|
74
|
+
if (!doc) return ''
|
|
75
|
+
// Escape every `*/` EXCEPT the one that closes the block. A Rust doc example
|
|
76
|
+
// holding a cron expression (`0 */5 * * *`) closes the comment early
|
|
77
|
+
// otherwise, and the generated file stops parsing — but escaping the closer
|
|
78
|
+
// too breaks it just as thoroughly, whether the block spans one line or many.
|
|
79
|
+
const closer = doc.lastIndexOf('*/')
|
|
80
|
+
const escaped =
|
|
81
|
+
closer === -1
|
|
82
|
+
? doc
|
|
83
|
+
: doc.slice(0, closer).replaceAll('*/', '*\\/') + doc.slice(closer)
|
|
84
|
+
return (
|
|
85
|
+
escaped
|
|
86
|
+
.split('\n')
|
|
87
|
+
.filter((l) => l.length > 0)
|
|
88
|
+
.map((l) => `${indent}${l}`)
|
|
89
|
+
.join('\n') + '\n'
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const interfaces = entries.filter((e) => e.kind === 'interface')
|
|
94
|
+
const structs = entries.filter((e) => e.kind === 'struct')
|
|
95
|
+
const impls = new Map(entries.filter((e) => e.kind === 'impl').map((e) => [e.name, e]))
|
|
96
|
+
const fns = entries.filter((e) => e.kind === 'fn')
|
|
97
|
+
|
|
98
|
+
const out = [
|
|
99
|
+
'// GENERATED FROM THE RUST — do not edit.',
|
|
100
|
+
'//',
|
|
101
|
+
"// Produced by scripts/generate-napi-types.mjs from napi-derive's type-def",
|
|
102
|
+
'// output. Editing this file by hand puts it back where it started: a',
|
|
103
|
+
'// description that can disagree with the code it describes.',
|
|
104
|
+
'',
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
for (const iface of interfaces) {
|
|
108
|
+
out.push(docBlock(iface.js_doc))
|
|
109
|
+
out.push(`export interface ${iface.name} {`)
|
|
110
|
+
for (const line of iface.def.split('\n')) {
|
|
111
|
+
out.push(line ? ` ${line.trim()}` : '')
|
|
112
|
+
}
|
|
113
|
+
out.push('}', '')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const struct of structs) {
|
|
117
|
+
const impl = impls.get(struct.name)
|
|
118
|
+
out.push(docBlock(struct.js_doc))
|
|
119
|
+
out.push(`export declare class ${struct.name} {`)
|
|
120
|
+
if (impl) {
|
|
121
|
+
for (const line of refine(struct.name, impl.def).split('\n')) {
|
|
122
|
+
out.push(line ? ` ${line.trim()}` : '')
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
out.push('}', '')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const fn of fns) {
|
|
129
|
+
out.push(docBlock(fn.js_doc))
|
|
130
|
+
// napi-derive emits the whole declaration for a function, unlike a struct
|
|
131
|
+
// where `def` holds only the members.
|
|
132
|
+
const declaration = fn.def.trim()
|
|
133
|
+
out.push(
|
|
134
|
+
declaration.startsWith('export declare function')
|
|
135
|
+
? `${declaration};`
|
|
136
|
+
: `export declare function ${fn.name}${declaration};`,
|
|
137
|
+
'',
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const stale = Object.keys(CALLBACK_REFINEMENTS).filter((k) => !used.has(k))
|
|
142
|
+
if (stale.length > 0) {
|
|
143
|
+
console.error(
|
|
144
|
+
`[napi-types] refinement(s) that matched nothing: ${stale.join(', ')}\n` +
|
|
145
|
+
'[napi-types] the Rust changed under them — update or remove the entry.',
|
|
146
|
+
)
|
|
147
|
+
process.exit(1)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
writeFileSync(output, out.join('\n'))
|
|
151
|
+
console.log(
|
|
152
|
+
`[napi-types] ${output} — ${interfaces.length} interface(s), ${structs.length} class(es), ${fns.length} function(s)`,
|
|
153
|
+
)
|
package/src/Schema.ts
CHANGED
|
@@ -1850,6 +1850,7 @@ export class RuleChain<Output = unknown> {
|
|
|
1850
1850
|
#nestedSchema: Record<string, RuleChain> | null = null;
|
|
1851
1851
|
#arrayItemChain: RuleChain | null = null;
|
|
1852
1852
|
#allowUnknown = false;
|
|
1853
|
+
#denyUnknown = false;
|
|
1853
1854
|
#metadata: Record<string, unknown> | null = null;
|
|
1854
1855
|
/** Extensions / MIME types declared by `file()` / `mimeTypes()`. */
|
|
1855
1856
|
#declaredExtnames: readonly string[] | null = null;
|
|
@@ -1933,6 +1934,11 @@ export class RuleChain<Output = unknown> {
|
|
|
1933
1934
|
get allowsUnknown(): boolean {
|
|
1934
1935
|
return this.#allowUnknown;
|
|
1935
1936
|
}
|
|
1937
|
+
|
|
1938
|
+
/** Whether an undeclared key fails the payload rather than being dropped. */
|
|
1939
|
+
get deniesUnknown(): boolean {
|
|
1940
|
+
return this.#denyUnknown;
|
|
1941
|
+
}
|
|
1936
1942
|
/** Free-form JSON Schema metadata attached with `meta()`. */
|
|
1937
1943
|
get metadata(): Record<string, unknown> | null {
|
|
1938
1944
|
return this.#metadata;
|
|
@@ -1980,6 +1986,7 @@ export class RuleChain<Output = unknown> {
|
|
|
1980
1986
|
next.#dateFormats = this.#dateFormats;
|
|
1981
1987
|
next.#coercions = [...this.#coercions];
|
|
1982
1988
|
next.#allowUnknown = this.#allowUnknown;
|
|
1989
|
+
next.#denyUnknown = this.#denyUnknown;
|
|
1983
1990
|
next.#metadata = this.#metadata ? { ...this.#metadata } : null;
|
|
1984
1991
|
next.#declaredExtnames = this.#declaredExtnames;
|
|
1985
1992
|
next.#declaredMimeTypes = this.#declaredMimeTypes;
|
|
@@ -2290,6 +2297,23 @@ export class RuleChain<Output = unknown> {
|
|
|
2290
2297
|
*/
|
|
2291
2298
|
allowUnknownProperties(): this {
|
|
2292
2299
|
this.#allowUnknown = true;
|
|
2300
|
+
this.#denyUnknown = false;
|
|
2301
|
+
return this;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
/**
|
|
2305
|
+
* Fail the payload on a key the shape does not declare, instead of dropping
|
|
2306
|
+
* it.
|
|
2307
|
+
*
|
|
2308
|
+
* A deliberate addition: upstream offers keep-or-drop and nothing else, and
|
|
2309
|
+
* dropping stays the default so a migrated schema behaves the same. This is
|
|
2310
|
+
* for an API whose contract is that it refuses what it does not understand
|
|
2311
|
+
* — where a client sending `emial` should be told, not silently ignored and
|
|
2312
|
+
* left wondering why the address never changed.
|
|
2313
|
+
*/
|
|
2314
|
+
denyUnknownProperties(): this {
|
|
2315
|
+
this.#denyUnknown = true;
|
|
2316
|
+
this.#allowUnknown = false;
|
|
2293
2317
|
return this;
|
|
2294
2318
|
}
|
|
2295
2319
|
|
|
@@ -2869,11 +2893,31 @@ export class RuleChain<Output = unknown> {
|
|
|
2869
2893
|
);
|
|
2870
2894
|
}
|
|
2871
2895
|
|
|
2872
|
-
/**
|
|
2896
|
+
/**
|
|
2897
|
+
* Must match a regular expression (TS-only — never dispatched to Rust).
|
|
2898
|
+
*
|
|
2899
|
+
* `toJSONSchema()` emits the expression's SOURCE. JSON Schema's `pattern`
|
|
2900
|
+
* has nowhere to carry flags, so `i`, `m` and `s` do not survive into the
|
|
2901
|
+
* emitted schema even though they are honoured here — a `/abc/i` validates
|
|
2902
|
+
* case-insensitively and exports as case-sensitive. Keep that in mind when
|
|
2903
|
+
* the emitted schema is handed to another validator.
|
|
2904
|
+
*/
|
|
2873
2905
|
regex(pattern: RegExp): this {
|
|
2906
|
+
// `test()` advances `lastIndex` on a global or sticky expression, so the
|
|
2907
|
+
// SAME value alternates between valid and invalid across calls. Reset it
|
|
2908
|
+
// before each test rather than stripping the flags: `y` anchors the
|
|
2909
|
+
// match at `lastIndex`, and dropping it would let the pattern match
|
|
2910
|
+
// anywhere instead.
|
|
2911
|
+
const matches = (value: string): boolean => {
|
|
2912
|
+
pattern.lastIndex = 0;
|
|
2913
|
+
return pattern.test(value);
|
|
2914
|
+
};
|
|
2874
2915
|
this.#pushRule({
|
|
2875
2916
|
name: "regex",
|
|
2876
|
-
|
|
2917
|
+
// The source is carried so `toJSONSchema()` can emit the constraint;
|
|
2918
|
+
// without it the emitted schema silently accepts anything.
|
|
2919
|
+
args: { pattern: pattern.source },
|
|
2920
|
+
validate: (v) => typeof v === "string" && matches(v),
|
|
2877
2921
|
message: "Invalid format",
|
|
2878
2922
|
});
|
|
2879
2923
|
return this;
|
|
@@ -3107,7 +3151,7 @@ export class RuleChain<Output = unknown> {
|
|
|
3107
3151
|
*
|
|
3108
3152
|
* Checksums are run where the country defines a short, well-defined one
|
|
3109
3153
|
* (BE, DE, NL, IT, PT, LU, CH); the others are FORMAT-only, which is stated
|
|
3110
|
-
* rather than implied. An unknown country
|
|
3154
|
+
* rather than implied. An unknown country RAISES rather than accepting the
|
|
3111
3155
|
* value unchecked.
|
|
3112
3156
|
*/
|
|
3113
3157
|
vat(options: VatOptions | ((field: FieldContext) => VatOptions)): this {
|
|
@@ -3316,7 +3360,7 @@ export class RuleChain<Output = unknown> {
|
|
|
3316
3360
|
return this.#stringMutation("toCamelCase", toCamelCase);
|
|
3317
3361
|
}
|
|
3318
3362
|
|
|
3319
|
-
/** HTML-escape
|
|
3363
|
+
/** HTML-escape the eight characters VineJS `escape` does. */
|
|
3320
3364
|
escape(): this {
|
|
3321
3365
|
return this.#stringMutation("escape", escapeHtml);
|
|
3322
3366
|
}
|
|
@@ -3977,6 +4021,24 @@ export class RuleChain<Output = unknown> {
|
|
|
3977
4021
|
) ?? grp.branches.find((candidate) => candidate.predicate === null);
|
|
3978
4022
|
if (branch) shape = { ...shape, ...branch.shape };
|
|
3979
4023
|
}
|
|
4024
|
+
if (this.#denyUnknown) {
|
|
4025
|
+
const declared = new Set(Object.keys(shape));
|
|
4026
|
+
const undeclared = Object.keys(source).filter(
|
|
4027
|
+
(key) => !declared.has(key),
|
|
4028
|
+
);
|
|
4029
|
+
if (undeclared.length > 0) {
|
|
4030
|
+
// Named one by one, and the shape listed: a typo is the common
|
|
4031
|
+
// case, and "unknown field" without the alternatives leaves the
|
|
4032
|
+
// caller to guess which spelling was expected.
|
|
4033
|
+
for (const key of undeclared) {
|
|
4034
|
+
errors.push({
|
|
4035
|
+
field: field ? `${field}.${key}` : key,
|
|
4036
|
+
rule: "unknownField",
|
|
4037
|
+
message: `unknown field \`${key}\`, expected one of ${[...declared].join(", ")}`,
|
|
4038
|
+
});
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
3980
4042
|
for (const [nestedField, chain] of Object.entries(shape)) {
|
|
3981
4043
|
const nestedResult = chain._validateWithTransform(
|
|
3982
4044
|
`${field}.${nestedField}`,
|
package/src/formats.ts
CHANGED
|
@@ -167,10 +167,13 @@ export const isMobile = (v: string): boolean =>
|
|
|
167
167
|
export function escapeHtml(v: string): string {
|
|
168
168
|
return v
|
|
169
169
|
.replace(/&/g, "&")
|
|
170
|
+
.replace(/"/g, """)
|
|
171
|
+
.replace(/'/g, "'")
|
|
170
172
|
.replace(/</g, "<")
|
|
171
173
|
.replace(/>/g, ">")
|
|
172
|
-
.replace(
|
|
173
|
-
.replace(
|
|
174
|
+
.replace(/\//g, "/")
|
|
175
|
+
.replace(/\\/g, "\")
|
|
176
|
+
.replace(/`/g, "`");
|
|
174
177
|
}
|
|
175
178
|
|
|
176
179
|
/** Options accepted by `normalizeEmail` (subset of VineJS's). */
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// GENERATED FROM THE RUST — do not edit.
|
|
2
|
+
//
|
|
3
|
+
// Produced by scripts/generate-napi-types.mjs from napi-derive's type-def
|
|
4
|
+
// output. Editing this file by hand puts it back where it started: a
|
|
5
|
+
// description that can disagree with the code it describes.
|
|
6
|
+
|
|
7
|
+
/** Validate data against a schema (JSON string). */
|
|
8
|
+
|
|
9
|
+
export declare function validate(requestJson: string): string;
|
package/src/native.ts
CHANGED
|
@@ -20,7 +20,16 @@ const platformMap: Record<string, string> = {
|
|
|
20
20
|
"win32-x64": "win32-x64-msvc",
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The engine's surface, as the Rust declares it.
|
|
25
|
+
*
|
|
26
|
+
* Derived from `./native/generated.js` — written by `pnpm build:napi-types`
|
|
27
|
+
* from napi-derive's own `type-def` output — rather than restated here, where
|
|
28
|
+
* nothing would notice a `pub fn` gaining a parameter or changing its return.
|
|
29
|
+
*/
|
|
30
|
+
type NativeRune = typeof import("./native/generated.js");
|
|
31
|
+
|
|
32
|
+
let native: NativeRune | undefined;
|
|
24
33
|
let loadError: unknown;
|
|
25
34
|
|
|
26
35
|
try {
|