gruber 0.9.0 → 0.9.2

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/CHANGELOG.md CHANGED
@@ -7,6 +7,36 @@ layout: simple.njk
7
7
 
8
8
  This file documents notable changes to the project
9
9
 
10
+ ## next
11
+
12
+ **fixes**
13
+
14
+ - `Terminator#waitForSignals` no longer exits the process
15
+
16
+ **unstable**
17
+
18
+ - Add `Structure.record`
19
+
20
+ ## 0.9.1
21
+
22
+ **features**
23
+
24
+ - Add `Structure.nullable`
25
+ - Add `Structure.enum`
26
+ - Add `Structure.undefined`
27
+ - Add `Structure.optional`
28
+ - Add `pickProperties` utility
29
+ - Add `getOrInsert` utility
30
+
31
+ **improved**
32
+
33
+ - short-circuit optional-parameters during TypeScript URL param parsing
34
+
35
+ **unstable**
36
+
37
+ - Add `Structure.fromJSONSchema`
38
+ - Add `Structure.tuple`
39
+
10
40
  ## 0.9
11
41
 
12
42
  **features**
@@ -130,6 +130,72 @@ export declare class Structure<T> {
130
130
  * ```
131
131
  */
132
132
  static union<T extends Structure<unknown>[]>(types: T): Structure<Infer<T[number]>>;
133
+ /**
134
+ * @unstable
135
+ *
136
+ * Attempts to create a Structure from a parsed [JSON Schema](https://json-schema.org/specification) value.
137
+ * This is implemented on a as-needed bases, currently it supports:
138
+ * - "const" → `Structure.literal`
139
+ * - type=string → `Structure.string`
140
+ * - type=number → `Structure.number`
141
+ * - type=boolean → `Structure.boolean`
142
+ * - type=array → "items" are recursively parsed and put into a `Structure.array`
143
+ * - type=object → "properties" are recursively parsed and put into a `Structure.object`
144
+ * - anyOf → `Structure.union`
145
+ *
146
+ * ```js
147
+ * Structure.fromJSONSchema({ type: "string" })
148
+ * Structure.fromJSONSchema({ type: "number" })
149
+ * Structure.fromJSONSchema({ type: "boolean" })
150
+ * Structure.fromJSONSchema({
151
+ * type: "object",
152
+ * properties: {
153
+ * name: { type:"string" },
154
+ * age: { type: "number" }
155
+ * },
156
+ * required: ["name"]
157
+ * })
158
+ * Structure.fromJSONSchema({ type: "array", items: { type: "string" } })
159
+ * Structure.fromJSONSchema({
160
+ * anyOf: [
161
+ * { type: "string" },
162
+ * { type: "number" }
163
+ * ]
164
+ * });
165
+ * ```
166
+ *
167
+ * notes
168
+ * - array "prefixItems" are not supported, maybe they could be mapped to tuples?
169
+ */
170
+ static fromJSONSchema(schema: any): Structure<any>;
171
+ /**
172
+ * @unstable
173
+ *
174
+ * Creates a Structure for arrays where each index has a different validation
175
+ *
176
+ * ```js
177
+ * Structure.tuple([Structure.string(), Structure.number()])
178
+ * ```
179
+ */
180
+ static tuple<const T extends Structure<any>[]>(types: T): Structure<InferObject<T>>;
181
+ /**
182
+ * @unstable
183
+ *
184
+ * Creates a Structure for objects that map a key to a common type of value
185
+ *
186
+ * ```js
187
+ * Structure.record(Structure.string(), Structure.number())
188
+ * Structure.record(
189
+ * Structure.string(),
190
+ * Structure.object({ name: Structure.string() })
191
+ * )
192
+ * Structure.record(
193
+ * Structure.enum(["name", "address", "emailAddress"]),
194
+ * Structure.string()
195
+ * )
196
+ * ```
197
+ */
198
+ static record<K extends string, V>(keyStruct: Structure<K>, valueStruct: Structure<V>): Structure<Record<K, V>>;
133
199
  /**
134
200
  * Define a Structure to validate the value is `null`
135
201
  *
@@ -171,5 +237,37 @@ export declare class Structure<T> {
171
237
  * ```
172
238
  */
173
239
  static date(): Structure<Date>;
240
+ /**
241
+ * Creates a Structure that validates a value is either another structure or a null value
242
+ *
243
+ * ```js
244
+ * Structure.nullable(Structure.string())
245
+ * ```
246
+ */
247
+ static nullable<T>(input: Structure<T>): Structure<T | null>;
248
+ /**
249
+ * Creates a Structure that validates a value is one of a set of literals
250
+ *
251
+ * ```js
252
+ * Structure.enum(['a string', 42, false])
253
+ * ```
254
+ */
255
+ static enum<T extends (string | number | boolean)[]>(values: T): Structure<string | number | boolean>;
256
+ /**
257
+ * Creates a Structure that validates a value is the `undefined` value
258
+ *
259
+ * ```js
260
+ * Structure.undefined()
261
+ * ```
262
+ */
263
+ static undefined(): Structure<undefined>;
264
+ /**
265
+ * Creates a Structure that validates another structure or is not defined
266
+ *
267
+ * ```js
268
+ * Structure.optional(Structure.string())
269
+ * ```
270
+ */
271
+ static optional<T>(input: Structure<T>): Structure<T | undefined>;
174
272
  }
