@atscript/typescript 0.1.31 → 0.1.33

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.mjs CHANGED
@@ -116,6 +116,9 @@ else obj[key] = value;
116
116
  return obj;
117
117
  }
118
118
  var BaseRenderer = class extends CodePrinter {
119
+ get unused() {
120
+ return this._unused ?? (this._unused = new Set(this.doc.getUnusedTokens().map((t) => t.text)));
121
+ }
119
122
  pre() {}
120
123
  post() {}
121
124
  render() {
@@ -169,15 +172,14 @@ var BaseRenderer = class extends CodePrinter {
169
172
  }
170
173
  }
171
174
  constructor(doc) {
172
- super(), _define_property$3(this, "doc", void 0), _define_property$3(this, "unused", void 0), this.doc = doc;
173
- this.unused = new Set(this.doc.getUnusedTokens().map((t) => t.text));
175
+ super(), _define_property$3(this, "doc", void 0), _define_property$3(this, "_unused", void 0), this.doc = doc;
174
176
  }
175
177
  };
176
178
 
177
179
  //#endregion
178
180
  //#region packages/typescript/src/codegen/utils.ts
181
+ const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
179
182
  function wrapProp(name) {
180
- const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
181
183
  if (!validIdentifier.test(name)) return `"${escapeQuotes(name)}"`;
182
184
  return name;
183
185
  }
@@ -264,13 +266,14 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
264
266
  }
265
267
  if (isPrimitive(def)) return this.write(renderPrimitiveTypeDef(def.config.type));
266
268
  }
267
- renderStructure(struct, asClass, interfaceNode) {
269
+ renderStructure(struct, asClass, interfaceNode, filterProps) {
268
270
  this.blockln("{}");
269
271
  const patterns = [];
270
272
  const propsDefs = new Set();
271
- for (const prop of Array.from(struct.props.values())) {
273
+ for (const prop of struct.props.values()) {
274
+ if (filterProps?.has(prop.id)) continue;
272
275
  if (prop.token("identifier")?.pattern) {
273
- patterns.push(prop);
276
+ if (!filterProps) patterns.push(prop);
274
277
  continue;
275
278
  }
276
279
  const phantomType = this.phantomPropType(prop.getDefinition());
@@ -286,34 +289,36 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
286
289
  }
287
290
  if (patterns.length > 0) {
288
291
  this.write(`[key: string]: `);
289
- if (patterns.length > 0) {
290
- for (const prop of patterns) propsDefs.add(this.renderTypeDefString(prop.getDefinition()));
291
- const defs = Array.from(propsDefs);
292
- if (defs.length > 1) {
293
- this.indent();
294
- for (const def of defs) {
295
- this.writeln();
296
- this.write("| ");
297
- def.split("\n").forEach((l) => this.write(l.trim()));
298
- }
299
- this.unindent();
292
+ for (const prop of patterns) propsDefs.add(this.renderTypeDefString(prop.getDefinition()));
293
+ const defs = Array.from(propsDefs);
294
+ if (defs.length > 1) {
295
+ this.indent();
296
+ for (const def of defs) {
300
297
  this.writeln();
301
- } else defs[0].split("\n").forEach((l) => this.writeln(l));
302
- }
303
- }
304
- if (asClass) {
305
- this.writeln("static __is_atscript_annotated_type: true");
306
- this.writeln(`static type: TAtscriptTypeObject<keyof ${asClass}, ${asClass}>`);
307
- this.writeln(`static metadata: TMetadataMap<AtscriptMetadata>`);
308
- this.writeln(`static validator: (opts?: Partial<TValidatorOptions>) => Validator<typeof ${asClass}>`);
309
- 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. */");
310
- this.writeln("static toJsonSchema: () => any");
311
- if (!this.opts?.exampleData) this.writeln("/** @deprecated Example Data support is disabled. To enable, set `exampleData: true` in tsPlugin options. */");
312
- this.writeln("static toExampleData?: () => any");
313
- if (interfaceNode && this.hasDbTable(interfaceNode)) this.renderFlat(interfaceNode);
298
+ this.write("| ");
299
+ def.split("\n").forEach((l) => this.write(l.trim()));
300
+ }
301
+ this.unindent();
302
+ this.writeln();
303
+ } else defs[0].split("\n").forEach((l) => this.writeln(l));
314
304
  }
305
+ if (asClass) this.renderStaticDeclarations(asClass, interfaceNode);
315
306
  this.pop();
316
307
  }
308
+ renderStaticDeclarations(asClass, interfaceNode) {
309
+ this.writeln("static __is_atscript_annotated_type: true");
310
+ this.writeln(`static type: TAtscriptTypeObject<keyof ${asClass}, ${asClass}>`);
311
+ this.writeln(`static metadata: TMetadataMap<AtscriptMetadata>`);
312
+ this.writeln(`static validator: (opts?: Partial<TValidatorOptions>) => Validator<typeof ${asClass}>`);
313
+ 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. */");
314
+ this.writeln("static toJsonSchema: () => any");
315
+ if (!this.opts?.exampleData) this.writeln("/** @deprecated Example Data support is disabled. To enable, set `exampleData: true` in tsPlugin options. */");
316
+ this.writeln("static toExampleData?: () => any");
317
+ if (interfaceNode && this.hasDbTable(interfaceNode)) {
318
+ this.renderFlat(interfaceNode);
319
+ this.renderPk(interfaceNode);
320
+ }
321
+ }
317
322
  renderInterface(node) {
318
323
  this.writeln();
319
324
  const exported = node.token("export")?.text === "export";
@@ -337,7 +342,7 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
337
342
  }
338
343
  if (!firstParentProps && isStructure(fpDef)) firstParentProps = fpDef.props;
339
344
  }
340
- this.renderStructureFiltered(resolved, node.id, firstParentProps, node);
345
+ this.renderStructure(resolved, node.id, node, firstParentProps);
341
346
  } else this.writeln("{}");
