@atscript/typescript 0.1.30 → 0.1.32

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/index.cjs CHANGED
@@ -141,6 +141,9 @@ else obj[key] = value;
141
141
  return obj;
142
142
  }
143
143
  var BaseRenderer = class extends CodePrinter {
144
+ get unused() {
145
+ return this._unused ?? (this._unused = new Set(this.doc.getUnusedTokens().map((t) => t.text)));
146
+ }
144
147
  pre() {}
145
148
  post() {}
146
149
  render() {
@@ -194,15 +197,14 @@ var BaseRenderer = class extends CodePrinter {
194
197
  }
195
198
  }
196
199
  constructor(doc) {
197
- super(), _define_property$3(this, "doc", void 0), _define_property$3(this, "unused", void 0), this.doc = doc;
198
- this.unused = new Set(this.doc.getUnusedTokens().map((t) => t.text));
200
+ super(), _define_property$3(this, "doc", void 0), _define_property$3(this, "_unused", void 0), this.doc = doc;
199
201
  }
200
202
  };
201
203
 
202
204
  //#endregion
203
205
  //#region packages/typescript/src/codegen/utils.ts
206
+ const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
204
207
  function wrapProp(name) {
205
- const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
206
208
  if (!validIdentifier.test(name)) return `"${escapeQuotes(name)}"`;
207
209
  return name;
208
210
  }
@@ -287,14 +289,16 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
287
289
  if (isGrp) this.write(")");
288
290
  return this.write("[]");
289
291
  }
292
+ if ((0, __atscript_core.isPrimitive)(def)) return this.write(renderPrimitiveTypeDef(def.config.type));
290
293
  }
291
- renderStructure(struct, asClass) {
294
+ renderStructure(struct, asClass, interfaceNode, filterProps) {
292
295
  this.blockln("{}");
293
296
  const patterns = [];
294
297
  const propsDefs = new Set();
295
- for (const prop of Array.from(struct.props.values())) {
298
+ for (const prop of struct.props.values()) {
299
+ if (filterProps?.has(prop.id)) continue;
296
300
  if (prop.token("identifier")?.pattern) {
297
- patterns.push(prop);
301
+ if (!filterProps) patterns.push(prop);
298
302
  continue;
299
303
  }
300
304
  const phantomType = this.phantomPropType(prop.getDefinition());
@@ -310,33 +314,36 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
310
314
  }
311
315
  if (patterns.length > 0) {
312
316
  this.write(`[key: string]: `);
313
- if (patterns.length > 0) {
314
- for (const prop of patterns) propsDefs.add(this.renderTypeDefString(prop.getDefinition()));
315
- const defs = Array.from(propsDefs);
316
- if (defs.length > 1) {
317
- this.indent();
318
- for (const def of defs) {
319
- this.writeln();
320
- this.write("| ");
321
- def.split("\n").forEach((l) => this.write(l.trim()));
322
- }
323
- this.unindent();
317
+ for (const prop of patterns) propsDefs.add(this.renderTypeDefString(prop.getDefinition()));
318
+ const defs = Array.from(propsDefs);
319
+ if (defs.length > 1) {
320
+ this.indent();
321
+ for (const def of defs) {
324
322
  this.writeln();
325
- } else defs[0].split("\n").forEach((l) => this.writeln(l));
326
- }
327
- }
328
- if (asClass) {
329
- this.writeln("static __is_atscript_annotated_type: true");
330
- this.writeln(`static type: TAtscriptTypeObject<keyof ${asClass}, ${asClass}>`);
331
- this.writeln(`static metadata: TMetadataMap<AtscriptMetadata>`);
332
- this.writeln(`static validator: (opts?: Partial<TValidatorOptions>) => Validator<typeof ${asClass}>`);
333
- if (resolveJsonSchemaMode(this.opts) === false) this.writeln("/** @deprecated JSON Schema support is disabled. Calling this method will throw a runtime error. To enable, set `jsonSchema: 'lazy'` or `jsonSchema: 'bundle'` in tsPlugin options, or add `@emit.jsonSchema` annotation to individual interfaces. */");
334
- this.writeln("static toJsonSchema: () => any");
335
- if (!this.opts?.exampleData) this.writeln("/** @deprecated Example Data support is disabled. To enable, set `exampleData: true` in tsPlugin options. */");
336
- this.writeln("static toExampleData?: () => any");
323
+ this.write("| ");
324
+ def.split("\n").forEach((l) => this.write(l.trim()));
325
+ }
326
+ this.unindent();
327
+ this.writeln();
328
+ } else defs[0].split("\n").forEach((l) => this.writeln(l));
337
329
  }
330
+ if (asClass) this.renderStaticDeclarations(asClass, interfaceNode);
338
331
  this.pop();
339
332
  }
333
+ renderStaticDeclarations(asClass, interfaceNode) {
334
+ this.writeln("static __is_atscript_annotated_type: true");
335
+ this.writeln(`static type: TAtscriptTypeObject<keyof ${asClass}, ${asClass}>`);
336
+ this.writeln(`static metadata: TMetadataMap<AtscriptMetadata>`);
337
+ this.writeln(`static validator: (opts?: Partial<TValidatorOptions>) => Validator<typeof ${asClass}>`);
338
+ if (resolveJsonSchemaMode(this.opts) === false) this.writeln("/** @deprecated JSON Schema support is disabled. Calling this method will throw a runtime error. To enable, set `jsonSchema: 'lazy'` or `jsonSchema: 'bundle'` in tsPlugin options, or add `@emit.jsonSchema` annotation to individual interfaces. */");
339
+ this.writeln("static toJsonSchema: () => any");
340
+ if (!this.opts?.exampleData) this.writeln("/** @deprecated Example Data support is disabled. To enable, set `exampleData: true` in tsPlugin options. */");
341
+ this.writeln("static toExampleData?: () => any");
342
+ if (interfaceNode && this.hasDbTable(interfaceNode)) {
343
+ this.renderFlat(interfaceNode);
344
+ this.renderPk(interfaceNode);
345
+ }
346
+ }
340
347
  renderInterface(node) {
341
348
  this.writeln();
342
349
  const exported = node.token("export")?.text === "export";
@@ -360,44 +367,16 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
360
367
  }
361
368
  if (!firstParentProps && (0, __atscript_core.isStructure)(fpDef)) firstParentProps = fpDef.props;
362
369
  }
363
- this.renderStructureFiltered(resolved, node.id, firstParentProps);
370
+ this.renderStructure(resolved, node.id, node, firstParentProps);
364
371
  } else this.writeln("{}");