175
273
  //# sourceMappingURL=structure.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"structure.d.ts","sourceRoot":"","sources":["structure.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"structure.d.ts","sourceRoot":"","sources":["structure.ts"],"names":[],"mappings":"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"}
@@ -310,6 +310,152 @@ export class Structure {
310
310
  throw new Structure.Error("Does not match any types in the union", context?.path, errors);
311
311
  });
312
312
  }
313
+ /**
314
+ * @unstable
315
+ *
316
+ * Attempts to create a Structure from a parsed [JSON Schema](https://json-schema.org/specification) value.
317
+ * This is implemented on a as-needed bases, currently it supports:
318
+ * - "const" → `Structure.literal`
319
+ * - type=string → `Structure.string`
320
+ * - type=number → `Structure.number`
321
+ * - type=boolean → `Structure.boolean`
322
+ * - type=array → "items" are recursively parsed and put into a `Structure.array`
323
+ * - type=object → "properties" are recursively parsed and put into a `Structure.object`
324
+ * - anyOf → `Structure.union`
325
+ *
326
+ * ```js
327
+ * Structure.fromJSONSchema({ type: "string" })
328
+ * Structure.fromJSONSchema({ type: "number" })
329
+ * Structure.fromJSONSchema({ type: "boolean" })
330
+ * Structure.fromJSONSchema({
331
+ * type: "object",
332
+ * properties: {
333
+ * name: { type:"string" },
334
+ * age: { type: "number" }
335
+ * },
336
+ * required: ["name"]
337
+ * })
338
+ * Structure.fromJSONSchema({ type: "array", items: { type: "string" } })
339
+ * Structure.fromJSONSchema({
340
+ * anyOf: [
341
+ * { type: "string" },
342
+ * { type: "number" }
343
+ * ]
344
+ * });
345
+ * ```
346
+ *
347
+ * notes
348
+ * - array "prefixItems" are not supported, maybe they could be mapped to tuples?
349
+ */
350
+ static fromJSONSchema(schema) {
351
+ if (schema.const)
352
+ return Structure.literal(schema.const);
353
+ if (schema.type === "string")
354
+ return Structure.string(schema.default);
355
+ if (schema.type === "number")
356
+ return Structure.number(schema.default);
357
+ if (schema.type === "boolean")
358
+ return Structure.boolean(schema.default);
359
+ if (schema.type === "array") {
360
+ return Structure.array(Structure.fromJSONSchema(schema.items));
361
+ }
362
+ if (schema.type === "object" && typeof schema.properties === "object") {
363
+ const fields = {};
364
+ const required = new Set(schema.required ?? []);
365
+ for (const [key, childSchema] of Object.entries(schema.properties)) {
366
+ const childStruct = Structure.fromJSONSchema(childSchema);
367
+ fields[key] = required.has(key)
368
+ ? childStruct
369
+ : Structure.optional(childStruct);
370
+ }
371
+ return Structure.object(fields);
372
+ }
373
+ if (Array.isArray(schema.anyOf)) {
374
+ return Structure.union(schema.anyOf.map((t) => Structure.fromJSONSchema(t)));
375
+ }
376
+ throw new TypeError("Unknown schema");
377
+ }
378
+ /**
379
+ * @unstable
380
+ *
381
+ * Creates a Structure for arrays where each index has a different validation
382
+ *
383
+ * ```js
384
+ * Structure.tuple([Structure.string(), Structure.number()])
385
+ * ```
386
+ */
387
+ static tuple(types) {
388
+ const schema = {
389
+ type: "array",
390
+ prefixItems: types.map((t) => t.schema),
391
+ };
392
+ return new Structure(schema, (value, context) => {
393
+ if (!Array.isArray(value))
394
+ throw new Error("Not an array");
395
+ if (value.length !== types.length)
396
+ throw new Error("Incorrect length");
397
+ let output = [];
398
+ let errors = [];
399
+ for (let i = 0; i < types.length; i++) {
400
+ const childContext = _nestContext(context, `${i}`);
401
+ try {
402
+ output.push(types[i].process(value[i], childContext));
403
+ }
404
+ catch (error) {
405
+ errors.push(Structure.Error.chain(error, childContext));
406
+ }
407
+ }
408
+ if (errors.length > 0) {
409
+ throw new Structure.Error("Tuple value does not match schema", context.path, errors);
410
+ }
411
+ return output;
412
+ });
413
+ }
414
+ /**
415
+ * @unstable
416
+ *
417
+ * Creates a Structure for objects that map a key to a common type of value
418
+ *
419
+ * ```js
420
+ * Structure.record(Structure.string(), Structure.number())
421
+ * Structure.record(
422
+ * Structure.string(),
423
+ * Structure.object({ name: Structure.string() })
424
+ * )
425
+ * Structure.record(
426
+ * Structure.enum(["name", "address", "emailAddress"]),
427
+ * Structure.string()
428
+ * )
429
+ * ```
430
+ */
431
+ static record(keyStruct, valueStruct) {
432
+ const schema = {
433
+ type: "object",
434
+ };
435
+ return new Structure(schema, (value, context) => {
436
+ if (typeof value !== "object")
437
+ throw new Error("Not an object");
438
+ const output = {};
439
+ const errors = [];
440
+ for (const entry of Object.entries(value)) {
441
+ try {
442
+ const ctx = _nestContext(context, entry[0]);
443
+ output[keyStruct.process(entry[0], ctx)] = valueStruct.process(entry[1], ctx);
444
+ }
445
+ catch (error) {
446
+ errors.push(error);
447
+ }
448
+ }
449
+ if (errors.length > 0) {
450
+ throw new Structure.Error("Invalid record", context.path, errors);
451
+ }
452
+ for (const key in output) {
453
+ if (output[key] === undefined)
454
+ delete output[key];
455
+ }
456
+ return output;
457
+ });
458
+ }
313
459
  /**
314
460
  * Define a Structure to validate the value is `null`
315
461
  *
@@ -408,4 +554,49 @@ export class Structure {
408
554
  throw new Error("not a Date");
409
555
  });
410
556
  }
557
+ /**
558
+ * Creates a Structure that validates a value is either another structure or a null value
559
+ *
560
+ * ```js
561
+ * Structure.nullable(Structure.string())
562
+ * ```
563
+ */
564
+ static nullable(input) {
565
+ return Structure.union([Structure.null(), input]);
566
+ }
567
+ /**
568
+ * Creates a Structure that validates a value is one of a set of literals
569
+ *
570
+ * ```js
571
+ * Structure.enum(['a string', 42, false])
572
+ * ```
573
+ */
574
+ static enum(values) {
575
+ return Structure.union(values.map((v) => Structure.literal(v)));
576
+ }
577
+ /**
578
+ * Creates a Structure that validates a value is the `undefined` value
579
+ *
580
+ * ```js
581
+ * Structure.undefined()
582
+ * ```
583
+ */
584
+ static undefined() {
585
+ return new Structure({}, (value) => {
586
+ if (value !== undefined)
587
+ throw new Error('value is not "undefined"');
588
+ else
589
+ return undefined;
590
+ });
591
+ }
592
+ /**
593
+ * Creates a Structure that validates another structure or is not defined
594
+ *
595
+ * ```js
596
+ * Structure.optional(Structure.string())
597
+ * ```
598
+ */
599
+ static optional(input) {
600
+ return Structure.union([input, Structure.undefined()]);
601
+ }
411
602
  }
