fast-json-stringify 6.2.0 → 6.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.
@@ -27,7 +27,7 @@ jobs:
27
27
  permissions:
28
28
  contents: write
29
29
  pull-requests: write
30
- uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
30
+ uses: fastify/workflows/.github/workflows/plugins-ci.yml@v6
31
31
  with:
32
32
  license-check: true
33
33
  lint: true
@@ -0,0 +1,19 @@
1
+ name: Lock Threads
2
+
3
+ on:
4
+ schedule:
5
+ - cron: '0 0 1 * *'
6
+ workflow_dispatch:
7
+
8
+ concurrency:
9
+ group: lock
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ lock-threads:
16
+ permissions:
17
+ issues: write
18
+ pull-requests: write
19
+ uses: fastify/workflows/.github/workflows/lock-threads.yml@v6
package/LICENSE CHANGED
@@ -1,9 +1,7 @@
1
1
  MIT License
2
2
 
3
3
  Copyright (c) 2016-present Matteo Collina
4
- Copyright (c) 2016-present The Fastify team
5
-
6
- The Fastify team members are listed at https://github.com/fastify/fastify#team.
4
+ Copyright (c) 2016-present The Fastify team <https://github.com/fastify/fastify#team>
7
5
 
8
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
9
7
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -79,7 +79,6 @@ compile-json-stringify date format x 803,214 ops/sec ±0.23% (97 runs sampled)
79
79
 
80
80
 
81
81
  <a name="example"></a>
82
- Try it out on RunKit: <a href="https://runkit.com/npm/fast-json-stringify">https://runkit.com/npm/fast-json-stringify</a>
83
82
  ## Example
84
83
 