365
372
  } else {
366
373
  this.write(`class ${node.id} `);
367
374
  const struct = node.getDefinition();
368
- if (struct?.entity === "structure") this.renderStructure(struct, node.id);
375
+ if (struct?.entity === "structure") this.renderStructure(struct, node.id, node);
369
376
  else this.writeln("{}");
370
377
  }
371
378
  this.writeln();
372
379
  }
373
- /**
374
- * Renders a structure block, optionally filtering out props that exist in a parent.
375
- */ renderStructureFiltered(struct, asClass, filterProps) {
376
- if (!filterProps) return this.renderStructure(struct, asClass);
377
- this.blockln("{}");
378
- for (const prop of Array.from(struct.props.values())) {
379
- if (filterProps.has(prop.id)) continue;
380
- if (prop.token("identifier")?.pattern) continue;
381
- const phantomType = this.phantomPropType(prop.getDefinition());
382
- if (phantomType) {
383
- this.writeln(`// ${prop.id}: ${phantomType}`);
384
- continue;
385
- }
386
- const optional = !!prop.token("optional");
387
- this.write(wrapProp(prop.id), optional ? "?" : "", ": ");
388
- const renderedDef = this.renderTypeDefString(prop.getDefinition());
389
- renderedDef.split("\n").forEach((l) => this.writeln(l));
390
- }
391
- this.writeln("static __is_atscript_annotated_type: true");
392
- this.writeln(`static type: TAtscriptTypeObject<keyof ${asClass}, ${asClass}>`);
393
- this.writeln(`static metadata: TMetadataMap<AtscriptMetadata>`);
394
- this.writeln(`static validator: (opts?: Partial<TValidatorOptions>) => Validator<typeof ${asClass}>`);
395
- if (resolveJsonSchemaMode(this.opts) === false) this.writeln("/** @deprecated JSON Schema support is disabled. Calling this method will throw a runtime error. To enable, set `jsonSchema: 'lazy'` or `jsonSchema: 'bundle'` in tsPlugin options, or add `@emit.jsonSchema` annotation to individual interfaces. */");
396
- this.writeln("static toJsonSchema: () => any");
397
- if (!this.opts?.exampleData) this.writeln("/** @deprecated Example Data support is disabled. To enable, set `exampleData: true` in tsPlugin options. */");
398
- this.writeln("static toExampleData?: () => any");
399
- this.pop();
400
- }
401
380
  renderType(node) {
402
381
  this.writeln();
403
382
  const exported = node.token("export")?.text === "export";
@@ -451,6 +430,87 @@ else if ((0, __atscript_core.isPrimitive)(realDef)) typeDef = `TAtscriptTypeFina
451
430
  this.writeln("const toExampleData: (() => any) | undefined");
452
431
  this.popln();
453
432
  }
433
+ /**
434
+ * Checks whether an interface has the `@db.table` annotation.
435
+ *
436
+ * NOTE: Only `@db.table` interfaces get the `__flat` static property.
437
+ * This is intentionally hardcoded — `__flat` exists solely to improve
438
+ * type-safety for filter expressions and `$select`/`$sort` operations
439
+ * in the DB layer. Non-DB interfaces have no use for dot-notation path types.
440
+ */ hasDbTable(node) {
441
+ return !!node.annotations?.some((a) => a.name === "db.table");
442
+ }
443
+ /**
444
+ * Renders the `static __flat` property — a map of all dot-notation paths
445
+ * to their TypeScript value types.
446
+ *
447
+ * This enables type-safe autocomplete for filter keys and `$select`/`$sort`
448
+ * paths when using `AtscriptDbTable`. The `FlatOf<T>` utility type extracts
449
+ * this map at the type level.
450
+ *
451
+ * Special rendering rules:
452
+ * - **Intermediate paths** (structures, arrays of structures) → `never`
453
+ * (prevents `$eq` comparisons, but allows `$select` and `$exists`)
454
+ * - **`@db.json` fields** → `string` (stored as serialized JSON in DB,
455
+ * not individually queryable)
456
+ * - **Leaf fields** → their original TypeScript type
457
+ */ renderFlat(node) {
458
+ const flatMap = (0, __atscript_core.flattenInterfaceNode)(this.doc, node);
459
+ if (flatMap.size === 0) return;
460
+ this.write("static __flat: ");
461
+ this.blockln("{}");
462
+ for (const [path$2, descriptor] of flatMap) {
463
+ this.write(`"${escapeQuotes(path$2)}"`);
464
+ if (descriptor.optional) this.write("?");
465
+ this.write(": ");
466
+ if (descriptor.intermediate) this.writeln("never");
467
+ else if (descriptor.dbJson) this.writeln("string");
468
+ else {
469
+ const originalDef = descriptor.propNode?.getDefinition();
470
+ const defToRender = originalDef && !((0, __atscript_core.isGroup)(descriptor.def) && descriptor.def !== originalDef) ? originalDef : descriptor.def;
471
+ const renderedDef = this.renderTypeDefString(defToRender);
472
+ renderedDef.split("\n").forEach((l) => this.writeln(l));
473
+ }
474
+ }
475
+ this.pop();
476
+ }
477
+ /**
478
+ * Renders the `static __pk` property — the primary key type for type-safe
479
+ * `deleteOne`/`findById` signatures on `AtscriptDbTable`.
480
+ *
481
+ * - **Single PK** (one `@meta.id`) → `static __pk: <scalar type>`
482
+ * - **Compound PK** (multiple `@meta.id`) → `static __pk: { field1: Type1; field2: Type2 }`
483
+ * - **No PK** → no `__pk` emitted
484
+ */ renderPk(node) {
485
+ let struct;
486
+ if (node.hasExtends) struct = this.doc.resolveInterfaceExtends(node);
487
+ if (!struct) struct = node.getDefinition();
488
+ if (!struct || !(0, __atscript_core.isStructure)(struct)) return;
489
+ const pkProps = [];
490
+ for (const [name, prop] of struct.props) {
491
+ if (prop.token("identifier")?.pattern) continue;
492
+ if (prop.countAnnotations("meta.id") > 0) pkProps.push({
493
+ name,
494
+ prop
495
+ });
496
+ }
497
+ if (pkProps.length === 0) return;
498
+ this.writeln();
499
+ if (pkProps.length === 1) {
500
+ this.write("static __pk: ");
501
+ const renderedDef = this.renderTypeDefString(pkProps[0].prop.getDefinition());
502
+ renderedDef.split("\n").forEach((l) => this.writeln(l));
503
+ } else {
504
+ this.write("static __pk: ");
505
+ this.blockln("{}");
506
+ for (const { name, prop } of pkProps) {
507
+ this.write(wrapProp(name), ": ");
508
+ const renderedDef = this.renderTypeDefString(prop.getDefinition());
509
+ renderedDef.split("\n").forEach((l) => this.writeln(l));
510
+ }
511
+ this.pop();
512
+ }
513
+ }
454
514
  phantomPropType(def) {
455
515
  if (!def) return undefined;
456
516
  if ((0, __atscript_core.isPrimitive)(def) && def.config.type === "phantom") return def.id;
@@ -499,24 +559,6 @@ function renderPrimitiveTypeDef(def) {
499
559
  }
500
560
  }
501
561
 
502
- //#endregion
503
- //#region packages/typescript/src/traverse.ts
504
- function forAnnotatedType(def, handlers) {
505
- switch (def.type.kind) {
506
- case "": {
507
- const typed = def;
508
- if (handlers.phantom && typed.type.designType === "phantom") return handlers.phantom(typed);
509
- return handlers.final(typed);
510
- }
511
- case "object": return handlers.object(def);
512
- case "array": return handlers.array(def);
513
- case "union": return handlers.union(def);
514
- case "intersection": return handlers.intersection(def);
515
- case "tuple": return handlers.tuple(def);
516
- default: throw new Error(`Unknown type kind "${def.type.kind}"`);
517
- }
518
- }
519
-
520
562
  //#endregion
521
563
  //#region packages/typescript/src/validator.ts
522
564
  function _define_property$1(obj, key, value) {
@@ -532,28 +574,35 @@ else obj[key] = value;
532
574
  const regexCache = new Map();
533
575
  var Validator = class {
534
576
  isLimitExceeded() {
535
- if (this.stackErrors.length > 0) return this.stackErrors[this.stackErrors.length - 1].length >= this.opts.errorLimit;
577
+ if (this.stackErrors.length > 0) {
578
+ const top = this.stackErrors[this.stackErrors.length - 1];
579
+ return top !== null && top.length >= this.opts.errorLimit;
580
+ }
536
581
  return this.errors.length >= this.opts.errorLimit;
537
582
  }
538
583
  push(name) {
539
584
  this.stackPath.push(name);
540
- this.stackErrors.push([]);
585
+ this.stackErrors.push(null);
586
+ this.cachedPath = this.stackPath.length <= 1 ? "" : this.stackPath[1] + (this.stackPath.length > 2 ? "." + this.stackPath.slice(2).join(".") : "");
541
587
  }
542
588
  pop(saveErrors) {
543
589
  this.stackPath.pop();
544
590
  const popped = this.stackErrors.pop();
545
- if (saveErrors && popped?.length) popped.forEach((error) => {
546
- this.error(error.message, error.path, error.details);
547
- });
591
+ if (saveErrors && popped !== null && popped !== undefined && popped.length > 0) for (const err of popped) this.error(err.message, err.path, err.details);
592
+ this.cachedPath = this.stackPath.length <= 1 ? "" : this.stackPath[1] + (this.stackPath.length > 2 ? "." + this.stackPath.slice(2).join(".") : "");
548
593
  return popped;
549
594
  }
550
595
  clear() {
551
- this.stackErrors[this.stackErrors.length - 1] = [];
596
+ this.stackErrors[this.stackErrors.length - 1] = null;
552
597
  }
553
598
  error(message, path$2, details) {
554
- const errors = this.stackErrors[this.stackErrors.length - 1] || this.errors;
599
+ let errors = this.stackErrors[this.stackErrors.length - 1];
600
+ if (!errors) if (this.stackErrors.length > 0) {
601
+ errors = [];
602
+ this.stackErrors[this.stackErrors.length - 1] = errors;
603
+ } else errors = this.errors;
555
604
  const error = {
556
- path: path$2 || this.path,
605
+ path: path$2 || this.cachedPath,
557
606
  message
558
607
  };
559
608
  if (details?.length) error.details = details;
@@ -573,9 +622,10 @@ var Validator = class {
573
622
  * @returns `true` if the value matches the type definition.
574
623
  * @throws {ValidatorError} When validation fails and `safe` is not `true`.
575
624
  */ validate(value, safe, context) {
576
- this.push("");
577
625
  this.errors = [];
578
626
  this.stackErrors = [];
627
+ this.stackPath = [""];
628
+ this.cachedPath = "";
579
629
  this.context = context;
580
630
  const passed = this.validateSafe(this.def, value);
581
631
  this.pop(!passed);
@@ -589,7 +639,7 @@ var Validator = class {
589
639
  validateSafe(def, value) {
590
640
  if (this.isLimitExceeded()) return false;
591
641
  if (!isAnnotatedType(def)) throw new Error("Can not validate not-annotated type");
592
- if (typeof this.opts.replace === "function") def = this.opts.replace(def, this.path);
642
+ if (typeof this.opts.replace === "function") def = this.opts.replace(def, this.cachedPath);
593
643
  if (def.optional && value === undefined) return true;
594
644
  for (const plugin of this.opts.plugins) {
595
645
  const result = plugin(this, def, value);
@@ -598,18 +648,21 @@ var Validator = class {
598
648
  return this.validateAnnotatedType(def, value);
599
649
  }
600
650
  get path() {
601
- return this.stackPath.slice(1).join(".");
651
+ return this.cachedPath;
602
652
  }
603
653
  validateAnnotatedType(def, value) {
604
- return forAnnotatedType(def, {
605
- final: (d) => this.validatePrimitive(d, value),
606
- phantom: () => true,
607
- object: (d) => this.validateObject(d, value),
608
- array: (d) => this.validateArray(d, value),
609
- union: (d) => this.validateUnion(d, value),
610
- intersection: (d) => this.validateIntersection(d, value),
611
- tuple: (d) => this.validateTuple(d, value)
612
- });
654
+ switch (def.type.kind) {
655
+ case "": {
656
+ if (def.type.designType === "phantom") return true;
657
+ return this.validatePrimitive(def, value);
658
+ }
659
+ case "object": return this.validateObject(def, value);
660
+ case "array": return this.validateArray(def, value);
661
+ case "union": return this.validateUnion(def, value);
662
+ case "intersection": return this.validateIntersection(def, value);
663
+ case "tuple": return this.validateTuple(def, value);
664
+ default: throw new Error(`Unknown type kind "${def.type.kind}"`);
665
+ }
613
666
  }
614
667
  validateUnion(def, value) {
615
668
  let i = 0;
@@ -673,6 +726,30 @@ var Validator = class {
673
726
  return false;
674
727
  }
675
728
  }
729
+ const uniqueItems = def.metadata.get("expect.array.uniqueItems");
730
+ if (uniqueItems) {
731
+ const separator = "▼↩";
732
+ const seen = new Set();
733
+ const keyProps = new Set();
734
+ if (def.type.of.type.kind === "object") {
735
+ for (const [key, val] of def.type.of.type.props.entries()) if (val.metadata.get("expect.array.key")) keyProps.add(key);
736
+ }
737
+ for (let idx = 0; idx < value.length; idx++) {
738
+ const item = value[idx];
739
+ let key;
740
+ if (keyProps.size > 0) {
741
+ key = "";
742
+ for (const prop of keyProps) key += JSON.stringify(item[prop]) + separator;
743
+ } else key = JSON.stringify(item);
744
+ if (seen.has(key)) {
745
+ this.push(String(idx));
746
+ this.error(uniqueItems.message || "Duplicate items are not allowed");
747
+ this.pop(true);
748
+ return false;
749
+ }
750
+ seen.add(key);
751
+ }
752
+ }
676
753
  let i = 0;
677
754
  let passed = true;
678
755
  for (const item of value) {
@@ -694,21 +771,20 @@ var Validator = class {
694
771
  let passed = true;
695
772
  const valueKeys = new Set(Object.keys(value));
696
773
  const typeKeys = new Set();
697
- const skipList = new Set();
774
+ let skipList;
698
775
  if (this.opts.skipList) {
699
- const path$2 = this.stackPath.length > 1 ? `${this.path}.` : "";
700
- this.opts.skipList.forEach((item) => {
701
- if (item.startsWith(path$2)) {
702
- const key = item.slice(path$2.length);
703
- skipList.add(key);
704
- valueKeys.delete(key);
705
- }
706
- });
776
+ const path$2 = this.stackPath.length > 1 ? `${this.cachedPath}.` : "";
777
+ for (const item of this.opts.skipList) if (item.startsWith(path$2)) {
778
+ const key = item.slice(path$2.length);
779
+ if (!skipList) skipList = new Set();
780
+ skipList.add(key);
781
+ valueKeys.delete(key);
782
+ }
707
783
  }
708
784
  let partialFunctionMatched = false;
709
- if (typeof this.opts.partial === "function") partialFunctionMatched = this.opts.partial(def, this.path);
785
+ if (typeof this.opts.partial === "function") partialFunctionMatched = this.opts.partial(def, this.cachedPath);
710
786
  for (const [key, item] of def.type.props.entries()) {
711
- if (skipList.has(key) || isPhantomType(item)) continue;
787
+ if (skipList && skipList.has(key) || isPhantomType(item)) continue;
712
788
  typeKeys.add(key);
713
789
  if (value[key] === undefined) {
714
790
  if (partialFunctionMatched || this.opts.partial === "deep" || this.opts.partial === true && this.stackPath.length <= 1) continue;
@@ -729,19 +805,21 @@ else {
729
805
  def: propDef
730
806
  });
731
807
  if (matched.length > 0) {
808
+ this.push(key);
732
809
  let keyPassed = false;
733
- for (const { def: def$1 } of matched) if (this.validateSafe(def$1, value[key])) {
734
- this.pop(false);
735
- keyPassed = true;
736
- break;
810
+ for (const { def: propDef } of matched) {
811
+ if (this.validateSafe(propDef, value[key])) {
812
+ keyPassed = true;
813
+ break;
814
+ }
815
+ this.clear();
737
816
  }
738
817
  if (!keyPassed) {
739
- this.push(key);
740
818
  this.validateSafe(matched[0].def, value[key]);
741
819
  this.pop(true);
742
820
  passed = false;
743
821
  if (this.isLimitExceeded()) return false;
744
- }
822
+ } else this.pop(false);
745
823
  } else if (this.opts.unknownProps !== "ignore") {
746
824
  if (this.opts.unknownProps === "error") {
747
825
  this.push(key);
@@ -894,11 +972,13 @@ else {
894
972
  /** Validation errors collected during the last {@link validate} call. */ _define_property$1(this, "errors", void 0);
895
973
  _define_property$1(this, "stackErrors", void 0);
896
974
  _define_property$1(this, "stackPath", void 0);
975
+ _define_property$1(this, "cachedPath", void 0);
897
976
  _define_property$1(this, "context", void 0);
898
977
  this.def = def;
899
978
  this.errors = [];
900
979
  this.stackErrors = [];
901
980
  this.stackPath = [];
981
+ this.cachedPath = "";
902
982
  this.opts = {
903
983
  partial: false,
904
984
  unknownProps: "error",
@@ -1042,6 +1122,24 @@ function isPhantomType(def) {
1042
1122
  return def.type.kind === "" && def.type.designType === "phantom";
1043
1123
  }
1044
1124
 
1125
+ //#endregion
1126
+ //#region packages/typescript/src/traverse.ts
1127
+ function forAnnotatedType(def, handlers) {
1128
+ switch (def.type.kind) {
1129
+ case "": {
1130
+ const typed = def;
1131
+ if (handlers.phantom && typed.type.designType === "phantom") return handlers.phantom(typed);
1132
+ return handlers.final(typed);
1133
+ }
1134
+ case "object": return handlers.object(def);
1135
+ case "array": return handlers.array(def);
1136
+ case "union": return handlers.union(def);
1137
+ case "intersection": return handlers.intersection(def);
1138
+ case "tuple": return handlers.tuple(def);
1139
+ default: throw new Error(`Unknown type kind "${def.type.kind}"`);
1140
+ }
1141
+ }
1142
+
1045
1143
  //#endregion
1046
1144
  //#region packages/typescript/src/json-schema.ts
1047
1145
  /**
@@ -1214,24 +1312,25 @@ var JsRenderer = class extends BaseRenderer {
1214
1312
  this.writeln("// prettier-ignore-start");
1215
1313
  this.writeln("/* eslint-disable */");
1216
1314
  this.writeln("/* oxlint-disable */");
1315
+ let hasMutatingAnnotate = false;
1316
+ const nodesByName = new Map();
1317
+ for (const node of this.doc.nodes) {
1318
+ if (node.entity === "annotate" && node.isMutating) hasMutatingAnnotate = true;
1319
+ if (node.__typeId !== null && node.__typeId !== undefined && node.id) {
1320
+ const name = node.id;
1321
+ if (!nodesByName.has(name)) nodesByName.set(name, []);
1322
+ nodesByName.get(name).push(node);
1323
+ }
1324
+ }
1325
+ for (const [name, nodes] of nodesByName) if (nodes.length === 1) this.typeIds.set(nodes[0], name);
1326
+ else for (let i = 0; i < nodes.length; i++) this.typeIds.set(nodes[i], `${name}__${i + 1}`);
1217
1327
  const imports = ["defineAnnotatedType as $", "annotate as $a"];
1218
- const hasMutatingAnnotate = this.doc.nodes.some((n) => n.entity === "annotate" && n.isMutating);
1219
1328
  if (hasMutatingAnnotate) imports.push("cloneRefProp as $c");
1220
1329
  const jsonSchemaMode = resolveJsonSchemaMode(this.opts);
1221
1330
  if (jsonSchemaMode === "lazy") imports.push("buildJsonSchema as $$");
1222
1331
  if (this.opts?.exampleData) imports.push("createDataFromAnnotatedType as $e");
1223
1332
  if (jsonSchemaMode === false) imports.push("throwFeatureDisabled as $d");
1224
1333
  this.writeln(`import { ${imports.join(", ")} } from "@atscript/typescript/utils"`);
1225
- const nameCounts = new Map();
1226
- const nodesByName = new Map();
1227
- for (const node of this.doc.nodes) if (node.__typeId !== null && node.__typeId !== undefined && node.id) {
1228
- const name = node.id;
1229
- nameCounts.set(name, (nameCounts.get(name) || 0) + 1);
1230
- if (!nodesByName.has(name)) nodesByName.set(name, []);
1231
- nodesByName.get(name).push(node);
1232
- }
1233
- for (const [name, nodes] of nodesByName) if (nodes.length === 1) this.typeIds.set(nodes[0], name);
1234
- else for (let i = 0; i < nodes.length; i++) this.typeIds.set(nodes[i], `${name}__${i + 1}`);
1235
1334
  }
1236
1335
  buildAdHocMap(annotateNodes) {
1237
1336
  const map = new Map();
@@ -1240,7 +1339,8 @@ else for (let i = 0; i < nodes.length; i++) this.typeIds.set(nodes[i], `${name}_
1240
1339
  const anns = entry.annotations || [];
1241
1340
  if (anns.length > 0) {
1242
1341
  const existing = map.get(path$2);
1243
- map.set(path$2, existing ? [...existing, ...anns] : anns);
1342
+ if (existing) existing.push(...anns);
1343
+ else map.set(path$2, [...anns]);
1244
1344
  }
1245
1345
  }