@@ -516,6 +516,117 @@ describe("Structure", () => {
516
516
  });
517
517
  });
518
518
 
519
+ describe("fromJSONSchema", () => {
520
+ it("parses constants", () => {
521
+ const result = Structure.fromJSONSchema({ const: 42 });
522
+ assertEquals(result.process(42), 42);
523
+ });
524
+ it("parses strings", () => {
525
+ const result = Structure.fromJSONSchema({ type: "string" });
526
+ assertEquals(result.process("Geoff Testington"), "Geoff Testington");
527
+ });
528
+ it("parses numbers", () => {
529
+ const result = Structure.fromJSONSchema({ type: "number" });
530
+ assertEquals(result.process(42), 42);
531
+ });
532
+ it("parses booleans", () => {
533
+ const result = Structure.fromJSONSchema({ type: "boolean" });
534
+ assertEquals(result.process(false), false);
535
+ });
536
+ it("parses arrays", () => {
537
+ const result = Structure.fromJSONSchema({
538
+ type: "array",
539
+ items: { type: "string" },
540
+ });
541
+ assertEquals(result.process(["A", "B", "C"]), ["A", "B", "C"]);
542
+ });
543
+ it("parses objects", () => {
544
+ const result = Structure.fromJSONSchema({
545
+ type: "object",
546
+ properties: { name: { type: "string" } },
547
+ required: ["name"],
548
+ });
549
+ assertEquals(result.process({ name: "Geoff" }), { name: "Geoff" });
550
+ });
551
+ it("parses objects with optionals", () => {
552
+ const result = Structure.fromJSONSchema({
553
+ type: "object",
554
+ properties: { name: { type: "string" } },
555
+ });
556
+ assertEquals(result.process({}), {});
557
+ });
558
+ it("parses unions", () => {
559
+ const result = Structure.fromJSONSchema({
560
+ anyOf: [{ type: "string" }, { type: "number" }],
561
+ });
562
+ assertEquals(result.process("Geoff Testington"), "Geoff Testington");
563
+ assertEquals(result.process(42), 42);
564
+ });
565
+ it("throws for unknown", () => {
566
+ assertThrows(() => Structure.fromJSONSchema({}));
567
+ });
568
+ });
569
+
570
+ describe("tuple", () => {
571
+ const struct = Structure.tuple([
572
+ Structure.string(),
573
+ Structure.number(),
574
+ Structure.literal("magic"),
575
+ ]);
576
+
577
+ it("allows matching arrays", () => {
578
+ assertEquals(struct.process(["Geoff T", 42, "magic"]), [
579
+ "Geoff T",
580
+ 42,
581
+ "magic",
582
+ ]);
583
+ });
584
+ it("blocks subsets", () => {
585
+ assertThrows(() => struct.process(["Geoff T", 42]));
586
+ });
587
+ it("blocks invalid", () => {
588
+ assertThrows(() => struct.process([42, "Geoff T", new Date()]));
589
+ });
590
+ });
591
+
592
+ describe("record", () => {
593
+ it("allows matching key-value pairs", () => {
594
+ const struct = Structure.record(Structure.string(), Structure.number());
595
+ const value = struct.process({ age: 42 });
596
+ assertEquals(value, { age: 42 });
597
+ });
598
+ it("allows enum keys", () => {
599
+ const struct = Structure.record(
600
+ Structure.enum(["name", "pet"]),
601
+ Structure.string(),
602
+ );
603
+ const value = struct.process({ name: "Geoff T" });
604
+ assertEquals(value, { name: "Geoff T" });
605
+ });
606
+ it("blocks invalid keys", () => {
607
+ const struct = Structure.record(
608
+ Structure.literal("name"),
609
+ Structure.string(),
610
+ );
611
+
612
+ assertThrows(
613
+ () => struct.process({ pet: "Hugo" }),
614
+ (error) => error instanceof Structure.Error,
615
+ );
616
+ });
617
+ it("blocks invalid values", () => {
618
+ const struct = Structure.record(
619
+ Structure.literal("name"),
620
+ Structure.string(),
621
+ );
622
+
623
+ assertThrows(
624
+ () => struct.process({ name: 119 }),
625
+ (error) => error instanceof Structure.Error,
626
+ );
627
+ });
628
+ });
629
+
519
630
  describe("null", () => {
520
631
  const struct = Structure.null();
521
632
 
@@ -360,6 +360,162 @@ export class Structure<T> {
360
360
  });
361
361
  }
