fastify-ata 0.3.0 → 0.4.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 CHANGED
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="./assets/fastify-ata.svg" alt="fastify-ata" width="640" />
3
+ </p>
4
+
1
5
  # fastify-ata
2
6
 
3
7
  Fastify plugin for [ata-validator](https://ata-validator.com) - JSON Schema validation powered by simdjson.
@@ -0,0 +1,28 @@
1
+ <svg width="100%" viewBox="0 0 1610 910" xmlns="http://www.w3.org/2000/svg" role="img" style="font-family: &quot;IBM Plex Mono&quot;, Inter, -apple-system, monospace;">
2
+ <title style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;IBM Plex Mono&quot;, Inter, -apple-system, monospace;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">fastify-ata</title>
3
+ <desc style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;IBM Plex Mono&quot;, Inter, -apple-system, monospace;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">Beyaz zemin üzerinde Fastify logosu ve ata yazısı</desc>
4
+
5
+ <rect x="0" y="0" width="1600" height="900" fill="#FFFFFF" style="fill:rgb(255, 255, 255);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;IBM Plex Mono&quot;, Inter, -apple-system, monospace;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
6
+
7
+ <!--
8
+ Kompozisyon: [FASTIFY LOGOSU] + "ata"
9
+ Logo harflerle aynı yükseklikte (optik olarak), yatay olarak akıyor.
10
+ Logonun aspect ratio'su 1:1 (300×300), ama görsel yoğunluğu sol tarafta.
11
+
12
+ Tipografi: ata — 240px, semibold, siyah
13
+ Logo yüksekliği yaklaşık 200px, ata harfleri ile baseline'da hizalı.
14
+
15
+ Layout:
16
+ [logo ~240px genişlik] [boşluk ~60px] "ata" [~330px genişlik]
17
+ Toplam ~630px, ortaya yerleşecek: x = (1600-630)/2 = 485
18
+ -->
19
+
20
+ <!-- Fastify logosu -->
21
+ <image x="410" y="330" width="260" height="260" href="data:image/png;base64,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" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;IBM Plex Mono&quot;, Inter, -apple-system, monospace;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
22
+
23
+ <!-- "ata" yazısı — logonun sağında, baseline hizalı -->
24
+ <g font-family="'IBM Plex Mono', 'Inter', -apple-system, monospace" fill="#0A0A0A" style="fill:rgb(10, 10, 10);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;IBM Plex Mono&quot;, Inter, -apple-system, monospace;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
25
+ <text x="730" y="520" font-size="240" font-weight="600" letter-spacing="-8" style="fill:rgb(10, 10, 10);stroke:none;color:rgb(255, 255, 255);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;IBM Plex Mono&quot;, Inter, -apple-system, monospace;font-size:240px;font-weight:600;text-anchor:start;dominant-baseline:auto">ata</text>
26
+ </g>
27
+
28
+ </svg>
@@ -0,0 +1,24 @@
1
+ 'use strict'
2
+
3
+ // Preload that makes Fastify use ata as its GLOBAL default validator, by
4
+ // intercepting the single `require('@fastify/ajv-compiler')` in Fastify's
5
+ // schema-controller and returning the ata factory instead.
6
+ //
7
+ // Usage (from a Fastify checkout):
8
+ // node --require /abs/path/fastify-ata/compat/ata-default-preload.js \
9
+ // --test test/schema-validation.test.js
10
+ //
11
+ // Tests that build their own AJV instance (require('ajv')) or set a custom
12
+ // validatorCompiler bypass this and keep using AJV, as they should.
13
+
14
+ const Module = require('module')
15
+ const path = require('path')
16
+ const AtaCompiler = require(path.join(__dirname, '..', 'compiler'))
17
+
18
+ const origLoad = Module._load
19
+ Module._load = function (request, parent, isMain) {
20
+ if (request === '@fastify/ajv-compiler') {
21
+ return AtaCompiler
22
+ }
23
+ return origLoad.apply(this, arguments)
24
+ }
package/compiler.js ADDED
@@ -0,0 +1,54 @@
1
+ 'use strict'
2
+
3
+ const { Validator } = require('ata-validator')
4
+
5
+ // @fastify/ajv-compiler-compatible factory so ata can be installed as
6
+ // Fastify's global default validator:
7
+ //
8
+ // Fastify({ schemaController: { compilersFactory: { buildValidator: AtaCompiler() } } })
9
+ //
10
+ // Shape mirrors @fastify/ajv-compiler:
11
+ // AtaCompiler() -> buildCompilerFromPool(externalSchemas, options)
12
+ // -> buildValidatorFunction({ schema }) -> validate(data)
13
+
14
+ function AtaCompiler() {
15
+ return function buildCompilerFromPool(externalSchemas, options) {
16
+ const customOptions = (options && options.customOptions) || {}
17
+ // Default to Fastify's default AJV behavior: coerce (array mode, so a scalar
18
+ // becomes a single-element array), apply defaults, strip undeclared
19
+ // properties. Honor explicit overrides, preserving the 'array' mode value.
20
+ const coerceTypes = customOptions.coerceTypes !== undefined ? customOptions.coerceTypes : 'array'
21
+ const removeAdditional = customOptions.removeAdditional !== undefined ? !!customOptions.removeAdditional : true
22
+ // Fastify's default AJV runs with allErrors: false, reporting only the
23
+ // first violation. Mirror that unless the caller opts into allErrors. ata's
24
+ // abortEarly returns a stub error, so instead collect fully and expose the
25
+ // first real error to keep its message and rich fields intact.
26
+ const firstErrorOnly = customOptions.allErrors !== true
27
+ const hasCoercion = coerceTypes || removeAdditional
28
+
29
+ const validatorOpts = {
30
+ schemas: externalSchemas,
31
+ coerceTypes,
32
+ removeAdditional,
33
+ }
34
+
35
+ return function buildValidatorFunction({ schema }) {
36
+ const validator = new Validator(schema, validatorOpts)
37
+ function validate(data) {
38
+ const result = validator.validate(data)
39
+ if (result.valid) {
40
+ validate.errors = null
41
+ return hasCoercion ? { value: data } : true
42
+ }
43
+ validate.errors = firstErrorOnly ? [result.errors[0]] : result.errors
44
+ return false
45
+ }
46
+ validate.errors = null
47
+ return validate
48
+ }
49
+ }
50
+ }
51
+
52
+ module.exports = AtaCompiler
53
+ module.exports.AtaCompiler = AtaCompiler
54
+ module.exports.default = AtaCompiler
package/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { FastifyPluginCallback } from 'fastify'
1
+ import { FastifyPluginCallback, FastifyTypeProvider } from 'fastify'
2
+ import { Infer, JSONSchema } from 'ata-validator'
2
3
 
3
4
  interface FastifyAtaOptions {
4
5
  /** Convert "42" -> 42 for integer fields, etc. */
@@ -11,8 +12,26 @@ interface FastifyAtaOptions {
11
12
  * only care about reject/accept.
12
13
  */
13
14
  abortEarly?: boolean
15
+ /**
16
+ * Install a schema error formatter that renders compiler-grade messages,
17
+ * including the ATA error code and a did-you-mean suggestion when available.
18
+ * Off by default to preserve AJV-compatible error messages.
19
+ */
20
+ prettyErrors?: boolean
14
21
  }
15
22
 
16
- declare const fastifyAta: FastifyPluginCallback<FastifyAtaOptions>
23
+ declare namespace fastifyAta {
24
+ /**
25
+ * Fastify type provider backed by ata's `Infer<S>`. Use with
26
+ * `Fastify().withTypeProvider<fastifyAta.AtaTypeProvider>()` and author route
27
+ * schemas with `defineSchema(...)` from ata-validator so `request.body`,
28
+ * `request.query`, etc. are typed from the schema with no manual annotation.
29
+ */
30
+ interface AtaTypeProvider extends FastifyTypeProvider {
31
+ validator: this['schema'] extends JSONSchema ? Infer<this['schema']> : unknown
32
+ serializer: this['schema'] extends JSONSchema ? Infer<this['schema']> : unknown
33
+ }
34
+ }
17
35
 
36
+ declare const fastifyAta: FastifyPluginCallback<FastifyAtaOptions>
18
37
  export = fastifyAta
package/index.js CHANGED
@@ -3,6 +3,18 @@
3
3
  const fp = require('fastify-plugin')
4
4
  const { Validator } = require('ata-validator')
5
5
 
6
+ function prettyFormat(errors, dataVar) {
7
+ const message = errors.map((e) => {
8
+ let line = `${dataVar}${e.instancePath || ''} ${e.message}`
9
+ if (e.code) line += ` [${e.code}]`
10
+ if (e.suggestion && e.suggestion.text) line += ` (${e.suggestion.text})`
11
+ return line
12
+ }).join(', ')
13
+ const err = new Error(message)
14
+ err.statusCode = 400
15
+ return err
16
+ }
17
+
6
18
  function fastifyAta(fastify, opts, done) {
7
19
  const cache = new WeakMap()
8
20
  const hasCoercion = !!(opts.coerceTypes || opts.removeAdditional)
@@ -12,6 +24,10 @@ function fastifyAta(fastify, opts, done) {
12
24
  abortEarly: opts.abortEarly || false,
13
25
  }
14
26
 
27
+ if (opts.prettyErrors) {
28
+ fastify.setSchemaErrorFormatter(prettyFormat)
29
+ }
30
+
15
31
  fastify.setValidatorCompiler(({ schema }) => {
16
32
  let validator = cache.get(schema)
17
33
  if (!validator) {
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "fastify-ata",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Fastify plugin for ata-validator. Runtime-competitive with the default, 24x faster serverless cold start, Standard Schema V1.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
7
  "scripts": {
8
- "test": "node test.js"
8
+ "test": "node test.js && node test-pretty-errors.js && node test-standalone.js && node test-compiler.js && node test-type-provider.js && tsc -p tsconfig.types.json"
9
9
  },
10
10
  "repository": {
11
11
  "type": "git",
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "homepage": "https://github.com/ata-core/fastify-ata#readme",
28
28
  "dependencies": {
29
- "ata-validator": "^0.11.0",
29
+ "ata-validator": "^0.17.0",
30
30
  "fastify-plugin": "^5.1.0",
31
31
  "sanitize-filename": "^1.6.4"
32
32
  },
@@ -34,7 +34,9 @@
34
34
  "fastify": ">=4.0.0"
35
35
  },
36
36
  "devDependencies": {
37
+ "@types/node": "^25.9.1",
37
38
  "autocannon": "^8.0.0",
38
- "fastify": "^5.8.4"
39
+ "fastify": "^5.8.4",
40
+ "typescript": "^6.0.3"
39
41
  }
40
42
  }
package/standalone.js CHANGED
@@ -73,13 +73,7 @@ function wrapValidator(validateFn) {
73
73
  validate.errors = null
74
74
  return true
75
75
  }
76
- validate.errors = result.errors.map(e => ({
77
- message: e.message,
78
- instancePath: e.path || '',
79
- schemaPath: '',
80
- keyword: e.code || 'validation',
81
- params: {},
82
- }))
76
+ validate.errors = result.errors
83
77
  return false
84
78
  }
85
79
  // Boolean result
@@ -0,0 +1,69 @@
1
+ 'use strict'
2
+
3
+ // Tests the @fastify/ajv-compiler-compatible factory so ata can serve as
4
+ // Fastify's GLOBAL default validator (not just a per-route compiler).
5
+
6
+ const Fastify = require('fastify')
7
+ const AtaCompiler = require('./compiler')
8
+
9
+ let pass = 0
10
+ let fail = 0
11
+
12
+ function assert(cond, msg) {
13
+ if (cond) { pass++; console.log(` PASS ${msg}`) }
14
+ else { fail++; console.log(` FAIL ${msg}`) }
15
+ }
16
+
17
+ async function run() {
18
+ console.log('\nfastify-ata compiler (global default) Tests\n')
19
+
20
+ // Factory shape mirrors @fastify/ajv-compiler: factory() -> build(ext, opts) -> compile({schema}) -> validate
21
+ const factory = AtaCompiler()
22
+ const build = factory({}, { customOptions: {} })
23
+ const validate = build({ schema: { type: 'object', properties: { n: { type: 'integer' } }, required: ['n'] } })
24
+ assert(validate({ n: 1 }) === true || (validate({ n: 1 }) && validate({ n: 1 }).value), 'compiler: valid input accepted')
25
+ const bad = validate({})
26
+ assert(bad === false, 'compiler: invalid input rejected')
27
+ assert(validate.errors && validate.errors[0].code, `compiler: rich errors present (got ${JSON.stringify(validate.errors && validate.errors[0])})`)
28
+
29
+ // As Fastify's GLOBAL default via schemaController, with cross-schema $ref
30
+ const app = Fastify({
31
+ schemaController: { compilersFactory: { buildValidator: AtaCompiler() } },
32
+ })
33
+ app.addSchema({ $id: 'addr', type: 'object', properties: { city: { type: 'string' } }, required: ['city'] })
34
+ app.post('/u', {
35
+ schema: {
36
+ body: {
37
+ type: 'object',
38
+ properties: { name: { type: 'string' }, address: { $ref: 'addr#' } },
39
+ required: ['name', 'address'],
40
+ },
41
+ },
42
+ }, (req, reply) => reply.send({ ok: true }))
43
+ await app.ready()
44
+
45
+ const okRes = await app.inject({ method: 'POST', url: '/u', payload: { name: 'M', address: { city: 'IST' } } })
46
+ assert(okRes.statusCode === 200, `compiler: cross-schema $ref valid -> 200 (got ${okRes.statusCode})`)
47
+
48
+ const badRes = await app.inject({ method: 'POST', url: '/u', payload: { name: 'M', address: {} } })
49
+ assert(badRes.statusCode === 400, `compiler: cross-schema $ref invalid -> 400 (got ${badRes.statusCode})`)
50
+ await app.close()
51
+
52
+ // coerceTypes default on (matches Fastify default): querystring "5" -> 5
53
+ const app2 = Fastify({
54
+ schemaController: { compilersFactory: { buildValidator: AtaCompiler() } },
55
+ })
56
+ app2.get('/q', {
57
+ schema: { querystring: { type: 'object', properties: { limit: { type: 'integer' } }, required: ['limit'] } },
58
+ }, (req, reply) => reply.send({ limit: req.query.limit, typ: typeof req.query.limit }))
59
+ await app2.ready()
60
+ const qRes = await app2.inject({ method: 'GET', url: '/q?limit=5' })
61
+ assert(qRes.statusCode === 200, `compiler: coerced querystring -> 200 (got ${qRes.statusCode})`)
62
+ assert(JSON.parse(qRes.payload).typ === 'number', `compiler: querystring coerced to number (got ${qRes.payload})`)
63
+ await app2.close()
64
+
65
+ console.log(`\n${pass}/${pass + fail} tests passed\n`)
66
+ process.exit(fail > 0 ? 1 : 0)
67
+ }
68
+
69
+ run().catch((err) => { console.error(err); process.exit(1) })
@@ -0,0 +1,60 @@
1
+ 'use strict'
2
+
3
+ const fastify = require('fastify')
4
+ const fastifyAta = require('./index')
5
+
6
+ let pass = 0
7
+ let fail = 0
8
+
9
+ function assert(cond, msg) {
10
+ if (cond) { pass++; console.log(` PASS ${msg}`) }
11
+ else { fail++; console.log(` FAIL ${msg}`) }
12
+ }
13
+
14
+ const schema = {
15
+ body: {
16
+ type: 'object',
17
+ properties: {
18
+ name: { type: 'string' },
19
+ age: { type: 'integer' },
20
+ },
21
+ required: ['name'],
22
+ },
23
+ }
24
+
25
+ async function run() {
26
+ console.log('\nfastify-ata prettyErrors Tests\n')
27
+
28
+ // prettyErrors ON: 400 message carries the compiler-grade code + suggestion
29
+ const app = fastify()
30
+ await app.register(fastifyAta, { prettyErrors: true })
31
+ app.post('/u', { schema }, (req, reply) => reply.send({ ok: true }))
32
+ await app.ready()
33
+
34
+ const r = await app.inject({ method: 'POST', url: '/u', payload: { age: 5 } })
35
+ assert(r.statusCode === 400, `prettyErrors: invalid returns 400 (got ${r.statusCode})`)
36
+ const msg = JSON.parse(r.payload).message
37
+ assert(/ATA\d{4}/.test(msg), `prettyErrors: message carries error code (got "${msg}")`)
38
+ assert(msg.includes('did you mean'), `prettyErrors: message carries suggestion (got "${msg}")`)
39
+ await app.close()
40
+
41
+ // prettyErrors OFF (default): message stays AJV-compatible, no ATA code
42
+ const app2 = fastify()
43
+ await app2.register(fastifyAta)
44
+ app2.post('/u', { schema }, (req, reply) => reply.send({ ok: true }))
45
+ await app2.ready()
46
+
47
+ const r2 = await app2.inject({ method: 'POST', url: '/u', payload: { age: 5 } })
48
+ assert(r2.statusCode === 400, `default: invalid returns 400 (got ${r2.statusCode})`)
49
+ const msg2 = JSON.parse(r2.payload).message
50
+ assert(!/ATA\d{4}/.test(msg2), `default: message has no ATA code (got "${msg2}")`)
51
+ await app2.close()
52
+
53
+ console.log(`\n${pass}/${pass + fail} tests passed\n`)
54
+ process.exit(fail > 0 ? 1 : 0)
55
+ }
56
+
57
+ run().catch((err) => {
58
+ console.error(err)
59
+ process.exit(1)
60
+ })
@@ -0,0 +1,41 @@
1
+ 'use strict'
2
+
3
+ const StandaloneValidator = require('./standalone')
4
+
5
+ let pass = 0
6
+ let fail = 0
7
+
8
+ function assert(cond, msg) {
9
+ if (cond) { pass++; console.log(` PASS ${msg}`) }
10
+ else { fail++; console.log(` FAIL ${msg}`) }
11
+ }
12
+
13
+ const schema = {
14
+ type: 'object',
15
+ properties: { name: { type: 'string' }, age: { type: 'integer' } },
16
+ required: ['name'],
17
+ }
18
+
19
+ function run() {
20
+ console.log('\nfastify-ata standalone error passthrough Tests\n')
21
+
22
+ const build = StandaloneValidator({ readMode: false, storeFunction() {} })()
23
+ const validate = build({ schema })
24
+
25
+ assert(validate({ name: 'x', age: 1 }) === true, 'standalone: valid input accepted')
26
+
27
+ const ok = validate({ age: 5 })
28
+ assert(ok === false, 'standalone: invalid input rejected')
29
+
30
+ const e = validate.errors[0]
31
+ assert(e.keyword === 'required', `standalone: keyword is the JSON Schema keyword, not the code (got "${e.keyword}")`)
32
+ assert(e.code === 'ATA7001', `standalone: rich code preserved (got "${e.code}")`)
33
+ assert(e.schemaPath === '#/required', `standalone: schemaPath preserved (got "${e.schemaPath}")`)
34
+ assert(e.params && e.params.missingProperty === 'name', `standalone: params preserved (got ${JSON.stringify(e.params)})`)
35
+ assert(e.suggestion && /did you mean/.test(e.suggestion.text), `standalone: suggestion preserved (got ${JSON.stringify(e.suggestion)})`)
36
+
37
+ console.log(`\n${pass}/${pass + fail} tests passed\n`)
38
+ process.exit(fail > 0 ? 1 : 0)
39
+ }
40
+
41
+ run()
@@ -0,0 +1,30 @@
1
+ 'use strict'
2
+
3
+ const fastify = require('fastify')
4
+ const fastifyAta = require('./index')
5
+ const { defineSchema } = require('ata-validator')
6
+
7
+ let pass = 0, fail = 0
8
+ function assert(cond, msg) { if (cond) { pass++; console.log(' PASS ', msg) } else { fail++; console.log(' FAIL ', msg) } }
9
+
10
+ async function run() {
11
+ console.log('\nfastify-ata type-provider runtime smoke\n')
12
+ const app = fastify().withTypeProvider()
13
+ await app.register(fastifyAta)
14
+ app.post('/u', {
15
+ schema: { body: defineSchema({ type: 'object', properties: { name: { type: 'string' }, age: { type: 'integer' } }, required: ['name'] }) },
16
+ }, (req, reply) => reply.send({ name: req.body.name }))
17
+ await app.ready()
18
+
19
+ const ok = await app.inject({ method: 'POST', url: '/u', payload: { name: 'Mert', age: 26 } })
20
+ assert(ok.statusCode === 200, `valid -> 200 (got ${ok.statusCode})`)
21
+ assert(JSON.parse(ok.payload).name === 'Mert', 'valid -> body usable in handler')
22
+
23
+ const bad = await app.inject({ method: 'POST', url: '/u', payload: { age: 26 } })
24
+ assert(bad.statusCode === 400, `missing required -> 400 (got ${bad.statusCode})`)
25
+ await app.close()
26
+
27
+ console.log(`\n${pass}/${pass + fail} passed\n`)
28
+ process.exit(fail > 0 ? 1 : 0)
29
+ }
30
+ run().catch((e) => { console.error(e); process.exit(1) })
package/test-types.ts ADDED
@@ -0,0 +1,38 @@
1
+ // Type-level test. Compiled with `tsc -p tsconfig.types.json` (no emit).
2
+ // A type error here = failure. Asserts the type provider narrows request types.
3
+ import Fastify from 'fastify'
4
+ import { defineSchema } from 'ata-validator'
5
+ import fastifyAta = require('./index')
6
+ type AtaTypeProvider = fastifyAta.AtaTypeProvider
7
+
8
+ // exact-type equality helper
9
+ type Expect<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false
10
+
11
+ const app = Fastify().withTypeProvider<AtaTypeProvider>()
12
+
13
+ app.post('/users', {
14
+ schema: {
15
+ body: defineSchema({
16
+ type: 'object',
17
+ properties: { name: { type: 'string' }, age: { type: 'integer' } },
18
+ required: ['name'],
19
+ }),
20
+ },
21
+ }, async (req) => {
22
+ // body must narrow: name required string, age optional number
23
+ const _name: string = req.body.name
24
+ const _age: number | undefined = req.body.age
25
+ const _exact: Expect<typeof req.body, { name: string; age?: number }> = true
26
+ void _name; void _age; void _exact
27
+ return { ok: true }
28
+ })
29
+
30
+ app.get('/search', {
31
+ schema: {
32
+ querystring: defineSchema({ type: 'object', properties: { q: { type: 'string' } }, required: ['q'] }),
33
+ },
34
+ }, async (req) => {
35
+ const _q: string = req.query.q
36
+ void _q
37
+ return { ok: true }
38
+ })
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "moduleResolution": "node",
6
+ "ignoreDeprecations": "6.0",
7
+ "esModuleInterop": true,
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "skipLibCheck": true,
11
+ "types": ["node"],
12
+ "lib": ["ES2022"]
13
+ },
14
+ "include": ["test-types.ts"],
15
+ "exclude": ["node_modules"]
16
+ }