1246
1346
  return map.size > 0 ? map : null;
@@ -1299,35 +1399,20 @@ else def = def.getDefinition() || def;
1299
1399
  this.renderExampleDataMethod(node);
1300
1400
  }
1301
1401
  renderInterface(node) {
1302
- this.writeln();
1303
- const exported = node.token("export")?.text === "export";
1304
- this.write(exported ? "export " : "");
1305
- this.write(`class ${node.id} `);
1306
- this.blockln("{}");
1307
- this.renderClassStatics(node);
1308
- this.popln();
1309
- this.postAnnotate.push(node);
1310
- this.writeln();
1402
+ this.renderDefinitionClass(node);
1311
1403
  }
1312
1404
  renderType(node) {
1313
- this.writeln();
1314
- const exported = node.token("export")?.text === "export";
1315
- this.write(exported ? "export " : "");
1316
- this.write(`class ${node.id} `);
1317
- this.blockln("{}");
1318
- this.renderClassStatics(node);
1319
- this.popln();
1320
- this.postAnnotate.push(node);
1321
- this.writeln();
1405
+ this.renderDefinitionClass(node);
1322
1406
  }
1323
1407
  renderAnnotate(node) {
1324
1408
  if (node.isMutating) {
1325
1409
  this.postAnnotate.push(node);
1326
1410
  return;
1327
1411
  }
1328
- const targetName = node.targetName;
1329
- const unwound = this.doc.unwindType(targetName);
1330
- if (!unwound?.def) return;
1412
+ if (!this.doc.unwindType(node.targetName)?.def) return;
1413
+ this.renderDefinitionClass(node);
1414
+ }
1415
+ renderDefinitionClass(node) {
1331
1416
  this.writeln();
1332
1417
  const exported = node.token("export")?.text === "export";
1333
1418
  this.write(exported ? "export " : "");
@@ -1485,6 +1570,11 @@ else handle.prop(prop.id, propHandle.$type);
1485
1570
  handle.annotate(a.name, true);
1486
1571
  break;
1487
1572
  }