342
347
  } else {
343
348
  this.write(`class ${node.id} `);
@@ -347,35 +352,6 @@ else this.writeln("{}");
347
352
  }
348
353
  this.writeln();
349
354
  }
350
- /**
351
- * Renders a structure block, optionally filtering out props that exist in a parent.
352
- */ renderStructureFiltered(struct, asClass, filterProps, interfaceNode) {
353
- if (!filterProps) return this.renderStructure(struct, asClass, interfaceNode);
354
- this.blockln("{}");
355
- for (const prop of Array.from(struct.props.values())) {
356
- if (filterProps.has(prop.id)) continue;
357
- if (prop.token("identifier")?.pattern) continue;
358
- const phantomType = this.phantomPropType(prop.getDefinition());
359
- if (phantomType) {
360
- this.writeln(`// ${prop.id}: ${phantomType}`);
361
- continue;
362
- }
363
- const optional = !!prop.token("optional");
364
- this.write(wrapProp(prop.id), optional ? "?" : "", ": ");
365
- const renderedDef = this.renderTypeDefString(prop.getDefinition());
366
- renderedDef.split("\n").forEach((l) => this.writeln(l));
367
- }
368
- this.writeln("static __is_atscript_annotated_type: true");
369
- this.writeln(`static type: TAtscriptTypeObject<keyof ${asClass}, ${asClass}>`);
370
- this.writeln(`static metadata: TMetadataMap<AtscriptMetadata>`);
371
- this.writeln(`static validator: (opts?: Partial<TValidatorOptions>) => Validator<typeof ${asClass}>`);
372
- 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. */");
373
- this.writeln("static toJsonSchema: () => any");
374
- if (!this.opts?.exampleData) this.writeln("/** @deprecated Example Data support is disabled. To enable, set `exampleData: true` in tsPlugin options. */");
375
- this.writeln("static toExampleData?: () => any");
376
- if (interfaceNode && this.hasDbTable(interfaceNode)) this.renderFlat(interfaceNode);
377
- this.pop();
378
- }
379
355
  renderType(node) {
380
356
  this.writeln();
381
357
  const exported = node.token("export")?.text === "export";
@@ -473,6 +449,88 @@ else {
473
449
  }
474
450
  this.pop();
475
451
  }
452
+ /**
453
+ * Renders the `static __pk` property — the primary key type for type-safe
454
+ * `deleteOne`/`findById` signatures on `AtscriptDbTable`.
455
+ *
456
+ * - **Single PK** (one `@meta.id`) → `static __pk: <scalar type>`
457
+ * - **Compound PK** (multiple `@meta.id`) → `static __pk: { field1: Type1; field2: Type2 }`
458
+ * - **No PK** → no `__pk` emitted (unless unique indexes exist)
459
+ * - **Unique indexes** (`@db.index.unique`) → appended as union members
460
+ * - **Mongo collection** → always includes `string` (ObjectId) in the union;
461
+ * if no `@meta.id` fields, `__pk` is just `string`
462
+ */ renderPk(node) {
463
+ const isMongoCollection = !!node.annotations?.some((a) => a.name === "db.mongo.collection");
464
+ let struct;
465
+ if (node.hasExtends) struct = this.doc.resolveInterfaceExtends(node);
466
+ if (!struct) struct = node.getDefinition();
467
+ if (!struct || !isStructure(struct)) return;
468
+ const structNode = struct;
469
+ const pkProps = [];
470
+ const uniqueByIndex = new Map();
471
+ for (const [name, prop] of structNode.props) {
472
+ if (prop.token("identifier")?.pattern) continue;
473
+ if (isMongoCollection && name === "_id") continue;
474
+ if (prop.countAnnotations("meta.id") > 0) pkProps.push({
475
+ name,
476
+ prop
477
+ });
478
+ if (prop.annotations) {
479
+ for (const ann of prop.annotations) if (ann.name === "db.index.unique") {
480
+ const indexName = ann.args[0]?.text ?? name;
481
+ let group = uniqueByIndex.get(indexName);
482
+ if (!group) {
483
+ group = [];
484
+ uniqueByIndex.set(indexName, group);
485
+ }
486
+ group.push({
487
+ name,
488
+ prop
489
+ });
490
+ }
491
+ }
492
+ }
493
+ const uniqueProps = [];
494
+ const pkNames = new Set(pkProps.map((p) => p.name));
495
+ for (const fields of uniqueByIndex.values()) if (fields.length === 1 && !pkNames.has(fields[0].name)) uniqueProps.push(fields[0]);
496
+ if (pkProps.length === 0 && uniqueProps.length === 0 && !isMongoCollection) return;
497
+ let mongoIdType;
498
+ if (isMongoCollection) {
499
+ const idProp = structNode.props.get("_id");
500
+ if (idProp) mongoIdType = this.renderTypeDefString(idProp.getDefinition()).trim();
501
+ mongoIdType ?? (mongoIdType = "string");
502
+ }
503
+ const uniqueTypes = [];
504
+ const seenTypes = new Set();
505
+ for (const { prop } of uniqueProps) {
506
+ const rendered = this.renderTypeDefString(prop.getDefinition()).trim();
507
+ if (!seenTypes.has(rendered)) {
508
+ seenTypes.add(rendered);
509
+ uniqueTypes.push(rendered);
510
+ }
511
+ }
512
+ this.writeln();
513
+ const uniqueSuffix = uniqueTypes.length > 0 ? ` | ${uniqueTypes.join(" | ")}` : "";
514
+ if (pkProps.length === 0 && !isMongoCollection) this.writeln(`static __pk: ${uniqueTypes.join(" | ")}`);
515
+ else if (pkProps.length === 0) this.writeln(`static __pk: ${mongoIdType}${uniqueSuffix}`);
516
+ else if (pkProps.length === 1) {
517
+ this.write("static __pk: ");
518
+ if (isMongoCollection) this.write(`${mongoIdType} | `);
519
+ const renderedDef = this.renderTypeDefString(pkProps[0].prop.getDefinition()).trim();
520
+ this.writeln(`${renderedDef}${uniqueSuffix}`);
521
+ } else {
522
+ this.write("static __pk: ");
523
+ if (isMongoCollection) this.write(`${mongoIdType} | `);
524
+ this.blockln("{}");
525
+ for (const { name, prop } of pkProps) {
526
+ this.write(wrapProp(name), ": ");
527
+ const renderedDef = this.renderTypeDefString(prop.getDefinition());
528
+ renderedDef.split("\n").forEach((l) => this.writeln(l));
529
+ }
530
+ this.pop();
531
+ this.writeln(uniqueSuffix);
532
+ }
533
+ }
476
534
  phantomPropType(def) {
477
535
  if (!def) return undefined;
478
536
  if (isPrimitive(def) && def.config.type === "phantom") return def.id;
@@ -521,24 +579,6 @@ function renderPrimitiveTypeDef(def) {
521
579
  }
522
580
  }