362
362
 
363
+ /**
364
+ * @unstable
365
+ *
366
+ * Attempts to create a Structure from a parsed [JSON Schema](https://json-schema.org/specification) value.
367
+ * This is implemented on a as-needed bases, currently it supports:
368
+ * - "const" → `Structure.literal`
369
+ * - type=string → `Structure.string`
370
+ * - type=number → `Structure.number`
371
+ * - type=boolean → `Structure.boolean`
372
+ * - type=array → "items" are recursively parsed and put into a `Structure.array`
373
+ * - type=object → "properties" are recursively parsed and put into a `Structure.object`
374
+ * - anyOf → `Structure.union`
375
+ *
376
+ * ```js
377
+ * Structure.fromJSONSchema({ type: "string" })
378
+ * Structure.fromJSONSchema({ type: "number" })
379
+ * Structure.fromJSONSchema({ type: "boolean" })
380
+ * Structure.fromJSONSchema({
381
+ * type: "object",
382
+ * properties: {
383
+ * name: { type:"string" },
384
+ * age: { type: "number" }
385
+ * },
386
+ * required: ["name"]
387
+ * })
388
+ * Structure.fromJSONSchema({ type: "array", items: { type: "string" } })
389
+ * Structure.fromJSONSchema({
390
+ * anyOf: [
391
+ * { type: "string" },
392
+ * { type: "number" }
393
+ * ]
394
+ * });
395
+ * ```
396
+ *
397
+ * notes
398
+ * - array "prefixItems" are not supported, maybe they could be mapped to tuples?
399
+ */
400
+ static fromJSONSchema(schema: any): Structure<any> {
401
+ if (schema.const) return Structure.literal(schema.const);
402
+ if (schema.type === "string") return Structure.string(schema.default);
403
+ if (schema.type === "number") return Structure.number(schema.default);
404
+ if (schema.type === "boolean") return Structure.boolean(schema.default);
405
+ if (schema.type === "array") {
406
+ return Structure.array(Structure.fromJSONSchema(schema.items));
407
+ }
408
+ if (schema.type === "object" && typeof schema.properties === "object") {
409
+ const fields: any = {};
410
+ const required = new Set(schema.required ?? []);
411
+ for (const [key, childSchema] of Object.entries(schema.properties)) {
412
+ const childStruct = Structure.fromJSONSchema(childSchema);
413
+ fields[key] = required.has(key)
414
+ ? childStruct
415
+ : Structure.optional(childStruct);
416
+ }
417
+ return Structure.object(fields);
418
+ }
419
+ if (Array.isArray(schema.anyOf)) {
420
+ return Structure.union(
421
+ schema.anyOf.map((t: any) => Structure.fromJSONSchema(t)),
422
+ );
423
+ }
424
+ throw new TypeError("Unknown schema");
425
+ }
426
+
427
+ /**
428
+ * @unstable
429
+ *
430
+ * Creates a Structure for arrays where each index has a different validation
431
+ *
432
+ * ```js
433
+ * Structure.tuple([Structure.string(), Structure.number()])
434
+ * ```
435
+ */
436
+ static tuple<const T extends Structure<any>[]>(
437
+ types: T,
438
+ ): Structure<InferObject<T>> {
439
+ const schema = {
440
+ type: "array",
441
+ prefixItems: types.map((t) => t.schema),
442
+ };
443
+ return new Structure(schema, (value, context) => {
444
+ if (!Array.isArray(value)) throw new Error("Not an array");
445
+ if (value.length !== types.length) throw new Error("Incorrect length");
446
+
447
+ let output: InferObject<T> = [] as any;
448
+ let errors: any[] = [];
449
+ for (let i = 0; i < types.length; i++) {
450
+ const childContext = _nestContext(context, `${i}`);
451
+ try {
452
+ output.push(types[i].process(value[i], childContext));
453
+ } catch (error) {
454
+ errors.push(Structure.Error.chain(error, childContext));
455
+ }
456
+ }
457
+ if (errors.length > 0) {
458
+ throw new Structure.Error(
459
+ "Tuple value does not match schema",
460
+ context.path,
461
+ errors,
462
+ );
463
+ }
464
+ return output;
465
+ });
466
+ }
467
+
468
+ /**
469
+ * @unstable
470
+ *
471
+ * Creates a Structure for objects that map a key to a common type of value
472
+ *
473
+ * ```js
474
+ * Structure.record(Structure.string(), Structure.number())
475
+ * Structure.record(
476
+ * Structure.string(),
477
+ * Structure.object({ name: Structure.string() })
478
+ * )
479
+ * Structure.record(
480
+ * Structure.enum(["name", "address", "emailAddress"]),
481
+ * Structure.string()
482
+ * )
483
+ * ```
484
+ */
485
+ static record<K extends string, V>(
486
+ keyStruct: Structure<K>,
487
+ valueStruct: Structure<V>,
488
+ ): Structure<Record<K, V>> {
489
+ const schema = {
490
+ type: "object",
491
+ };
492
+ return new Structure<Record<K, V>>(schema, (value, context) => {
493
+ if (typeof value !== "object") throw new Error("Not an object");
494
+
495
+ const output: any = {};
496
+ const errors: any[] = [];
497
+ for (const entry of Object.entries(value as any)) {
498
+ try {
499
+ const ctx = _nestContext(context, entry[0]);
500
+ output[keyStruct.process(entry[0], ctx)] = valueStruct.process(
501
+ entry[1],
502
+ ctx,
503
+ );
504
+ } catch (error) {
505
+ errors.push(error as Error);
506
+ }
507
+ }
508
+ if (errors.length > 0) {
509
+ throw new Structure.Error("Invalid record", context.path, errors);
510
+ }
511
+ for (const key in output) {
512
+ if (output[key] === undefined) delete output[key];
513
+ }
514
+
515
+ return output;
516
+ });
517
+ }
518
+
363
519
  /**
364
520
  * Define a Structure to validate the value is `null`
365
521
  *
@@ -473,4 +629,51 @@ export class Structure<T> {
473
629
  throw new Error("not a Date");
474
630
  });
475
631
  }
632
+
633
+ /**
634
+ * Creates a Structure that validates a value is either another structure or a null value
635
+ *
636
+ * ```js
637
+ * Structure.nullable(Structure.string())
638
+ * ```
639
+ */
640
+ static nullable<T>(input: Structure<T>) {
641
+ return Structure.union([Structure.null(), input]);
642
+ }
643
+
644
+ /**
645
+ * Creates a Structure that validates a value is one of a set of literals
646
+ *
647
+ * ```js
648
+ * Structure.enum(['a string', 42, false])
649
+ * ```
650
+ */
651
+ static enum<T extends (string | number | boolean)[]>(values: T) {
652
+ return Structure.union(values.map((v) => Structure.literal(v)));
653
+ }
654
+
655
+ /**
656
+ * Creates a Structure that validates a value is the `undefined` value
657
+ *
658
+ * ```js
659
+ * Structure.undefined()
660
+ * ```
661
+ */
662
+ static undefined() {
663
+ return new Structure<undefined>({}, (value) => {
664
+ if (value !== undefined) throw new Error('value is not "undefined"');
665
+ else return undefined;
666
+ });
667
+ }
668
+
669
+ /**
670
+ * Creates a Structure that validates another structure or is not defined
671
+ *
672
+ * ```js
673
+ * Structure.optional(Structure.string())
674
+ * ```
675
+ */
676
+ static optional<T>(input: Structure<T>) {
677
+ return Structure.union([input, Structure.undefined()]);
678
+ }
476
679
  }