85
84
  ```js
@@ -347,9 +346,9 @@ const stringify = fastJson({
347
346
  properties: {
348
347
  'undecidedType': {
349
348
  'anyOf': [{
350
- type: 'string'
349
+ type: 'string'
351
350
  }, {
352
- type: 'boolean'
351
+ type: 'boolean'
353
352
  }]
354
353
  }
355
354
  }
@@ -645,8 +644,8 @@ const stringify = fastJson({
645
644
  type: 'object',
646
645
  properties: {
647
646
  'code': {
648
- type: 'string',
649
- format 'unsafe'
647
+ type: 'string',
648
+ format: 'unsafe'
650
649
  }
651
650
  }
652
651
  })
@@ -84,13 +84,13 @@ const arraySchemaAJVJTD = {
84
84
  const dateFormatSchema = {
85
85
  description: 'Date of birth',
86
86
  type: 'string',
87
- format: 'date'
87
+ format: 'datetime'
88
88
  }
89
89
 
90
90
  const dateFormatSchemaCJS = {
91
91
  description: 'Date of birth',
92
92
  type: 'string',
93
- format: 'date'
93
+ format: 'datetime'
94
94
  }
95
95
 
96
96
  const obj = {
@@ -169,155 +169,171 @@ for (let i = STR_LEN; i < LARGE_ARRAY_SIZE; ++i) {
169
169
  }
170
170
  }
171
171
 
172
- Number(str)
173
-
174
172
  for (let i = 0; i < MULTI_ARRAY_LENGTH; i++) {
175
173
  multiArray[i] = obj
176
174
  }
177
175
 
178
- suite.add('FJS creation', function () {
176
+ suite.add('fast-json-stringify: creation', function () {
179
177
  FJS(schema)
180
178
  })
181
- suite.add('CJS creation', function () {
179
+ suite.add('compile-json-stringify: creation', function () {
182
180
  CJS(schemaCJS)
183
181
  })
184
- suite.add('AJV Serialize creation', function () {
182
+ suite.add('AJV: creation', function () {
185
183
  ajv.compileSerializer(schemaAJVJTD)
186
184
  })
187
- suite.add('json-accelerator creation', function () {
185
+ suite.add('json-accelerator: creation', function () {
188
186
  createAccelerator(schema)
189
187
  })
190
188
 
191
- suite.add('JSON.stringify array', function () {
189
+ suite.add('JSON.stringify: array', function () {
192
190
  JSON.stringify(multiArray)
193
191
  })
194
192
 
195
- suite.add('fast-json-stringify array default', function () {
193
+ suite.add('fast-json-stringify [default]: array', function () {
196
194
  stringifyArrayDefault(multiArray)
197
195
  })
198
196
 
199
- suite.add('json-accelerator array', function () {
197
+ suite.add('json-accelerator: array', function () {
200
198
  accelArray(multiArray)
201
199
  })
202
200
 
203
- suite.add('fast-json-stringify array json-stringify', function () {
201
+ suite.add('fast-json-stringify [json-stringify]: array', function () {
204
202
  stringifyArrayJSONStringify(multiArray)
205
203
  })
206
204
 
207
- suite.add('compile-json-stringify array', function () {
205
+ suite.add('compile-json-stringify: array', function () {
208
206
  CJSStringifyArray(multiArray)
209
207
  })
210
208
 
211
- suite.add('AJV Serialize array', function () {
209
+ suite.add('AJV: array', function () {
212
210
  ajvSerializeArray(multiArray)
213
211
  })
214
212
 
215
- suite.add('JSON.stringify large array', function () {
213
+ suite.add('JSON.stringify: large array', function () {
216
214
  JSON.stringify(largeArray)
217
215
  })
218
216
 
219
- suite.add('fast-json-stringify large array default', function () {
217
+ suite.add('fast-json-stringify [default]: large array', function () {
220
218
  stringifyArrayDefault(largeArray)
221
219
  })
222
220
 
223
- suite.add('fast-json-stringify large array json-stringify', function () {
221
+ suite.add('fast-json-stringify [json-stringify]: large array', function () {
224
222
  stringifyArrayJSONStringify(largeArray)
225
223
  })
226
224
 
227
- suite.add('compile-json-stringify large array', function () {
225
+ suite.add('compile-json-stringify: large array', function () {
228
226
  CJSStringifyArray(largeArray)
229
227
  })
230
228
 
231
- suite.add('AJV Serialize large array', function () {
229
+ suite.add('AJV: large array', function () {
232
230
  ajvSerializeArray(largeArray)
233
231
  })
234
232
 
235
- suite.add('JSON.stringify long string', function () {
233
+ suite.add('JSON.stringify: long string', function () {
236
234
  JSON.stringify(str)
237
235
  })
238
236
 
239
- suite.add('fast-json-stringify long string', function () {
237
+ suite.add('fast-json-stringify: long string', function () {
240
238
  stringifyString(str)
241
239
  })
242
240
 
243
- suite.add('json-accelerator long string', function () {
241
+ suite.add('json-accelerator: long string', function () {
244
242
  stringifyString(str)
245
243
  })
246
244
 
247
- suite.add('compile-json-stringify long string', function () {
245
+ suite.add('compile-json-stringify: long string', function () {
248
246
  CJSStringifyString(str)
249
247
  })
250
248
 
251
- suite.add('AJV Serialize long string', function () {
249
+ suite.add('AJV: long string', function () {
252
250
  ajvSerializeString(str)
253
251
  })
254
252
 
255
- suite.add('JSON.stringify short string', function () {
253
+ suite.add('JSON.stringify: short string', function () {
256
254
  JSON.stringify('hello world')
257
255
  })
258
256
 
259
- suite.add('fast-json-stringify short string', function () {
257
+ suite.add('fast-json-stringify: short string', function () {
260
258
  stringifyString('hello world')
261
259
  })
262
260
 
263
- suite.add('json-accelerator short string', function () {
261
+ suite.add('json-accelerator: short string', function () {
264
262
  accelString('hello world')
265
263
  })
266
264
 
267
- suite.add('compile-json-stringify short string', function () {
265
+ suite.add('compile-json-stringify: short string', function () {
268
266
  CJSStringifyString('hello world')
269
267
  })
270
268
 
271
- suite.add('AJV Serialize short string', function () {
269
+ suite.add('AJV: short string', function () {
272
270
  ajvSerializeString('hello world')
273
271
  })
274
272
 
275
- suite.add('JSON.stringify obj', function () {
273
+ suite.add('JSON.stringify: obj', function () {
276
274
  JSON.stringify(obj)
277
275
  })
278
276
 
279
- suite.add('fast-json-stringify obj', function () {
277
+ suite.add('fast-json-stringify: obj', function () {
280
278
  stringify(obj)
281
279
  })
282
280
 
283
- suite.add('json-accelerator obj', function () {
281
+ suite.add('json-accelerator: obj', function () {
284
282
  accelStringify(obj)
285
283
  })
286
284
 
287
- suite.add('compile-json-stringify obj', function () {
285
+ suite.add('compile-json-stringify: obj', function () {
288
286
  CJSStringify(obj)
289
287
  })
290
288
 
291
- suite.add('AJV Serialize obj', function () {
289
+ suite.add('AJV: obj', function () {
292
290
  ajvSerialize(obj)
293
291
  })
294
292
 
295
- suite.add('JSON stringify date', function () {
293
+ suite.add('JSON.stringify: date', function () {
296
294
  JSON.stringify(date)
297
295
  })
298
296
 
299
- suite.add('fast-json-stringify date format', function () {
297
+ suite.add('fast-json-stringify: date', function () {
300
298
  stringifyDate(date)
301
299
  })
302
300
 
303
- suite.add('json-accelerate date format', function () {
301
+ suite.add('json-accelerate: date', function () {
304
302
  accelDate(date)
305
303
  })
306
304
 
307
- suite.add('compile-json-stringify date format', function () {
305
+ suite.add('compile-json-stringify: date', function () {
308
306
  CJSStringifyDate(date)
309
307
  })
310
308
 
311
309
  suite.run().then(() => {
312
- for (const task of suite.tasks) {
313
- const hz = task.result.hz // ops/sec
314
- const rme = task.result.rme // relative margin of error (%)
315
- const samples = task.result.samples.length
310
+ const results = suite.tasks.map(task => ({
311
+ name: task.name,
312
+ hz: task.result.throughput.mean,
313
+ rme: task.result.latency.rme,
314
+ samples: task.result.latency.df + 1
315
+ }))
316
+
317
+ const scenarios = {}
318
+ for (const result of results) {
319
+ const [library, scenario] = result.name.split(':').map(s => s.trim())
320
+
321
+ if (!scenarios[scenario]) scenarios[scenario] = []
322
+ scenarios[scenario].push({ ...result, library })
323
+ }
316
324
 
317
- const formattedHz = hz.toLocaleString('en-US', { maximumFractionDigits: 0 })
318
- const formattedRme = rme.toFixed(2)
325
+ for (const [scenario, tasks] of Object.entries(scenarios)) {
326
+ console.log(`\n--- ${scenario} ---`)
327
+ const sorted = tasks.sort((a, b) => b.hz - a.hz)
328
+ const winner = sorted[0]
319
329
 
320
- const output = `${task.name} x ${formattedHz} ops/sec ±${formattedRme}% (${samples} runs sampled)`
321
- console.log(output)
330
+ for (const task of sorted) {
331
+ const formattedHz = task.hz.toLocaleString('en-US', { maximumFractionDigits: 0 })
332
+ const formattedRme = task.rme.toFixed(2)
333
+ const isWinner = task === winner
334
+ const prefix = isWinner ? '🏆 ' : ' '
335
+
336
+ console.log(`${prefix}${task.library.padEnd(40)} x ${formattedHz.padStart(15)} ops/sec ±${formattedRme}% (${task.samples} runs sampled)`)
337
+ }
322
338
  }
323
339
  }).catch(err => console.error(`Error: ${err.message}`))
@@ -21,9 +21,9 @@ bench.add(benchmark.name, () => {
21
21
  stringify(benchmark.input)
22
22
  }).run().then(() => {
23
23
  const task = bench.tasks[0]
24
- const hz = task.result.hz // ops/sec
25
- const rme = task.result.rme // relative margin of error (%)
26
- const samples = task.result.samples.length
24
+ const hz = task.result.throughput.mean // ops/sec
25
+ const rme = task.result.latency.rme // relative margin of error (%)
26
+ const samples = task.result.latency.df + 1 // degrees of freedom + 1 = sample count
27
27
 
28
28
  const formattedHz = hz.toLocaleString('en-US', { maximumFractionDigits: 0 })
29
29
  const formattedRme = rme.toFixed(2)
package/index.js CHANGED
@@ -304,14 +304,14 @@ function buildExtraObjectPropertiesSerializer (context, location, addComma, objV
304
304
  const propertiesKeys = Object.keys(schema.properties || {})
305
305
 
306
306
  let code = `