523
581
 
524
- //#endregion
525
- //#region packages/typescript/src/traverse.ts
526
- function forAnnotatedType(def, handlers) {
527
- switch (def.type.kind) {
528
- case "": {
529
- const typed = def;
530
- if (handlers.phantom && typed.type.designType === "phantom") return handlers.phantom(typed);
531
- return handlers.final(typed);
532
- }
533
- case "object": return handlers.object(def);
534
- case "array": return handlers.array(def);
535
- case "union": return handlers.union(def);
536
- case "intersection": return handlers.intersection(def);
537
- case "tuple": return handlers.tuple(def);
538
- default: throw new Error(`Unknown type kind "${def.type.kind}"`);
539
- }
540
- }
541
-
542
582
  //#endregion
543
583
  //#region packages/typescript/src/validator.ts
544
584
  function _define_property$1(obj, key, value) {
@@ -554,28 +594,35 @@ else obj[key] = value;
554
594
  const regexCache = new Map();
555
595
  var Validator = class {
556
596
  isLimitExceeded() {
557
- if (this.stackErrors.length > 0) return this.stackErrors[this.stackErrors.length - 1].length >= this.opts.errorLimit;
597
+ if (this.stackErrors.length > 0) {
598
+ const top = this.stackErrors[this.stackErrors.length - 1];
599
+ return top !== null && top.length >= this.opts.errorLimit;
600
+ }
558
601
  return this.errors.length >= this.opts.errorLimit;
559
602
  }
560
603
  push(name) {
561
604
  this.stackPath.push(name);
562
- this.stackErrors.push([]);
605
+ this.stackErrors.push(null);
606
+ this.cachedPath = this.stackPath.length <= 1 ? "" : this.stackPath[1] + (this.stackPath.length > 2 ? "." + this.stackPath.slice(2).join(".") : "");
563
607
  }
564
608
  pop(saveErrors) {
565
609
  this.stackPath.pop();
566
610
  const popped = this.stackErrors.pop();
567
- if (saveErrors && popped?.length) popped.forEach((error) => {
568
- this.error(error.message, error.path, error.details);
569
- });
611
+ if (saveErrors && popped !== null && popped !== undefined && popped.length > 0) for (const err of popped) this.error(err.message, err.path, err.details);
612
+ this.cachedPath = this.stackPath.length <= 1 ? "" : this.stackPath[1] + (this.stackPath.length > 2 ? "." + this.stackPath.slice(2).join(".") : "");
570
613
  return popped;
571
614
  }
572
615
  clear() {
573
- this.stackErrors[this.stackErrors.length - 1] = [];
616
+ this.stackErrors[this.stackErrors.length - 1] = null;
574
617
  }
575
618
  error(message, path$1, details) {
576
- const errors = this.stackErrors[this.stackErrors.length - 1] || this.errors;
619
+ let errors = this.stackErrors[this.stackErrors.length - 1];
620
+ if (!errors) if (this.stackErrors.length > 0) {
621
+ errors = [];
622
+ this.stackErrors[this.stackErrors.length - 1] = errors;
623
+ } else errors = this.errors;
577
624
  const error = {
578
- path: path$1 || this.path,
625
+ path: path$1 || this.cachedPath,
579
626
  message
580
627
  };
581
628
  if (details?.length) error.details = details;
@@ -595,9 +642,10 @@ var Validator = class {
595
642
  * @returns `true` if the value matches the type definition.
596
643
  * @throws {ValidatorError} When validation fails and `safe` is not `true`.
597
644
  */ validate(value, safe, context) {
598
- this.push("");
599
645
  this.errors = [];
600
646
  this.stackErrors = [];
647
+ this.stackPath = [""];
648
+ this.cachedPath = "";
601
649
  this.context = context;
602
650
  const passed = this.validateSafe(this.def, value);
603
651
  this.pop(!passed);
@@ -611,7 +659,7 @@ var Validator = class {
611
659
  validateSafe(def, value) {
612
660
  if (this.isLimitExceeded()) return false;
613
661
  if (!isAnnotatedType(def)) throw new Error("Can not validate not-annotated type");
614
- if (typeof this.opts.replace === "function") def = this.opts.replace(def, this.path);
662
+ if (typeof this.opts.replace === "function") def = this.opts.replace(def, this.cachedPath);
615
663
  if (def.optional && value === undefined) return true;
616
664
  for (const plugin of this.opts.plugins) {
617
665
  const result = plugin(this, def, value);
@@ -620,18 +668,21 @@ var Validator = class {
620
668
  return this.validateAnnotatedType(def, value);
621
669
  }
622
670
  get path() {
623
- return this.stackPath.slice(1).join(".");
671
+ return this.cachedPath;
624
672
  }
625
673
  validateAnnotatedType(def, value) {
626
- return forAnnotatedType(def, {
627
- final: (d) => this.validatePrimitive(d, value),
628
- phantom: () => true,
629
- object: (d) => this.validateObject(d, value),
630
- array: (d) => this.validateArray(d, value),
631
- union: (d) => this.validateUnion(d, value),
632
- intersection: (d) => this.validateIntersection(d, value),
633
- tuple: (d) => this.validateTuple(d, value)
634
- });
674
+ switch (def.type.kind) {
675
+ case "": {
676
+ if (def.type.designType === "phantom") return true;
677
+ return this.validatePrimitive(def, value);
678
+ }
679
+ case "object": return this.validateObject(def, value);
680
+ case "array": return this.validateArray(def, value);
681
+ case "union": return this.validateUnion(def, value);
682
+ case "intersection": return this.validateIntersection(def, value);
683
+ case "tuple": return this.validateTuple(def, value);
684
+ default: throw new Error(`Unknown type kind "${def.type.kind}"`);
685
+ }
635
686
  }
636
687
  validateUnion(def, value) {
637
688
  let i = 0;
@@ -695,6 +746,30 @@ var Validator = class {
695
746
  return false;
696
747
  }
697
748
  }
749
+ const uniqueItems = def.metadata.get("expect.array.uniqueItems");
750
+ if (uniqueItems) {
751
+ const separator = "▼↩";
752
+ const seen = new Set();
753
+ const keyProps = new Set();
754
+ if (def.type.of.type.kind === "object") {
755
+ for (const [key, val] of def.type.of.type.props.entries()) if (val.metadata.get("expect.array.key")) keyProps.add(key);
756
+ }
757
+ for (let idx = 0; idx < value.length; idx++) {
758
+ const item = value[idx];
759
+ let key;
760
+ if (keyProps.size > 0) {
761
+ key = "";
762
+ for (const prop of keyProps) key += JSON.stringify(item[prop]) + separator;
763
+ } else key = JSON.stringify(item);
764
+ if (seen.has(key)) {
765
+ this.push(String(idx));
766
+ this.error(uniqueItems.message || "Duplicate items are not allowed");
767
+ this.pop(true);
768
+ return false;
769
+ }
770
+ seen.add(key);
771
+ }
772
+ }
698
773
  let i = 0;
699
774
  let passed = true;
700
775
  for (const item of value) {
@@ -716,21 +791,20 @@ var Validator = class {
716
791
  let passed = true;
717
792
  const valueKeys = new Set(Object.keys(value));
718
793
  const typeKeys = new Set();
719
- const skipList = new Set();
794
+ let skipList;
720
795
  if (this.opts.skipList) {
721
- const path$1 = this.stackPath.length > 1 ? `${this.path}.` : "";
722
- this.opts.skipList.forEach((item) => {
723
- if (item.startsWith(path$1)) {
724
- const key = item.slice(path$1.length);
725
- skipList.add(key);
726
- valueKeys.delete(key);
727
- }
728
- });
796
+ const path$1 = this.stackPath.length > 1 ? `${this.cachedPath}.` : "";
797
+ for (const item of this.opts.skipList) if (item.startsWith(path$1)) {
798
+ const key = item.slice(path$1.length);
799
+ if (!skipList) skipList = new Set();
800
+ skipList.add(key);
801
+ valueKeys.delete(key);
802
+ }
729
803
  }
730
804
  let partialFunctionMatched = false;
731
- if (typeof this.opts.partial === "function") partialFunctionMatched = this.opts.partial(def, this.path);
805
+ if (typeof this.opts.partial === "function") partialFunctionMatched = this.opts.partial(def, this.cachedPath);
732
806
  for (const [key, item] of def.type.props.entries()) {
733
- if (skipList.has(key) || isPhantomType(item)) continue;
807
+ if (skipList && skipList.has(key) || isPhantomType(item)) continue;
734
808
  typeKeys.add(key);
735
809
  if (value[key] === undefined) {
736
810
  if (partialFunctionMatched || this.opts.partial === "deep" || this.opts.partial === true && this.stackPath.length <= 1) continue;
@@ -751,19 +825,21 @@ else {
751
825
  def: propDef
752
826
  });
753
827
  if (matched.length > 0) {
828
+ this.push(key);
754
829
  let keyPassed = false;
755
- for (const { def: def$1 } of matched) if (this.validateSafe(def$1, value[key])) {
756
- this.pop(false);
757
- keyPassed = true;
758
- break;
830
+ for (const { def: propDef } of matched) {
831
+ if (this.validateSafe(propDef, value[key])) {
832
+ keyPassed = true;
833
+ break;
834
+ }
835
+ this.clear();
759
836
  }
760
837
  if (!keyPassed) {
761
- this.push(key);
762
838
  this.validateSafe(matched[0].def, value[key]);
763
839
  this.pop(true);
764
840
  passed = false;
765
841
  if (this.isLimitExceeded()) return false;
766
- }
842
+ } else this.pop(false);
767
843
  } else if (this.opts.unknownProps !== "ignore") {
768
844
  if (this.opts.unknownProps === "error") {
769
845
  this.push(key);
@@ -916,11 +992,13 @@ else {
916
992
  /** Validation errors collected during the last {@link validate} call. */ _define_property$1(this, "errors", void 0);
917
993
  _define_property$1(this, "stackErrors", void 0);
918
994
  _define_property$1(this, "stackPath", void 0);
995
+ _define_property$1(this, "cachedPath", void 0);
919
996
  _define_property$1(this, "context", void 0);
920
997
  this.def = def;
921
998
  this.errors = [];
922
999
  this.stackErrors = [];
923
1000
  this.stackPath = [];
1001
+ this.cachedPath = "";
924
1002
  this.opts = {
925
1003
  partial: false,
926
1004
  unknownProps: "error",
@@ -938,6 +1016,24 @@ var ValidatorError = class extends Error {
938
1016
 
939
1017
  //#endregion
940
1018
  //#region packages/typescript/src/annotated-type.ts
1019
+ const COMPLEX_KINDS = new Set([
1020
+ "union",
1021
+ "intersection",
1022
+ "tuple"
1023
+ ]);
1024
+ /** Shared validator method reused by all annotated type nodes. */ function validatorMethod(opts) {
1025
+ return new Validator(this, opts);
1026
+ }
1027
+ function createAnnotatedTypeNode(type, metadata, opts) {
1028
+ return {
1029
+ __is_atscript_annotated_type: true,
1030
+ type,
1031
+ metadata,
1032
+ validator: validatorMethod,
1033
+ id: opts?.id,
1034
+ optional: opts?.optional
1035
+ };
1036
+ }
941
1037
  function isAnnotatedType(type) {
942
1038
  return type && type.__is_atscript_annotated_type;
943
1039
  }
@@ -954,38 +1050,24 @@ function defineAnnotatedType(_kind, base) {
954
1050
  const kind = _kind || "";
955
1051
  const type = base?.type || {};
956
1052
  type.kind = kind;
957
- if ([
958
- "union",
959
- "intersection",
960
- "tuple"
961
- ].includes(kind)) type.items = [];
1053
+ if (COMPLEX_KINDS.has(kind)) type.items = [];
962
1054
  if (kind === "object") {
963
1055
  type.props = new Map();
964
1056
  type.propsPatterns = [];
965
1057
  }
966
1058
  type.tags = new Set();
967
1059
  const metadata = base?.metadata || new Map();
968
- if (base) Object.assign(base, {
969
- __is_atscript_annotated_type: true,
970
- metadata,
971
- type,
972
- validator(opts) {
973
- return new Validator(this, opts);
974
- }
975
- });
976
- else base = {
1060
+ const payload = {
977
1061
  __is_atscript_annotated_type: true,
978
1062
  metadata,
979
1063
  type,
980
- validator(opts) {
981
- return new Validator(this, opts);
982
- }
1064
+ validator: validatorMethod
983
1065
  };
1066
+ base = base ? Object.assign(base, payload) : payload;
984
1067
  const handle = {
985
1068
  $type: base,
986
1069
  $def: type,
987
1070
  $metadata: metadata,
988
- _existingObject: undefined,
989
1071
  tags(...tags) {
990
1072
  for (const tag of tags) this.$def.tags.add(tag);
991
1073
  return this;
@@ -1026,27 +1108,18 @@ else base = {
1026
1108
  return this;
1027
1109
  },
1028
1110
  refTo(type$1, chain) {
1111
+ if (!isAnnotatedType(type$1)) throw new Error(`${type$1} is not annotated type`);
1029
1112
  let newBase = type$1;
1030
1113
  const typeName = type$1.name || "Unknown";
1031
- if (isAnnotatedType(newBase)) {
1032
- let keys = "";
1033
- for (const c of chain || []) {
1034
- keys += `["${c}"]`;
1035
- if (newBase.type.kind === "object" && newBase.type.props.has(c)) newBase = newBase.type.props.get(c);
1036
- else throw new Error(`Can't find prop ${typeName}${keys}`);
1114
+ if (chain) for (let i = 0; i < chain.length; i++) {
1115
+ const c = chain[i];
1116
+ if (newBase.type.kind === "object" && newBase.type.props.has(c)) newBase = newBase.type.props.get(c);
1117
+ else {
1118
+ const keys = chain.slice(0, i + 1).map((k) => `["${k}"]`).join("");
1119
+ throw new Error(`Can't find prop ${typeName}${keys}`);
1037
1120
  }
1038
- if (!newBase && keys) throw new Error(`Can't find prop ${typeName}${keys}`);
1039
- else if (!newBase) throw new Error(`"${typeName}" is not annotated type`);
1040
- this.$type = {
1041
- __is_atscript_annotated_type: true,
1042
- type: newBase.type,
1043
- metadata,
1044
- id: newBase.id,
1045
- validator(opts) {
1046
- return new Validator(this, opts);
1047
- }
1048
- };
1049
- } else throw new Error(`${type$1} is not annotated type`);
1121
+ }
1122
+ this.$type = createAnnotatedTypeNode(newBase.type, metadata, { id: newBase.id });
1050
1123
  return this;
1051
1124
  },
1052
1125
  annotate(key, value, asArray) {
@@ -1064,6 +1137,24 @@ function isPhantomType(def) {
1064
1137
  return def.type.kind === "" && def.type.designType === "phantom";
1065
1138
  }
1066
1139
 
1140
+ //#endregion
1141
+ //#region packages/typescript/src/traverse.ts
1142
+ function forAnnotatedType(def, handlers) {
1143
+ switch (def.type.kind) {
1144
+ case "": {
1145
+ const typed = def;
1146
+ if (handlers.phantom && typed.type.designType === "phantom") return handlers.phantom(typed);
1147
+ return handlers.final(typed);
1148
+ }
1149
+ case "object": return handlers.object(def);
1150
+ case "array": return handlers.array(def);
1151
+ case "union": return handlers.union(def);
1152
+ case "intersection": return handlers.intersection(def);
1153
+ case "tuple": return handlers.tuple(def);
1154
+ default: throw new Error(`Unknown type kind "${def.type.kind}"`);
1155
+ }
1156
+ }
1157
+
1067
1158
  //#endregion
1068
1159
  //#region packages/typescript/src/json-schema.ts
1069
1160
  /**
@@ -1078,10 +1169,10 @@ function isPhantomType(def) {
1078
1169
  const firstObj = items[0].type;
1079
1170
  const candidates = [];
1080
1171
  for (const [propName, propType] of firstObj.props.entries()) if (propType.type.kind === "" && propType.type.value !== undefined) candidates.push(propName);
1081
- const validCandidates = [];
1172
+ let result = null;
1082
1173
  for (const candidate of candidates) {
1083
1174
  const values = new Set();
1084
- const mapping = {};
1175
+ const indexMapping = {};
1085
1176
  let valid = true;
1086
1177
  for (let i = 0; i < items.length; i++) {
1087
1178
  const obj = items[i].type;
@@ -1096,19 +1187,21 @@ function isPhantomType(def) {
1096
1187
  break;
1097
1188
  }
1098
1189
  values.add(val);
1099
- mapping[String(val)] = `#/oneOf/${i}`;
1190
+ indexMapping[String(val)] = i;
1191
+ }
1192
+ if (valid) {
1193
+ if (result) return null;
1194
+ result = {
1195
+ propertyName: candidate,
1196
+ indexMapping
1197
+ };
1100
1198
  }
1101
- if (valid) validCandidates.push({
1102
- propertyName: candidate,
1103
- mapping
1104
- });
1105
1199
  }
1106
- if (validCandidates.length === 1) return validCandidates[0];
1107
- return null;
1200
+ return result;
1108
1201
  }
1109
1202
  function buildJsonSchema(type) {
1110
1203
  const defs = {};
1111
- let isRoot = true;
1204
+ let hasDefs = false;
1112
1205
  const buildObject = (d) => {
1113
1206
  const properties = {};
1114
1207
  const required = [];
@@ -1125,15 +1218,15 @@ function buildJsonSchema(type) {
1125
1218
  return schema$1;
1126
1219
  };
1127
1220
  const build = (def) => {
1128
- if (def.id && def.type.kind === "object" && !isRoot) {
1221
+ if (def.id && def.type.kind === "object" && def !== type) {
1129
1222
  const name = def.id;
1130
1223
  if (!defs[name]) {
1224
+ hasDefs = true;
1131
1225
  defs[name] = {};
1132
1226
  defs[name] = buildObject(def);
1133
1227
  }
1134
1228
  return { $ref: `#/$defs/${name}` };
1135
1229
  }
1136
- isRoot = false;
1137
1230
  const meta = def.metadata;
1138
1231
  return forAnnotatedType(def, {
1139
1232
  phantom() {
@@ -1158,11 +1251,9 @@ function buildJsonSchema(type) {
1158
1251
  if (disc) {
1159
1252
  const oneOf = d.type.items.map(build);
1160
1253
  const mapping = {};
1161
- for (const [val, origPath] of Object.entries(disc.mapping)) {
1162
- const idx = Number.parseInt(origPath.split("/").pop(), 10);
1254
+ for (const [val, idx] of Object.entries(disc.indexMapping)) {
1163
1255
  const item = d.type.items[idx];
1164
- if (item.id && defs[item.id]) mapping[val] = `#/$defs/${item.id}`;
1165
- else mapping[val] = origPath;
1256
+ mapping[val] = item.id && defs[item.id] ? `#/$defs/${item.id}` : `#/oneOf/${idx}`;
1166
1257
  }
1167
1258
  return {
1168
1259
  oneOf,
@@ -1212,7 +1303,7 @@ else schema$1.allOf = (schema$1.allOf || []).concat(patterns.map((p) => ({ patte
1212
1303
  });
1213
1304
  };
1214
1305
  const schema = build(type);
1215
- if (Object.keys(defs).length > 0) return {
1306
+ if (hasDefs) return {
1216
1307
  ...schema,
1217
1308
  $defs: defs
1218
1309
  };
@@ -1236,24 +1327,25 @@ var JsRenderer = class extends BaseRenderer {
1236
1327
  this.writeln("// prettier-ignore-start");
1237
1328
  this.writeln("/* eslint-disable */");
1238
1329
  this.writeln("/* oxlint-disable */");
1330
+ let hasMutatingAnnotate = false;
1331
+ const nodesByName = new Map();
1332
+ for (const node of this.doc.nodes) {
1333
+ if (node.entity === "annotate" && node.isMutating) hasMutatingAnnotate = true;
1334
+ if (node.__typeId !== null && node.__typeId !== undefined && node.id) {
1335
+ const name = node.id;
1336
+ if (!nodesByName.has(name)) nodesByName.set(name, []);
1337
+ nodesByName.get(name).push(node);
1338
+ }
1339
+ }
1340
+ for (const [name, nodes] of nodesByName) if (nodes.length === 1) this.typeIds.set(nodes[0], name);
1341
+ else for (let i = 0; i < nodes.length; i++) this.typeIds.set(nodes[i], `${name}__${i + 1}`);
1239
1342
  const imports = ["defineAnnotatedType as $", "annotate as $a"];
1240
- const hasMutatingAnnotate = this.doc.nodes.some((n) => n.entity === "annotate" && n.isMutating);
1241
1343
  if (hasMutatingAnnotate) imports.push("cloneRefProp as $c");
1242
1344
  const jsonSchemaMode = resolveJsonSchemaMode(this.opts);
1243
1345
  if (jsonSchemaMode === "lazy") imports.push("buildJsonSchema as $$");
1244
1346
  if (this.opts?.exampleData) imports.push("createDataFromAnnotatedType as $e");
1245
1347
  if (jsonSchemaMode === false) imports.push("throwFeatureDisabled as $d");
1246
1348
  this.writeln(`import { ${imports.join(", ")} } from "@atscript/typescript/utils"`);
1247
- const nameCounts = new Map();
1248
- const nodesByName = new Map();
1249
- for (const node of this.doc.nodes) if (node.__typeId !== null && node.__typeId !== undefined && node.id) {
1250
- const name = node.id;
1251
- nameCounts.set(name, (nameCounts.get(name) || 0) + 1);
1252
- if (!nodesByName.has(name)) nodesByName.set(name, []);
1253
- nodesByName.get(name).push(node);
1254
- }
1255
- for (const [name, nodes] of nodesByName) if (nodes.length === 1) this.typeIds.set(nodes[0], name);
1256
- else for (let i = 0; i < nodes.length; i++) this.typeIds.set(nodes[i], `${name}__${i + 1}`);
1257
1349
  }
1258
1350
  buildAdHocMap(annotateNodes) {
1259
1351
  const map = new Map();
@@ -1262,7 +1354,8 @@ else for (let i = 0; i < nodes.length; i++) this.typeIds.set(nodes[i], `${name}_
1262
1354
  const anns = entry.annotations || [];
1263
1355
  if (anns.length > 0) {
1264
1356
  const existing = map.get(path$1);
1265
- map.set(path$1, existing ? [...existing, ...anns] : anns);
1357
+ if (existing) existing.push(...anns);
1358
+ else map.set(path$1, [...anns]);
1266
1359
  }
1267
1360
  }
1268
1361
  return map.size > 0 ? map : null;
@@ -1321,35 +1414,20 @@ else def = def.getDefinition() || def;
1321
1414
  this.renderExampleDataMethod(node);
1322
1415
  }
1323
1416
  renderInterface(node) {
1324
- this.writeln();
1325
- const exported = node.token("export")?.text === "export";
1326
- this.write(exported ? "export " : "");
1327
- this.write(`class ${node.id} `);
1328
- this.blockln("{}");
1329
- this.renderClassStatics(node);
1330
- this.popln();
1331
- this.postAnnotate.push(node);
1332
- this.writeln();
1417
+ this.renderDefinitionClass(node);
1333
1418
  }
1334
1419
  renderType(node) {
1335
- this.writeln();
1336
- const exported = node.token("export")?.text === "export";
1337
- this.write(exported ? "export " : "");
1338
- this.write(`class ${node.id} `);
1339
- this.blockln("{}");
1340
- this.renderClassStatics(node);
1341
- this.popln();
1342
- this.postAnnotate.push(node);
1343
- this.writeln();
1420
+ this.renderDefinitionClass(node);
1344
1421
  }
1345
1422
  renderAnnotate(node) {
1346
1423
  if (node.isMutating) {
1347
1424
  this.postAnnotate.push(node);
1348
1425
  return;
1349
1426
  }
1350
- const targetName = node.targetName;
1351
- const unwound = this.doc.unwindType(targetName);
1352
- if (!unwound?.def) return;
1427
+ if (!this.doc.unwindType(node.targetName)?.def) return;
1428
+ this.renderDefinitionClass(node);
1429
+ }
1430
+ renderDefinitionClass(node) {
1353
1431
  this.writeln();
1354
1432
  const exported = node.token("export")?.text === "export";
1355
1433
  this.write(exported ? "export " : "");
@@ -1507,6 +1585,11 @@ else handle.prop(prop.id, propHandle.$type);
1507
1585
  handle.annotate(a.name, true);
1508
1586
  break;
1509
1587
  }
1588
+ case "expect.array.uniqueItems":
1589
+ case "expect.array.key": {
1590
+ handle.annotate(a.name, { message: a.args[0]?.text });
1591
+ break;
1592
+ }
1510
1593
  default:
1511
1594
  }
1512
1595
  });
@@ -1574,10 +1657,7 @@ else handle.prop(prop.id, propHandle.$type);
1574
1657
  this.writeln(`$("array"${name ? `, ${name}` : ""})`).indent().defineArray(node).unindent();
1575
1658
  return this;
1576
1659
  }
1577
- default: {
1578
- console.log("!!!!!!! UNKNOWN", node.entity);
1579
- return this;
1580
- }
1660
+ default: return this;
1581
1661
  }
1582
1662
  }
1583
1663
  defineConst(node) {
@@ -1789,40 +1869,25 @@ else targetValue = "true";
1789
1869
  this.writeln(`$c(${clone.parentPath}, "${escapeQuotes(clone.propName)}")`);
1790
1870
  }
1791
1871
  }
1792
- for (const { entry, accessors } of entryAccessors) {
1793
- const anns = entry.annotations;
1794
- for (const accessor of accessors) {
1795
- const cleared = new Set();
1796
- for (const an of anns) {
1797
- const { value, multiple } = this.computeAnnotationValue(entry, an);
1798
- if (multiple) {
1799
- if (!cleared.has(an.name)) {
1800
- const spec = this.doc.resolveAnnotation(an.name);
1801
- if (!spec || spec.config.mergeStrategy !== "append") this.writeln(`${accessor}.metadata.delete("${escapeQuotes(an.name)}")`);
1802
- cleared.add(an.name);
1803
- }
1804
- this.writeln(`$a(${accessor}.metadata, "${escapeQuotes(an.name)}", ${value}, true)`);
1805
- } else this.writeln(`$a(${accessor}.metadata, "${escapeQuotes(an.name)}", ${value})`);
1806
- }
1807
- }
1808
- }
1872
+ for (const { entry, accessors } of entryAccessors) for (const accessor of accessors) this.emitMutatingAnnotations(entry, entry.annotations, accessor);
1809
1873
  const topAnnotations = node.annotations;
1810
- if (topAnnotations && topAnnotations.length > 0) {
1811
- const cleared = new Set();
1812
- for (const an of topAnnotations) {
1813
- const { value, multiple } = this.computeAnnotationValue(node, an);
1814
- if (multiple) {
1815
- if (!cleared.has(an.name)) {
1816
- const spec = this.doc.resolveAnnotation(an.name);
1817
- if (!spec || spec.config.mergeStrategy !== "append") this.writeln(`${targetName}.metadata.delete("${escapeQuotes(an.name)}")`);
1818
- cleared.add(an.name);
1819
- }
1820
- this.writeln(`$a(${targetName}.metadata, "${escapeQuotes(an.name)}", ${value}, true)`);
1821
- } else this.writeln(`$a(${targetName}.metadata, "${escapeQuotes(an.name)}", ${value})`);
1822
- }
1823
- }
1874
+ if (topAnnotations && topAnnotations.length > 0) this.emitMutatingAnnotations(node, topAnnotations, targetName);
1824
1875
  this.writeln();
1825
1876
  }
1877
+ emitMutatingAnnotations(node, annotations, accessor) {
1878
+ const cleared = new Set();
1879
+ for (const an of annotations) {
1880
+ const { value, multiple } = this.computeAnnotationValue(node, an);
1881
+ if (multiple) {
1882
+ if (!cleared.has(an.name)) {
1883
+ const spec = this.doc.resolveAnnotation(an.name);
1884
+ if (!spec || spec.config.mergeStrategy !== "append") this.writeln(`${accessor}.metadata.delete("${escapeQuotes(an.name)}")`);
1885
+ cleared.add(an.name);
1886
+ }
1887
+ this.writeln(`$a(${accessor}.metadata, "${escapeQuotes(an.name)}", ${value}, true)`);
1888
+ } else this.writeln(`$a(${accessor}.metadata, "${escapeQuotes(an.name)}", ${value})`);
1889
+ }
1890
+ }
1826
1891
  resolveTargetDef(targetName) {
1827
1892
  const unwound = this.doc.unwindType(targetName);
1828
1893
  if (!unwound?.def) return undefined;