@@ -1 +1 @@
1
- {"version":3,"file":"terminator.d.ts","sourceRoot":"","sources":["terminator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,iBAAiB;IACjC,+FAA+F;IAC/F,OAAO,EAAE,MAAM,CAAC;IAEhB,qCAAqC;IACrC,OAAO,EAAE,MAAM,EAAE,CAAC;IAElB,4DAA4D;IAC5D,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAErE,oEAAoE;IACpE,WAAW,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3D;AAED,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,aAAa,CAAC;AAExD,MAAM,MAAM,gBAAgB,GAAG,MAAM,OAAO,CAAC;AAK7C;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,UAAU;IACtB,KAAK,EAAE,eAAe,CAAa;IACnC,OAAO,EAAE,iBAAiB,CAAC;IAC3B,MAAM,EAAE,YAAY,CAAC;gBAET,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,YAAY;IAK5D;;;;;;;;OAQG;IACH,KAAK,CAAC,KAAK,EAAE,gBAAgB;IAM7B;;;;;;;;;;OAUG;IACG,SAAS,CAAC,KAAK,EAAE,gBAAgB;IAevC;;;;;;;;;OASG;IACH,WAAW,IAAI,QAAQ;IAOvB;;;;;;;;;;;;;OAaG;IACG,cAAc;CAGpB"}
1
+ {"version":3,"file":"terminator.d.ts","sourceRoot":"","sources":["terminator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,iBAAiB;IACjC,+FAA+F;IAC/F,OAAO,EAAE,MAAM,CAAC;IAEhB,qCAAqC;IACrC,OAAO,EAAE,MAAM,EAAE,CAAC;IAElB,4DAA4D;IAC5D,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAErE,oEAAoE;IACpE,WAAW,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3D;AAED,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,aAAa,CAAC;AAExD,MAAM,MAAM,gBAAgB,GAAG,MAAM,OAAO,CAAC;AAK7C;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,UAAU;IACtB,KAAK,EAAE,eAAe,CAAa;IACnC,OAAO,EAAE,iBAAiB,CAAC;IAC3B,MAAM,EAAE,YAAY,CAAC;gBAET,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,YAAY;IAK5D;;;;;;;;OAQG;IACH,KAAK,CAAC,KAAK,EAAE,gBAAgB;IAM7B;;;;;;;;;;OAUG;IACG,SAAS,CAAC,KAAK,EAAE,gBAAgB;IAevC;;;;;;;;;OASG;IACH,WAAW,IAAI,QAAQ;IAOvB;;;;;;;;;;;;;OAaG;IACG,cAAc;CAWpB"}