307
- const propertiesKeys = ${JSON.stringify(propertiesKeys)}
308
- for (const [key, value] of Object.entries(${objVar})) {
307
+ for (const key of Object.keys(${objVar})) {
309
308
  if (
310
- propertiesKeys.includes(key) ||
311
- value === undefined ||
312
- typeof value === 'function' ||
313
- typeof value === 'symbol'
309
+ ${propertiesKeys.length > 0 ? propertiesKeys.map(k => `key === ${JSON.stringify(k)}`).join(' || ') + ' ||' : ''}
310
+ ${objVar}[key] === undefined ||
311
+ typeof ${objVar}[key] === 'function' ||
312
+ typeof ${objVar}[key] === 'symbol'
314
313
  ) continue
314
+ const value = ${objVar}[key]
315
315
  `
316
316
 
317
317
  const patternPropertiesLocation = location.getPropertyLocation('patternProperties')
@@ -385,45 +385,99 @@ function buildInnerObject (context, location, objVar) {
385
385
 
386
386
  const localUid = context.uid++
387
387
  let addComma = ''
388
- const needsRuntimeComma = propertiesKeys.length > 1 || schema.patternProperties || (schema.additionalProperties !== undefined && schema.additionalProperties !== false)
389
388
 
390
- if (needsRuntimeComma) {
391
- code += `let addComma_${localUid} = false\n`
392
- addComma = `!addComma_${localUid} && (addComma_${localUid} = true) || (json += JSON_STR_COMMA)`
393
- }
389
+ if (requiredProperties.length > 0) {
390
+ // If we have required properties, we know that at least one property will be serialized.
391
+ // We can avoid the runtime check for the comma.
394
392
 
395
- for (const key of propertiesKeys) {
396
- let propertyLocation = propertiesLocation.getPropertyLocation(key)
397
- if (propertyLocation.schema.$ref) {
398
- propertyLocation = resolveRef(context, propertyLocation)
399
- }
393
+ // The first property is required, so we don't need a comma.
394
+ // For the subsequent properties, we can blindly add a comma.
400
395
 
401
- const sanitizedKey = JSON.stringify(key)
402
- const value = `value_${key.replace(/[^a-zA-Z0-9]/g, '_')}_${context.uid++}`
403
- const defaultValue = propertyLocation.schema.default
404
- const isRequired = requiredProperties.includes(key)
396
+ for (let i = 0; i < propertiesKeys.length; i++) {
397
+ const key = propertiesKeys[i]
398
+ const propertyLocation = propertiesLocation.getPropertyLocation(key)
405
399
 
406
- code += `
400
+ let resolvedLocation = propertyLocation
401
+ if (propertyLocation.schema.$ref) {
402
+ resolvedLocation = resolveRef(context, propertyLocation)
403
+ }
404
+
405
+ const sanitizedKey = JSON.stringify(key)
406
+ const value = `value_${key.replace(/[^a-zA-Z0-9]/g, '_')}_${context.uid++}`
407
+ const defaultValue = resolvedLocation.schema.default
408
+ const isRequired = requiredProperties.includes(key)
409
+
410
+ // i === 0 means it's the first property, and it IS required (due to sort). No comma.
411
+ // i > 0 means it follows a required property (or a sequence starting with one). Unconditional comma.
412
+ const currentAddComma = i === 0 ? '' : 'json += JSON_STR_COMMA'
413
+
414
+ code += `
407
415
  const ${value} = ${objVar}[${sanitizedKey}]
408
416
  if (${value} !== undefined) {
409
- ${addComma}
417
+ ${currentAddComma}
410
418
  json += ${JSON.stringify(sanitizedKey + ':')}
411
- ${buildValue(context, propertyLocation, `${value}`)}
419
+ ${buildValue(context, resolvedLocation, `${value}`)}
412
420
  }`
413
421
 
414
- if (defaultValue !== undefined) {
415
- code += ` else {
416
- ${addComma}
422
+ if (defaultValue !== undefined) {
423
+ code += ` else {
424
+ ${currentAddComma}
417
425
  json += ${JSON.stringify(sanitizedKey + ':' + JSON.stringify(defaultValue))}
418
426
  }
419
427
  `
420
- } else if (isRequired) {
421
- code += ` else {
428
+ } else if (isRequired) {
429
+ code += ` else {
422
430
  throw new Error('${sanitizedKey.replace(/'/g, '\\\'')} is required!')
423
431
  }
424
432
  `
425
- } else {
426
- code += '\n'
433
+ } else {
434
+ code += '\n'
435
+ }
436
+ }
437
+
438
+ addComma = 'json += JSON_STR_COMMA'
439
+ } else {
440
+ const needsRuntimeComma = propertiesKeys.length > 1 || schema.patternProperties || (schema.additionalProperties !== undefined && schema.additionalProperties !== false)
441
+
442
+ if (needsRuntimeComma) {
443
+ code += `let addComma_${localUid} = false\n`
444
+ addComma = `!addComma_${localUid} && (addComma_${localUid} = true) || (json += JSON_STR_COMMA)`
445
+ }
446
+
447
+ for (const key of propertiesKeys) {
448
+ let propertyLocation = propertiesLocation.getPropertyLocation(key)
449
+ if (propertyLocation.schema.$ref) {
450
+ propertyLocation = resolveRef(context, propertyLocation)
451
+ }
452
+
453
+ const sanitizedKey = JSON.stringify(key)
454
+ const value = `value_${key.replace(/[^a-zA-Z0-9]/g, '_')}_${context.uid++}`
455
+ const defaultValue = propertyLocation.schema.default
456
+ const isRequired = requiredProperties.includes(key) // Should be false here but good to keep
457
+
458
+ code += `
459
+ const ${value} = ${objVar}[${sanitizedKey}]
460
+ if (${value} !== undefined) {
461
+ ${addComma}
462
+ json += ${JSON.stringify(sanitizedKey + ':')}
463
+ ${buildValue(context, propertyLocation, `${value}`)}
464
+ }`
465
+
466
+ if (defaultValue !== undefined) {
467
+ code += ` else {
468
+ ${addComma}
469
+ json += ${JSON.stringify(sanitizedKey + ':' + JSON.stringify(defaultValue))}
470
+ }
471
+ `
472
+ } else if (isRequired) {
473
+ // Should not happen if requiredProperties.length === 0 but safety
474
+ code += ` else {
475
+ throw new Error('${sanitizedKey.replace(/'/g, '\\\'')} is required!')
476
+ }
477
+ `
478
+ } else {
479
+ code += '\n'
480
+ }
427
481
  }
428
482
  }
429
483
 
@@ -514,7 +568,7 @@ function buildObject (context, location, input) {
514
568
  const jsonPointer = location.jsonPointer || ''
515
569
  const fullPath = `${schemaId}#${jsonPointer}`
516
570
 
517
- if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema)) {
571
+ if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema) || schemaId !== '') {
518
572
  const functionName = generateFuncName(context)
519
573
  context.functionsNamesBySchema.set(schema, functionName)
520
574
 
@@ -573,7 +627,7 @@ function buildArray (context, location, input) {
573
627
  const jsonPointer = location.jsonPointer || ''
574
628
  const fullPath = `${schemaId}#${jsonPointer}`
575
629
 
576
- if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema)) {
630
+ if (context.recursivePaths.has(fullPath) || context.buildingSet.has(schema) || schemaId !== '') {
577
631
  const functionName = generateFuncName(context)
578
632
  context.functionsNamesBySchema.set(schema, functionName)
579
633
 
@@ -611,10 +665,15 @@ function buildArray (context, location, input) {
611
665
 
612
666
  if (Array.isArray(itemsSchema)) {
613
667
  for (let i = 0, itemsSchemaLength = itemsSchema.length; i < itemsSchemaLength; i++) {
614
- const item = itemsSchema[i]
668
+ let item = itemsSchema[i]
669
+ let itemLocation = itemsLocation.getPropertyLocation(i)
670
+ if (itemLocation.schema.$ref) {
671
+ itemLocation = resolveRef(context, itemLocation)
672
+ item = itemLocation.schema
673
+ }
615
674
  const value = `value_${i}`
616
675
  functionCode += `const ${value} = obj[${i}]`
617
- const tmpRes = buildValue(context, itemsLocation.getPropertyLocation(i), value)
676
+ const tmpRes = buildValue(context, itemLocation, value)
618
677
  functionCode += `
619
678
  if (${i} < arrayLength) {
620
679
  if (${buildArrayTypeCondition(item.type, value)}) {
@@ -641,12 +700,14 @@ function buildArray (context, location, input) {
641
700
  } else {
642
701
  const code = buildValue(context, itemsLocation, 'value')
643
702
  functionCode += `
644
- for (let i = 0; i < arrayLength; i++) {
645
- if (i) {
703
+ if (arrayLength > 0) {
704
+ const value = obj[0]
705
+ ${code}
706
+ for (let i = 1; i < arrayLength; i++) {
646
707
  json += JSON_STR_COMMA
708
+ const value = obj[i]
709
+ ${code}
647
710
  }
648
- const value = obj[i]
649
- ${code}
650
711
  }`
651
712
  }
652
713
 
@@ -691,10 +752,15 @@ function buildArray (context, location, input) {
691
752
  const localUid = context.uid++
692
753
  inlinedCode += `let addComma_${localUid} = false\n`
693
754
  for (let i = 0, itemsSchemaLength = itemsSchema.length; i < itemsSchemaLength; i++) {
694
- const item = itemsSchema[i]
755
+ let item = itemsSchema[i]
756
+ let itemLocation = itemsLocation.getPropertyLocation(i)
757
+ if (itemLocation.schema.$ref) {
758
+ itemLocation = resolveRef(context, itemLocation)
759
+ item = itemLocation.schema
760
+ }
695
761
  const value = `value_${i}_${context.uid++}`
696
762
  inlinedCode += `const ${value} = ${objVar}[${i}]`
697
- const tmpRes = buildValue(context, itemsLocation.getPropertyLocation(i), value)
763
+ const tmpRes = buildValue(context, itemLocation, value)
698
764
  inlinedCode += `
699
765
  if (${i} < arrayLength_${objVar}) {
700
766
  if (${buildArrayTypeCondition(item.type, value)}) {
@@ -717,12 +783,14 @@ function buildArray (context, location, input) {
717
783
  } else {
718
784
  const code = buildValue(context, itemsLocation, 'value')
719
785
  inlinedCode += `
720
- for (let i = 0; i < arrayLength_${objVar}; i++) {
721
- if (i) {
786
+ if (arrayLength_${objVar} > 0) {
787
+ const value = ${objVar}[0]
788
+ ${code}
789
+ for (let i = 1; i < arrayLength_${objVar}; i++) {
722
790
  json += JSON_STR_COMMA
791
+ const value = ${objVar}[i]
792
+ ${code}
723
793
  }
724
- const value = ${objVar}[i]
725
- ${code}
726
794
  }`
727
795
  }
728
796
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fast-json-stringify",
3
- "version": "6.2.0",
3
+ "version": "6.4.0",
4
4
  "description": "Stringify your JSON at max speed",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -12,7 +12,7 @@
12
12
  "benchmark": "node --expose-gc ./benchmark/bench-cmp-lib.js",
13
13
  "lint": "eslint",
14
14
  "lint:fix": "eslint --fix",
15
- "test:typescript": "tsd",
15
+ "test:typescript": "tstyche",
16
16
  "test:unit": "c8 node --test",
17
17
  "test": "npm run test:unit && npm run test:typescript"
18
18
  },
@@ -62,19 +62,20 @@
62
62
  }
63
63
  ],
64
64
  "devDependencies": {
65
- "@sinclair/typebox": "^0.34.3",
66
- "c8": "^10.1.2",
65
+ "c8": "^11.0.0",
67
66
  "cli-select": "^1.1.2",
68
67
  "compile-json-stringify": "^0.1.2",
69
- "eslint": "^9.17.0",
68
+ "eslint": "^9.39.2",
70
69
  "fast-json-stringify": ".",
71
70
  "is-my-json-valid": "^2.20.6",
72
- "json-accelerator": "^0.0.2",
73
- "neostandard": "^0.12.0",
74
- "simple-git": "^3.23.0",
75
- "tinybench": "^5.0.1",
76
- "tsd": "^0.32.0",
77
- "webpack": "^5.90.3"
71
+ "json-accelerator": "^0.1.7",
72
+ "neostandard": "^0.13.0",
73
+ "simple-git": "^3.30.0",
74
+ "tinybench": "^6.0.0",
75
+ "tstyche": "^7.1.0",
76
+ "typebox": "^1.0.81",
77
+ "typescript": "~6.0.2",
78
+ "webpack": "^5.104.1"
78
79
  },
79
80
  "dependencies": {
80
81
  "@fastify/merge-json-schemas": "^0.2.0",
@@ -790,3 +790,23 @@ test('should build merged schemas twice', (t) => {
790
790
  t.assert.equal(stringify({ enums: 'BAR' }), '{"enums":"BAR"}')
791
791
  }
792
792
  })
793
+
794
+ test('invalid anyOf schema', (t) => {
795
+ t.plan(1)
796
+
797
+ const schema = {
798
+ type: 'object',
799
+ properties: {
800
+ prop: {
801
+ anyOf: 'not array' // invalid, anyOf must be array
802
+ }
803
+ }
804
+ }
805
+
806
+ try {
807
+ build(schema)
808
+ t.assert.fail('Should throw')
809
+ } catch (err) {
810
+ t.assert.ok(err.message.includes('schema is invalid'))
811
+ }
812
+ })
@@ -61,6 +61,34 @@ buildTest({
61
61
  dates: [new Date(1), new Date(2)]
62
62
  })
63
63
 
64
+ buildTest({
65
+ title: 'dates tuple $ref',
66
+ definitions: {
67
+ dateTime: {
68
+ type: 'string',
69
+ format: 'date-time'
70
+ }
71
+ },
72
+ type: 'object',
73
+ properties: {
74
+ dates: {
75
+ type: 'array',
76
+ minItems: 2,
77
+ maxItems: 2,
78
+ items: [
79
+ {
80
+ $ref: '#/definitions/dateTime'
81
+ },
82
+ {
83
+ $ref: '#/definitions/dateTime'
84
+ }
85
+ ]
86
+ }
87
+ }
88
+ }, {
89
+ dates: [new Date(1), new Date(2)]
90
+ })
91
+
64
92
  buildTest({
65
93
  title: 'string array',
66
94
  type: 'object',
@@ -358,6 +386,28 @@ test('array items is a schema and additionalItems is false', (t) => {
358
386
  t.assert.equal(validate({ foo: ['foo', 'bar'] }), true)
359
387
  })
360
388
 
389
+ test('array items is a list of schema and additionalItems is a schema', (t) => {
390
+ t.plan(1)
391
+
392
+ const schema = {
393
+ type: 'object',
394
+ properties: {
395
+ foo: {
396
+ type: 'array',
397
+ items: [
398
+ { type: 'string' }
399
+ ],
400
+ additionalItems: { type: 'number' }
401
+ }
402
+ }
403
+ }
404
+
405
+ const stringify = build(schema)
406
+ const result = stringify({ foo: ['foo', 42] })
407
+
408
+ t.assert.equal(result, '{"foo":["foo",42]}')
409
+ })
410
+
361
411
  // https://github.com/fastify/fast-json-stringify/issues/279
362
412
  test('object array with anyOf and symbol', (t) => {
363
413
  t.plan(1)
@@ -119,3 +119,24 @@ test('debug should restore the same serializer instance', t => {
119
119
  const compiled = fjs.restore(debugMode)
120
120
  t.assert.equal(compiled(3.95), 4)
121
121
  })
122
+
123
+ test('Serializer restoreFromState', t => {
124
+ t.plan(1)
125
+
126
+ const Serializer = require('../lib/serializer')
127
+ const serializer = new Serializer()
128
+ const state = serializer.getState()
129
+ const restored = Serializer.restoreFromState(state)
130
+ t.assert.ok(restored instanceof Serializer)
131
+ })
132
+
133
+ test('Validator restoreFromState', t => {
134
+ t.plan(1)
135
+
136
+ const Validator = require('../lib/validator')
137
+ const validator = new Validator()
138
+ validator.addSchema({ type: 'string' }, 'test')
139
+ const state = validator.getState()
140
+ const restored = Validator.restoreFromState(state)
141
+ t.assert.ok(restored instanceof Validator)
142
+ })
@@ -466,3 +466,75 @@ test('external recursive if/then/else', (t) => {
466
466
  const stringify = build(schema, { schema: { externalSchema } })
467
467
  t.assert.equal(stringify(data), '{"a":{"base":"a","bar":"42"},"b":{"base":"b","baz":"43"}}')
468
468
  })
469
+
470
+ test('invalid if-then-else schema', (t) => {
471
+ t.plan(1)
472
+
473
+ const schema = {
474
+ type: 'object',
475
+ if: {
476
+ type: 'object',
477
+ properties: {
478
+ kind: { type: 'string', enum: ['foobar'] }
479
+ }
480
+ },
481
+ then: {
482
+ type: 'object',
483
+ properties: {
484
+ foo: { type: 'string' }
485
+ }
486
+ },
487
+ else: {
488
+ type: 'object',
489
+ properties: 'invalid' // properties must be object
490
+ }
491
+ }
492
+
493
+ try {
494
+ build(schema)
495
+ t.assert.fail('Should throw')
496
+ } catch (err) {
497
+ t.assert.ok(err.message.includes('schema is invalid'))
498
+ }
499
+ })
500
+
501
+ test('if-then-else with allOf', (t) => {
502
+ t.plan(2)
503
+
504
+ const schema = {
505
+ type: 'object',
506
+ allOf: [
507
+ {
508
+ type: 'object',
509
+ properties: {
510
+ base: { type: 'string' }
511
+ }
512
+ }
513
+ ],
514
+ if: {
515
+ type: 'object',
516
+ properties: {
517
+ kind: { type: 'string', enum: ['foobar'] }
518
+ }
519
+ },
520
+ then: {
521
+ type: 'object',
522
+ properties: {
523
+ foo: { type: 'string' }
524
+ }
525
+ },
526
+ else: {
527
+ type: 'object',
528
+ properties: {
529
+ bar: { type: 'string' }
530
+ }
531
+ }
532
+ }
533
+
534
+ const stringify = build(schema)
535
+ const data = { base: 'test', kind: 'foobar', foo: 'value' }
536
+ const output = stringify(data)
537
+
538
+ t.assert.doesNotThrow(() => JSON.parse(output))
539
+ t.assert.equal(output, '{"base":"test","foo":"value"}')
540
+ })
@@ -16,3 +16,23 @@ test('Should throw on invalid schema', t => {
16
16
  })
17
17
  }, { message: /^"invalid" schema is invalid:.*/ })
18
18
  })
19
+
20
+ test('invalid not schema', (t) => {
21
+ t.plan(1)
22
+
23
+ const schema = {
24
+ type: 'object',
25
+ properties: {
26
+ prop: {
27
+ not: 'not object' // invalid, not must be object
28
+ }
29
+ }
30
+ }
31
+
32
+ try {
33
+ build(schema)
34
+ t.assert.fail('Should throw')
35
+ } catch (err) {
36
+ t.assert.ok(err.message.includes('schema is invalid'))
37
+ }
38
+ })
@@ -0,0 +1,183 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('node:test')
4
+ const build = require('..')
5
+
6
+ test('external $ref schema should be reused, not inlined at every reference', (t) => {
7
+ t.plan(3)
8
+
9
+ const contactSchema = {
10
+ $id: 'contact.json',
11
+ type: 'object',
12
+ properties: {
13
+ firstName: { type: 'string' },
14
+ lastName: { type: 'string' },
15
+ email: { type: 'string' }
16
+ }
17
+ }
18
+
19
+ // Schema referencing the same external $ref multiple times
20
+ const parentSchema = {
21
+ type: 'object',
22
+ properties: {
23
+ owner: { $ref: 'contact.json' },
24
+ assignee: { $ref: 'contact.json' },
25
+ reporter: { $ref: 'contact.json' }
26
+ }
27
+ }
28
+
29
+ const serializer = build(parentSchema, { schema: { 'contact.json': contactSchema } })
30
+ const code = serializer.toString()
31
+
32
+ // The serialized function should contain extracted anonymous functions
33
+ // rather than inlining the full contact schema at every reference.
34
+ // With the fix, `firstName` appears 0 times in the main serializer body
35
+ // (it is inside the extracted function, which is not included in .toString())
36
+ // or at most 1 time if in a single extracted function.
37
+ const firstNameMatches = code.match(/firstName/g)
38
+ t.assert.ok(firstNameMatches === null || firstNameMatches.length <= 1,
39
+ `firstName should appear at most once (extracted function), got ${firstNameMatches ? firstNameMatches.length : 0}`)
40
+
41
+ // Verify correct serialization
42
+ const data = {
43
+ owner: { firstName: 'John', lastName: 'Doe', email: 'john@example.com' },
44
+ assignee: { firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com' },
45
+ reporter: { firstName: 'Bob', lastName: 'Jones', email: 'bob@example.com' }
46
+ }
47
+
48
+ const output = serializer(data)
49
+ t.assert.doesNotThrow(() => JSON.parse(output))
50
+ t.assert.equal(output, JSON.stringify(data))
51
+ })
52
+
53
+ test('external $ref schema reused with anyOf wrapping', (t) => {
54
+ t.plan(2)
55
+
56
+ const contactSchema = {
57
+ $id: 'contact.json',
58
+ type: 'object',
59
+ properties: {
60
+ firstName: { type: 'string' },
61
+ lastName: { type: 'string' },
62
+ email: { type: 'string' }
63
+ }
64
+ }
65
+
66
+ // Common pattern: anyOf wrapping $ref for polymorphic fields (populated object OR string ID OR null)
67
+ const parentSchema = {
68
+ type: 'object',
69
+ properties: {
70
+ owner: {
71
+ anyOf: [{ type: ['string', 'null'] }, { $ref: 'contact.json' }]
72
+ },
73
+ assignee: {
74
+ anyOf: [{ type: ['string', 'null'] }, { $ref: 'contact.json' }]
75
+ },
76
+ reporter: {
77
+ anyOf: [{ type: ['string', 'null'] }, { $ref: 'contact.json' }]
78
+ }
79
+ }
80
+ }
81
+
82
+ const serializer = build(parentSchema, { schema: { 'contact.json': contactSchema } })
83
+
84
+ // Serialize with populated objects
85
+ const data = {
86
+ owner: { firstName: 'John', lastName: 'Doe', email: 'john@example.com' },
87
+ assignee: { firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com' },
88
+ reporter: { firstName: 'Bob', lastName: 'Jones', email: 'bob@example.com' }
89
+ }
90
+
91
+ const output = serializer(data)
92
+ t.assert.doesNotThrow(() => JSON.parse(output))
93
+ t.assert.equal(output, JSON.stringify(data))
94
+ })
95
+
96
+ test('external $ref schema reused in array items', (t) => {
97
+ t.plan(2)
98
+
99
+ const contactSchema = {
100
+ $id: 'contact.json',
101
+ type: 'object',
102
+ properties: {
103
+ firstName: { type: 'string' },
104
+ lastName: { type: 'string' }
105
+ }
106
+ }
107
+
108
+ const parentSchema = {
109
+ type: 'object',
110
+ properties: {
111
+ contacts: {
112
+ type: 'array',
113
+ items: { $ref: 'contact.json' }
114
+ },
115
+ primary: { $ref: 'contact.json' },
116
+ secondary: { $ref: 'contact.json' }
117
+ }
118
+ }
119
+
120
+ const serializer = build(parentSchema, { schema: { 'contact.json': contactSchema } })
121
+
122
+ const data = {
123
+ contacts: [
124
+ { firstName: 'Alice', lastName: 'Wonder' },
125
+ { firstName: 'Bob', lastName: 'Builder' }
126
+ ],
127
+ primary: { firstName: 'Charlie', lastName: 'Charm' },
128
+ secondary: { firstName: 'Diana', lastName: 'Prince' }
129
+ }
130
+
131
+ const output = serializer(data)
132
+ t.assert.doesNotThrow(() => JSON.parse(output))
133
+ t.assert.equal(output, JSON.stringify(data))
134
+ })
135
+
136
+ test('external array $ref schema should be reused, not inlined at every reference', (t) => {
137
+ t.plan(2)
138
+
139
+ const tagsSchema = {
140
+ $id: 'tags.json',
141
+ type: 'array',
142
+ items: { type: 'string' }
143
+ }
144
+
145
+ const parentSchema = {
146
+ type: 'object',
147
+ properties: {
148
+ a: { $ref: 'tags.json' },
149
+ b: { $ref: 'tags.json' },
150
+ c: { $ref: 'tags.json' }
151
+ }
152
+ }
153
+
154
+ const serializer = build(parentSchema, { schema: { 'tags.json': tagsSchema } })
155
+
156
+ const data = {
157
+ a: ['x', 'y'],
158
+ b: ['z'],
159
+ c: []
160
+ }
161
+
162
+ const output = serializer(data)
163
+ t.assert.doesNotThrow(() => JSON.parse(output))
164
+ t.assert.equal(output, JSON.stringify(data))
165
+ })
166
+
167
+ test('inline anonymous schemas should still be inlined (not extracted)', (t) => {
168
+ t.plan(1)
169
+
170
+ // Inline schemas (no external $ref) should continue to work normally
171
+ const schema = {
172
+ type: 'object',
173
+ properties: {
174
+ a: { type: 'object', properties: { x: { type: 'string' } } },
175
+ b: { type: 'object', properties: { x: { type: 'string' } } }
176
+ }
177
+ }
178
+
179
+ const serializer = build(schema)
180
+ const output = serializer({ a: { x: 'hello' }, b: { x: 'world' } })
181
+
182
+ t.assert.equal(output, '{"a":{"x":"hello"},"b":{"x":"world"}}')
183
+ })
@@ -1,57 +1,57 @@
1
- 'use strict'
2
-
3
- const { test } = require('node:test')
4
- const build = require('..')
5
-
6
- test('additionalProperties: false', (t) => {
7
- t.plan(1)
8
- const stringify = build({
9
- title: 'additionalProperties',
10
- type: 'object',
11
- properties: {
12
- foo: {
13
- type: 'string'
14
- }
15
- },
16
- additionalProperties: false
17
- })
18
-
19
- const obj = { foo: 'a', bar: 'b', baz: 'c' }
20
- t.assert.equal(stringify(obj), '{"foo":"a"}')
21
- })
22
-
23
- test('additionalProperties: {}', (t) => {
24
- t.plan(1)
25
- const stringify = build({
26
- title: 'additionalProperties',
27
- type: 'object',
28
- properties: {
29
- foo: {
30
- type: 'string'
31
- }
32
- },
33
- additionalProperties: {}
34
- })
35
-
36
- const obj = { foo: 'a', bar: 'b', baz: 'c' }
37
- t.assert.equal(stringify(obj), '{"foo":"a","bar":"b","baz":"c"}')
38
- })
39
-
40
- test('additionalProperties: {type: string}', (t) => {
41
- t.plan(1)
42
- const stringify = build({
43
- title: 'additionalProperties',
44
- type: 'object',
45
- properties: {
46
- foo: {
47
- type: 'string'
48
- }
49
- },
50
- additionalProperties: {
51
- type: 'string'
52
- }
53
- })
54
-
55
- const obj = { foo: 'a', bar: 'b', baz: 'c' }
56
- t.assert.equal(stringify(obj), '{"foo":"a","bar":"b","baz":"c"}')
57
- })
1
+ 'use strict'
2
+
3
+ const { test } = require('node:test')
4
+ const build = require('..')
5
+
6
+ test('additionalProperties: false', (t) => {
7
+ t.plan(1)
8
+ const stringify = build({
9
+ title: 'additionalProperties',
10
+ type: 'object',
11
+ properties: {
12
+ foo: {
13
+ type: 'string'
14
+ }
15
+ },
16
+ additionalProperties: false
17
+ })
18
+
19
+ const obj = { foo: 'a', bar: 'b', baz: 'c' }
20
+ t.assert.equal(stringify(obj), '{"foo":"a"}')
21
+ })
22
+
23
+ test('additionalProperties: {}', (t) => {
24
+ t.plan(1)
25
+ const stringify = build({
26
+ title: 'additionalProperties',
27
+ type: 'object',
28
+ properties: {
29
+ foo: {
30
+ type: 'string'
31
+ }
32
+ },
33
+ additionalProperties: {}
34
+ })
35
+
36
+ const obj = { foo: 'a', bar: 'b', baz: 'c' }
37
+ t.assert.equal(stringify(obj), '{"foo":"a","bar":"b","baz":"c"}')
38
+ })
39
+
40
+ test('additionalProperties: {type: string}', (t) => {
41
+ t.plan(1)
42
+ const stringify = build({
43
+ title: 'additionalProperties',
44
+ type: 'object',
45
+ properties: {
46
+ foo: {
47
+ type: 'string'
48
+ }
49
+ },
50
+ additionalProperties: {
51
+ type: 'string'
52
+ }
53
+ })
54
+
55
+ const obj = { foo: 'a', bar: 'b', baz: 'c' }
56
+ t.assert.equal(stringify(obj), '{"foo":"a","bar":"b","baz":"c"}')
57
+ })
@@ -488,3 +488,23 @@ test('all array items does not match oneOf types', (t) => {
488
488
 
489
489
  t.assert.throws(() => stringify({ data: [null, false, true, undefined, [], {}] }))
490
490
  })
491
+
492
+ test('invalid oneOf schema', (t) => {
493
+ t.plan(1)
494
+
495
+ const schema = {
496
+ type: 'object',
497
+ properties: {
498
+ prop: {
499
+ oneOf: 'not array' // invalid, oneOf must be array
500
+ }
501
+ }
502
+ }
503
+
504
+ try {
505
+ build(schema)
506
+ t.assert.fail('Should throw')
507
+ } catch (err) {
508
+ t.assert.ok(err.message.includes('schema is invalid'))
509
+ }
510
+ })
package/test/ref.test.js CHANGED
@@ -2044,3 +2044,34 @@ test('ref internal - throw if schema has definition twice with different shape',
2044
2044
  t.assert.equal(err.message, 'There is already another anchor "#uri" in schema "test".')
2045
2045
  }
2046
2046
  })
2047
+
2048
+ test('ref nested', (t) => {
2049
+ t.plan(2)
2050
+
2051
+ const schema = {
2052
+ definitions: {
2053
+ def1: {
2054
+ $ref: '#/definitions/def2'
2055
+ },
2056
+ def2: {
2057
+ type: 'string'
2058
+ }
2059
+ },
2060
+ type: 'object',
2061
+ properties: {
2062
+ str: {
2063
+ $ref: '#/definitions/def1'
2064
+ }
2065
+ }
2066
+ }
2067
+
2068
+ const object = {
2069
+ str: 'test'
2070
+ }
2071
+
2072
+ const stringify = build(schema)
2073
+ const output = stringify(object)
2074
+
2075
+ t.assert.doesNotThrow(() => JSON.parse(output))
2076
+ t.assert.equal(output, '{"str":"test"}')
2077
+ })
@@ -4,7 +4,7 @@ const { test } = require('node:test')
4
4
  const build = require('..')
5
5
 
6
6
  test('nested object in pattern properties for typebox', (t) => {
7
- const { Type } = require('@sinclair/typebox')
7
+ const { Type } = require('typebox')
8
8
 
9
9
  t.plan(1)
10
10
 
@@ -1,7 +1,7 @@
1
1
  // eslint-disable-next-line @typescript-eslint/no-unused-vars -- Test using this disabled, see https://github.com/fastify/fast-json-stringify/pull/683
2
2
  import Ajv from 'ajv'
3
3
  import build, { restore, Schema, validLargeArrayMechanisms } from '..'
4
- import { expectError, expectType } from 'tsd'
4
+ import { expect } from 'tstyche'
5
5
 
6
6
  // Number schemas
7
7
  build({
@@ -26,9 +26,10 @@ build({
26
26
  build({
27
27
  type: 'number'
28
28
  }, { rounding: 'trunc' })
29
- expectError(build({
29
+
30
+ expect(build).type.not.toBeCallableWith({
30
31
  type: 'number'
31
- }, { rounding: 'invalid' }))
32
+ }, { rounding: 'invalid' })
32
33
 
33
34
  // String schema
34
35
  build({
@@ -153,7 +154,6 @@ build({
153
154
  })({ something: 'a string', somethingElse: 42 })
154
155
 
155
156
  // String schema with format
156
-
157
157
  build({
158
158
  type: 'string',
159
159
  format: 'date-time'
@@ -181,7 +181,6 @@ str = build({
181
181
  type: 'number'
182
182
  }, { mode: 'standalone' })
183
183
  */
184
-
185
184
  const debugCompiled = build({
186
185
  title: 'default string',
187
186
  type: 'object',
@@ -191,11 +190,12 @@ const debugCompiled = build({
191
190
  }
192
191
  }
193
192
  }, { mode: 'debug' })
194
- expectType<ReturnType<typeof build>>(build.restore(debugCompiled))
195
- expectType<ReturnType<typeof build>>(restore(debugCompiled))
196
193
 
197
- expectType<string[]>(build.validLargeArrayMechanisms)
198
- expectType<string[]>(validLargeArrayMechanisms)
194
+ expect(build.restore(debugCompiled)).type.toBe(build({} as Schema))
195
+ expect(restore(debugCompiled)).type.toBe(build({} as Schema))
196
+
197
+ expect(build.validLargeArrayMechanisms).type.toBe<string[]>()
198
+ expect(validLargeArrayMechanisms).type.toBe<string[]>()
199
199
 
200
200
  /**
201
201
  * Schema inference
@@ -211,10 +211,11 @@ const stringify3 = build({
211
211
  type: 'object',
212
212
  properties: { a: { type: 'string' } },
213
213
  })
214
+
214
215
  stringify3<InferenceSchema>({ id: '123' })
215
216
  stringify3<InferenceSchema>({ a: 123, id: '123' })
216
- expectError(stringify3<InferenceSchema>({ anotherOne: 'bar' }))
217
- expectError(stringify3<Schema>({ a: 'bar' }))
217
+ expect(stringify3<InferenceSchema>).type.not.toBeCallableWith({ anotherOne: 'bar' })
218
+ expect(stringify3<Schema>).type.not.toBeCallableWith({ a: 'bar' })
218
219
 
219
220
  // Without inference
220
221
  const stringify4 = build({
@@ -231,29 +232,28 @@ const stringify5 = build({
231
232
  type: 'string',
232
233
  })
233
234
  stringify5('foo')
234
- expectError(stringify5({ id: '123' }))
235
+ expect(stringify5).type.not.toBeCallableWith({ id: '123' })
235
236
 
236
237
  // Without inference - null type
237
238
  const stringify6 = build({
238
239
  type: 'null',
239
240
  })
240
241
  stringify6(null)
241
- expectError(stringify6('a string'))
242
+ expect(stringify6).type.not.toBeCallableWith('a string')
242
243
 
243
244
  // Without inference - boolean type
244
245
  const stringify7 = build({
245
246
  type: 'boolean',
246
247
  })
247
248
  stringify7(true)
248
- expectError(stringify7('a string'))
249
+ expect(stringify7).type.not.toBeCallableWith('a string')
249
250
 
250
251
  // largeArrayMechanism
251
-
252
252
  build({}, { largeArrayMechanism: 'json-stringify' })
253
253
  build({}, { largeArrayMechanism: 'default' })
254
- expectError(build({} as Schema, { largeArrayMechanism: 'invalid' }))
254
+ expect(build).type.not.toBeCallableWith({} as Schema, { largeArrayMechanism: 'invalid' })
255
255
 
256
256
  build({}, { largeArraySize: 2000 })
257
257
  build({}, { largeArraySize: '2e4' })
258
258
  build({}, { largeArraySize: 2n })
259
- expectError(build({} as Schema, { largeArraySize: ['asdf'] }))
259
+ expect(build).type.not.toBeCallableWith({} as Schema, { largeArraySize: ['asdf'] })
package/.github/stale.yml DELETED
@@ -1,21 +0,0 @@
1
- # Number of days of inactivity before an issue becomes stale
2
- daysUntilStale: 15
3
- # Number of days of inactivity before a stale issue is closed
4
- daysUntilClose: 7
5
- # Issues with these labels will never be considered stale
6
- exemptLabels:
7
- - "discussion"
8
- - "feature request"
9
- - "bug"
10
- - "help wanted"
11
- - "plugin suggestion"
12
- - "good first issue"
13
- # Label to use when marking an issue as stale
14
- staleLabel: stale
15
- # Comment to post when marking an issue as stale. Set to `false` to disable
16
- markComment: >
17
- This issue has been automatically marked as stale because it has not had
18
- recent activity. It will be closed if no further activity occurs. Thank you
19
- for your contributions.
20
- # Comment to post when closing a stale issue. Set to `false` to disable
21
- closeComment: false