1573
+ case "expect.array.uniqueItems":
1574
+ case "expect.array.key": {
1575
+ handle.annotate(a.name, { message: a.args[0]?.text });
1576
+ break;
1577
+ }
1488
1578
  default:
1489
1579
  }
1490
1580
  });
@@ -1552,10 +1642,7 @@ else handle.prop(prop.id, propHandle.$type);
1552
1642
  this.writeln(`$("array"${name ? `, ${name}` : ""})`).indent().defineArray(node).unindent();
1553
1643
  return this;
1554
1644
  }
1555
- default: {
1556
- console.log("!!!!!!! UNKNOWN", node.entity);
1557
- return this;
1558
- }
1645
+ default: return this;
1559
1646
  }
1560
1647
  }
1561
1648
  defineConst(node) {
@@ -1767,40 +1854,25 @@ else targetValue = "true";
1767
1854
  this.writeln(`$c(${clone.parentPath}, "${escapeQuotes(clone.propName)}")`);
1768
1855
  }
1769
1856
  }
1770
- for (const { entry, accessors } of entryAccessors) {
1771
- const anns = entry.annotations;
1772
- for (const accessor of accessors) {
1773
- const cleared = new Set();
1774
- for (const an of anns) {
1775
- const { value, multiple } = this.computeAnnotationValue(entry, an);
1776
- if (multiple) {
1777
- if (!cleared.has(an.name)) {
1778
- const spec = this.doc.resolveAnnotation(an.name);
1779
- if (!spec || spec.config.mergeStrategy !== "append") this.writeln(`${accessor}.metadata.delete("${escapeQuotes(an.name)}")`);
1780
- cleared.add(an.name);
1781
- }
1782
- this.writeln(`$a(${accessor}.metadata, "${escapeQuotes(an.name)}", ${value}, true)`);
1783
- } else this.writeln(`$a(${accessor}.metadata, "${escapeQuotes(an.name)}", ${value})`);
1784
- }
1785
- }
1786
- }
1857
+ for (const { entry, accessors } of entryAccessors) for (const accessor of accessors) this.emitMutatingAnnotations(entry, entry.annotations, accessor);
1787
1858
  const topAnnotations = node.annotations;
