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