1788
- if (topAnnotations && topAnnotations.length > 0) {
1789
- const cleared = new Set();
1790
- for (const an of topAnnotations) {
1791
- const { value, multiple } = this.computeAnnotationValue(node, an);
1792
- if (multiple) {
1793
- if (!cleared.has(an.name)) {
1794
- const spec = this.doc.resolveAnnotation(an.name);
1795
- if (!spec || spec.config.mergeStrategy !== "append") this.writeln(`${targetName}.metadata.delete("${escapeQuotes(an.name)}")`);
1796
- cleared.add(an.name);
1797
- }
1798
- this.writeln(`$a(${targetName}.metadata, "${escapeQuotes(an.name)}", ${value}, true)`);
1799
- } else this.writeln(`$a(${targetName}.metadata, "${escapeQuotes(an.name)}", ${value})`);
1800
- }
1801
- }
1859
+ if (topAnnotations && topAnnotations.length > 0) this.emitMutatingAnnotations(node, topAnnotations, targetName);
1802
1860
  this.writeln();
1803
1861
  }
1862
+ emitMutatingAnnotations(node, annotations, accessor) {
1863
+ const cleared = new Set();
1864
+ for (const an of annotations) {
1865
+ const { value, multiple } = this.computeAnnotationValue(node, an);
1866
+ if (multiple) {
1867
+ if (!cleared.has(an.name)) {
1868
+ const spec = this.doc.resolveAnnotation(an.name);
1869
+ if (!spec || spec.config.mergeStrategy !== "append") this.writeln(`${accessor}.metadata.delete("${escapeQuotes(an.name)}")`);
1870
+ cleared.add(an.name);
1871
+ }
1872
+ this.writeln(`$a(${accessor}.metadata, "${escapeQuotes(an.name)}", ${value}, true)`);
1873
+ } else this.writeln(`$a(${accessor}.metadata, "${escapeQuotes(an.name)}", ${value})`);
1874
+ }
1875
+ }
1804
1876
  resolveTargetDef(targetName) {
1805
1877
  const unwound = this.doc.unwindType(targetName);
1806
1878
  if (!unwound?.def) return undefined;