@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.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
  }
@@ -289,13 +291,14 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
289
291
  }
290
292
  if ((0, __atscript_core.isPrimitive)(def)) return this.write(renderPrimitiveTypeDef(def.config.type));
291
293
  }
292
- renderStructure(struct, asClass, interfaceNode) {
294
+ renderStructure(struct, asClass, interfaceNode, filterProps) {
293
295
  this.blockln("{}");
294
296
  const patterns = [];
295
297
  const propsDefs = new Set();
296
- for (const prop of Array.from(struct.props.values())) {
298
+ for (const prop of struct.props.values()) {
299
+ if (filterProps?.has(prop.id)) continue;
297
300
  if (prop.token("identifier")?.pattern) {
298
- patterns.push(prop);
301
+ if (!filterProps) patterns.push(prop);
299
302
  continue;
300
303
  }
301
304
  const phantomType = this.phantomPropType(prop.getDefinition());
@@ -311,34 +314,36 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
311
314
  }
312
315
  if (patterns.length > 0) {
313
316
  this.write(`[key: string]: `);
314
- if (patterns.length > 0) {
315
- for (const prop of patterns) propsDefs.add(this.renderTypeDefString(prop.getDefinition()));
316
- const defs = Array.from(propsDefs);
317
- if (defs.length > 1) {
318
- this.indent();
319
- for (const def of defs) {
320
- this.writeln();
321
- this.write("| ");
322
- def.split("\n").forEach((l) => this.write(l.trim()));
323
- }
324
- 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) {
325
322
  this.writeln();
326
- } else defs[0].split("\n").forEach((l) => this.writeln(l));
327
- }
328
- }
329
- if (asClass) {
330
- this.writeln("static __is_atscript_annotated_type: true");
331
- this.writeln(`static type: TAtscriptTypeObject<keyof ${asClass}, ${asClass}>`);
332
- this.writeln(`static metadata: TMetadataMap<AtscriptMetadata>`);
333
- this.writeln(`static validator: (opts?: Partial<TValidatorOptions>) => Validator<typeof ${asClass}>`);
334
- 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. */");
335
- this.writeln("static toJsonSchema: () => any");
336
- if (!this.opts?.exampleData) this.writeln("/** @deprecated Example Data support is disabled. To enable, set `exampleData: true` in tsPlugin options. */");
337
- this.writeln("static toExampleData?: () => any");
338
- if (interfaceNode && this.hasDbTable(interfaceNode)) this.renderFlat(interfaceNode);
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));
339
329
  }
330
+ if (asClass) this.renderStaticDeclarations(asClass, interfaceNode);
340
331
  this.pop();
341
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
+ }
342
347
  renderInterface(node) {
343
348
  this.writeln();
344
349
  const exported = node.token("export")?.text === "export";
@@ -362,7 +367,7 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
362
367
  }
363
368
  if (!firstParentProps && (0, __atscript_core.isStructure)(fpDef)) firstParentProps = fpDef.props;
364
369
  }
365
- this.renderStructureFiltered(resolved, node.id, firstParentProps, node);
370
+ this.renderStructure(resolved, node.id, node, firstParentProps);
366
371
  } else this.writeln("{}");
367
372
  } else {
368
373
  this.write(`class ${node.id} `);
@@ -372,35 +377,6 @@ else this.writeln("{}");
372
377
  }
373
378
  this.writeln();
374
379
  }
375
- /**
376
- * Renders a structure block, optionally filtering out props that exist in a parent.
377
- */ renderStructureFiltered(struct, asClass, filterProps, interfaceNode) {
378
- if (!filterProps) return this.renderStructure(struct, asClass, interfaceNode);
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
- if (interfaceNode && this.hasDbTable(interfaceNode)) this.renderFlat(interfaceNode);
402
- this.pop();
403
- }
404
380
  renderType(node) {
405
381
  this.writeln();
406
382
  const exported = node.token("export")?.text === "export";
@@ -498,6 +474,88 @@ else {
498
474
  }
499
475
  this.pop();
500
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 (unless unique indexes exist)
484
+ * - **Unique indexes** (`@db.index.unique`) → appended as union members
485
+ * - **Mongo collection** → always includes `string` (ObjectId) in the union;
486
+ * if no `@meta.id` fields, `__pk` is just `string`
487
+ */ renderPk(node) {
488
+ const isMongoCollection = !!node.annotations?.some((a) => a.name === "db.mongo.collection");
489
+ let struct;
490
+ if (node.hasExtends) struct = this.doc.resolveInterfaceExtends(node);
491
+ if (!struct) struct = node.getDefinition();
492
+ if (!struct || !(0, __atscript_core.isStructure)(struct)) return;
493
+ const structNode = struct;
494
+ const pkProps = [];
495
+ const uniqueByIndex = new Map();
496
+ for (const [name, prop] of structNode.props) {
497
+ if (prop.token("identifier")?.pattern) continue;
498
+ if (isMongoCollection && name === "_id") continue;
499
+ if (prop.countAnnotations("meta.id") > 0) pkProps.push({
500
+ name,
501
+ prop
502
+ });
503
+ if (prop.annotations) {
504
+ for (const ann of prop.annotations) if (ann.name === "db.index.unique") {
505
+ const indexName = ann.args[0]?.text ?? name;
506
+ let group = uniqueByIndex.get(indexName);
507
+ if (!group) {
508
+ group = [];
509
+ uniqueByIndex.set(indexName, group);
510
+ }
511
+ group.push({
512
+ name,
513
+ prop
514
+ });
515
+ }
516
+ }
517
+ }
518
+ const uniqueProps = [];
519
+ const pkNames = new Set(pkProps.map((p) => p.name));
520
+ for (const fields of uniqueByIndex.values()) if (fields.length === 1 && !pkNames.has(fields[0].name)) uniqueProps.push(fields[0]);
521
+ if (pkProps.length === 0 && uniqueProps.length === 0 && !isMongoCollection) return;
522
+ let mongoIdType;
523
+ if (isMongoCollection) {
524
+ const idProp = structNode.props.get("_id");
525
+ if (idProp) mongoIdType = this.renderTypeDefString(idProp.getDefinition()).trim();
526
+ mongoIdType ?? (mongoIdType = "string");
527
+ }
528
+ const uniqueTypes = [];
529
+ const seenTypes = new Set();
530
+ for (const { prop } of uniqueProps) {
531
+ const rendered = this.renderTypeDefString(prop.getDefinition()).trim();
532
+ if (!seenTypes.has(rendered)) {
533
+ seenTypes.add(rendered);
534
+ uniqueTypes.push(rendered);
535
+ }
536
+ }
537
+ this.writeln();
538
+ const uniqueSuffix = uniqueTypes.length > 0 ? ` | ${uniqueTypes.join(" | ")}` : "";
539
+ if (pkProps.length === 0 && !isMongoCollection) this.writeln(`static __pk: ${uniqueTypes.join(" | ")}`);
540
+ else if (pkProps.length === 0) this.writeln(`static __pk: ${mongoIdType}${uniqueSuffix}`);
541
+ else if (pkProps.length === 1) {
542
+ this.write("static __pk: ");
543
+ if (isMongoCollection) this.write(`${mongoIdType} | `);
544
+ const renderedDef = this.renderTypeDefString(pkProps[0].prop.getDefinition()).trim();
545
+ this.writeln(`${renderedDef}${uniqueSuffix}`);
546
+ } else {
547
+ this.write("static __pk: ");
548
+ if (isMongoCollection) this.write(`${mongoIdType} | `);
549
+ this.blockln("{}");
550
+ for (const { name, prop } of pkProps) {
551
+ this.write(wrapProp(name), ": ");
552
+ const renderedDef = this.renderTypeDefString(prop.getDefinition());
553
+ renderedDef.split("\n").forEach((l) => this.writeln(l));
554
+ }
555
+ this.pop();
556
+ this.writeln(uniqueSuffix);
557
+ }
558
+ }
501
559
  phantomPropType(def) {
502
560
  if (!def) return undefined;
503
561
  if ((0, __atscript_core.isPrimitive)(def) && def.config.type === "phantom") return def.id;
@@ -546,24 +604,6 @@ function renderPrimitiveTypeDef(def) {
546
604
  }
547
605
  }
548
606
 
549
- //#endregion
550
- //#region packages/typescript/src/traverse.ts
551
- function forAnnotatedType(def, handlers) {
552
- switch (def.type.kind) {
553
- case "": {
554
- const typed = def;
555
- if (handlers.phantom && typed.type.designType === "phantom") return handlers.phantom(typed);
556
- return handlers.final(typed);
557
- }
558
- case "object": return handlers.object(def);
559
- case "array": return handlers.array(def);
560
- case "union": return handlers.union(def);
561
- case "intersection": return handlers.intersection(def);
562
- case "tuple": return handlers.tuple(def);
563
- default: throw new Error(`Unknown type kind "${def.type.kind}"`);
564
- }
565
- }
566
-
567
607
  //#endregion
568
608
  //#region packages/typescript/src/validator.ts
569
609
  function _define_property$1(obj, key, value) {
@@ -579,28 +619,35 @@ else obj[key] = value;
579
619
  const regexCache = new Map();
580
620
  var Validator = class {
581
621
  isLimitExceeded() {
582
- if (this.stackErrors.length > 0) return this.stackErrors[this.stackErrors.length - 1].length >= this.opts.errorLimit;
622
+ if (this.stackErrors.length > 0) {
623
+ const top = this.stackErrors[this.stackErrors.length - 1];
624
+ return top !== null && top.length >= this.opts.errorLimit;
625
+ }
583
626
  return this.errors.length >= this.opts.errorLimit;
584
627
  }
585
628
  push(name) {
586
629
  this.stackPath.push(name);
587
- this.stackErrors.push([]);
630
+ this.stackErrors.push(null);
631
+ this.cachedPath = this.stackPath.length <= 1 ? "" : this.stackPath[1] + (this.stackPath.length > 2 ? "." + this.stackPath.slice(2).join(".") : "");
588
632
  }
589
633
  pop(saveErrors) {
590
634
  this.stackPath.pop();
591
635
  const popped = this.stackErrors.pop();
592
- if (saveErrors && popped?.length) popped.forEach((error) => {
593
- this.error(error.message, error.path, error.details);
594
- });
636
+ if (saveErrors && popped !== null && popped !== undefined && popped.length > 0) for (const err of popped) this.error(err.message, err.path, err.details);
637
+ this.cachedPath = this.stackPath.length <= 1 ? "" : this.stackPath[1] + (this.stackPath.length > 2 ? "." + this.stackPath.slice(2).join(".") : "");
595
638
  return popped;
596
639
  }
597
640
  clear() {
598
- this.stackErrors[this.stackErrors.length - 1] = [];
641
+ this.stackErrors[this.stackErrors.length - 1] = null;
599
642
  }
600
643
  error(message, path$2, details) {
601
- const errors = this.stackErrors[this.stackErrors.length - 1] || this.errors;
644
+ let errors = this.stackErrors[this.stackErrors.length - 1];
645
+ if (!errors) if (this.stackErrors.length > 0) {
646
+ errors = [];
647
+ this.stackErrors[this.stackErrors.length - 1] = errors;
648
+ } else errors = this.errors;
602
649
  const error = {
603
- path: path$2 || this.path,
650
+ path: path$2 || this.cachedPath,
604
651
  message
605
652
  };
606
653
  if (details?.length) error.details = details;
@@ -620,9 +667,10 @@ var Validator = class {
620
667
  * @returns `true` if the value matches the type definition.
621
668
  * @throws {ValidatorError} When validation fails and `safe` is not `true`.
622
669
  */ validate(value, safe, context) {
623
- this.push("");
624
670
  this.errors = [];
625
671
  this.stackErrors = [];
672
+ this.stackPath = [""];
673
+ this.cachedPath = "";
626
674
  this.context = context;
627
675
  const passed = this.validateSafe(this.def, value);
628
676
  this.pop(!passed);
@@ -636,7 +684,7 @@ var Validator = class {
636
684
  validateSafe(def, value) {
637
685
  if (this.isLimitExceeded()) return false;
638
686
  if (!isAnnotatedType(def)) throw new Error("Can not validate not-annotated type");
639
- if (typeof this.opts.replace === "function") def = this.opts.replace(def, this.path);
687
+ if (typeof this.opts.replace === "function") def = this.opts.replace(def, this.cachedPath);
640
688
  if (def.optional && value === undefined) return true;
641
689
  for (const plugin of this.opts.plugins) {
642
690
  const result = plugin(this, def, value);
@@ -645,18 +693,21 @@ var Validator = class {
645
693
  return this.validateAnnotatedType(def, value);
646
694
  }
647
695
  get path() {
648
- return this.stackPath.slice(1).join(".");
696
+ return this.cachedPath;
649
697
  }
650
698
  validateAnnotatedType(def, value) {
651
- return forAnnotatedType(def, {
652
- final: (d) => this.validatePrimitive(d, value),
653
- phantom: () => true,
654
- object: (d) => this.validateObject(d, value),
655
- array: (d) => this.validateArray(d, value),
656
- union: (d) => this.validateUnion(d, value),
657
- intersection: (d) => this.validateIntersection(d, value),
658
- tuple: (d) => this.validateTuple(d, value)
659
- });
699
+ switch (def.type.kind) {
700
+ case "": {
701
+ if (def.type.designType === "phantom") return true;
702
+ return this.validatePrimitive(def, value);
703
+ }
704
+ case "object": return this.validateObject(def, value);
705
+ case "array": return this.validateArray(def, value);
706
+ case "union": return this.validateUnion(def, value);
707
+ case "intersection": return this.validateIntersection(def, value);
708
+ case "tuple": return this.validateTuple(def, value);
709
+ default: throw new Error(`Unknown type kind "${def.type.kind}"`);
710
+ }
660
711
  }
661
712
  validateUnion(def, value) {
662
713
  let i = 0;
@@ -720,6 +771,30 @@ var Validator = class {
720
771
  return false;
721
772
  }
722
773
  }
774
+ const uniqueItems = def.metadata.get("expect.array.uniqueItems");
775
+ if (uniqueItems) {
776
+ const separator = "▼↩";
777
+ const seen = new Set();
778
+ const keyProps = new Set();
779
+ if (def.type.of.type.kind === "object") {
780
+ for (const [key, val] of def.type.of.type.props.entries()) if (val.metadata.get("expect.array.key")) keyProps.add(key);
781
+ }
782
+ for (let idx = 0; idx < value.length; idx++) {
783
+ const item = value[idx];
784
+ let key;
785
+ if (keyProps.size > 0) {
786
+ key = "";
787
+ for (const prop of keyProps) key += JSON.stringify(item[prop]) + separator;
788
+ } else key = JSON.stringify(item);
789
+ if (seen.has(key)) {
790
+ this.push(String(idx));
791
+ this.error(uniqueItems.message || "Duplicate items are not allowed");
792
+ this.pop(true);
793
+ return false;
794
+ }
795
+ seen.add(key);
796
+ }
797
+ }
723
798
  let i = 0;
724
799
  let passed = true;
725
800
  for (const item of value) {
@@ -741,21 +816,20 @@ var Validator = class {
741
816
  let passed = true;
742
817
  const valueKeys = new Set(Object.keys(value));
743
818
  const typeKeys = new Set();
744
- const skipList = new Set();
819
+ let skipList;
745
820
  if (this.opts.skipList) {
746
- const path$2 = this.stackPath.length > 1 ? `${this.path}.` : "";
747
- this.opts.skipList.forEach((item) => {
748
- if (item.startsWith(path$2)) {
749
- const key = item.slice(path$2.length);
750
- skipList.add(key);
751
- valueKeys.delete(key);
752
- }
753
- });
821
+ const path$2 = this.stackPath.length > 1 ? `${this.cachedPath}.` : "";
822
+ for (const item of this.opts.skipList) if (item.startsWith(path$2)) {
823
+ const key = item.slice(path$2.length);
824
+ if (!skipList) skipList = new Set();
825
+ skipList.add(key);
826
+ valueKeys.delete(key);
827
+ }
754
828
  }
755
829
  let partialFunctionMatched = false;
756
- if (typeof this.opts.partial === "function") partialFunctionMatched = this.opts.partial(def, this.path);
830
+ if (typeof this.opts.partial === "function") partialFunctionMatched = this.opts.partial(def, this.cachedPath);
757
831
  for (const [key, item] of def.type.props.entries()) {
758
- if (skipList.has(key) || isPhantomType(item)) continue;
832
+ if (skipList && skipList.has(key) || isPhantomType(item)) continue;
759
833
  typeKeys.add(key);
760
834
  if (value[key] === undefined) {
761
835
  if (partialFunctionMatched || this.opts.partial === "deep" || this.opts.partial === true && this.stackPath.length <= 1) continue;
@@ -776,19 +850,21 @@ else {
776
850
  def: propDef
777
851
  });
778
852
  if (matched.length > 0) {
853
+ this.push(key);
779
854
  let keyPassed = false;
780
- for (const { def: def$1 } of matched) if (this.validateSafe(def$1, value[key])) {
781
- this.pop(false);
782
- keyPassed = true;
783
- break;
855
+ for (const { def: propDef } of matched) {
856
+ if (this.validateSafe(propDef, value[key])) {
857
+ keyPassed = true;
858
+ break;
859
+ }
860
+ this.clear();
784
861
  }
785
862
  if (!keyPassed) {
786
- this.push(key);
787
863
  this.validateSafe(matched[0].def, value[key]);
788
864
  this.pop(true);
789
865
  passed = false;
790
866
  if (this.isLimitExceeded()) return false;
791
- }
867
+ } else this.pop(false);
792
868
  } else if (this.opts.unknownProps !== "ignore") {
793
869
  if (this.opts.unknownProps === "error") {
794
870
  this.push(key);
@@ -941,11 +1017,13 @@ else {
941
1017
  /** Validation errors collected during the last {@link validate} call. */ _define_property$1(this, "errors", void 0);
942
1018
  _define_property$1(this, "stackErrors", void 0);
943
1019
  _define_property$1(this, "stackPath", void 0);
1020
+ _define_property$1(this, "cachedPath", void 0);
944
1021
  _define_property$1(this, "context", void 0);
945
1022
  this.def = def;
946
1023
  this.errors = [];
947
1024
  this.stackErrors = [];
948
1025
  this.stackPath = [];
1026
+ this.cachedPath = "";
949
1027
  this.opts = {
950
1028
  partial: false,
951
1029
  unknownProps: "error",
@@ -963,6 +1041,24 @@ var ValidatorError = class extends Error {
963
1041
 
964
1042
  //#endregion
965
1043
  //#region packages/typescript/src/annotated-type.ts
1044
+ const COMPLEX_KINDS = new Set([
1045
+ "union",
1046
+ "intersection",
1047
+ "tuple"
1048
+ ]);
1049
+ /** Shared validator method reused by all annotated type nodes. */ function validatorMethod(opts) {
1050
+ return new Validator(this, opts);
1051
+ }
1052
+ function createAnnotatedTypeNode(type, metadata, opts) {
1053
+ return {
1054
+ __is_atscript_annotated_type: true,
1055
+ type,
1056
+ metadata,
1057
+ validator: validatorMethod,
1058
+ id: opts?.id,
1059
+ optional: opts?.optional
1060
+ };
1061
+ }
966
1062
  function isAnnotatedType(type) {
967
1063
  return type && type.__is_atscript_annotated_type;
968
1064
  }
@@ -979,38 +1075,24 @@ function defineAnnotatedType(_kind, base) {
979
1075
  const kind = _kind || "";
980
1076
  const type = base?.type || {};
981
1077
  type.kind = kind;
982
- if ([
983
- "union",
984
- "intersection",
985
- "tuple"
986
- ].includes(kind)) type.items = [];
1078
+ if (COMPLEX_KINDS.has(kind)) type.items = [];
987
1079
  if (kind === "object") {
988
1080
  type.props = new Map();
989
1081
  type.propsPatterns = [];
990
1082
  }
991
1083
  type.tags = new Set();
992
1084
  const metadata = base?.metadata || new Map();
993
- if (base) Object.assign(base, {
994
- __is_atscript_annotated_type: true,
995
- metadata,
996
- type,
997
- validator(opts) {
998
- return new Validator(this, opts);
999
- }
1000
- });
1001
- else base = {
1085
+ const payload = {
1002
1086
  __is_atscript_annotated_type: true,
1003
1087
  metadata,
1004
1088
  type,
1005
- validator(opts) {
1006
- return new Validator(this, opts);
1007
- }
1089
+ validator: validatorMethod
1008
1090
  };
1091
+ base = base ? Object.assign(base, payload) : payload;
1009
1092
  const handle = {
1010
1093
  $type: base,
1011
1094
  $def: type,
1012
1095
  $metadata: metadata,
1013
- _existingObject: undefined,
1014
1096
  tags(...tags) {
1015
1097
  for (const tag of tags) this.$def.tags.add(tag);
1016
1098
  return this;
@@ -1051,27 +1133,18 @@ else base = {
1051
1133
  return this;
1052
1134
  },
1053
1135
  refTo(type$1, chain) {
1136
+ if (!isAnnotatedType(type$1)) throw new Error(`${type$1} is not annotated type`);
1054
1137
  let newBase = type$1;
1055
1138
  const typeName = type$1.name || "Unknown";
1056
- if (isAnnotatedType(newBase)) {
1057
- let keys = "";
1058
- for (const c of chain || []) {
1059
- keys += `["${c}"]`;
1060
- if (newBase.type.kind === "object" && newBase.type.props.has(c)) newBase = newBase.type.props.get(c);
1061
- else throw new Error(`Can't find prop ${typeName}${keys}`);
1139
+ if (chain) for (let i = 0; i < chain.length; i++) {
1140
+ const c = chain[i];
1141
+ if (newBase.type.kind === "object" && newBase.type.props.has(c)) newBase = newBase.type.props.get(c);
1142
+ else {
1143
+ const keys = chain.slice(0, i + 1).map((k) => `["${k}"]`).join("");
1144
+ throw new Error(`Can't find prop ${typeName}${keys}`);
1062
1145
  }
1063
- if (!newBase && keys) throw new Error(`Can't find prop ${typeName}${keys}`);
1064
- else if (!newBase) throw new Error(`"${typeName}" is not annotated type`);
1065
- this.$type = {
1066
- __is_atscript_annotated_type: true,
1067
- type: newBase.type,
1068
- metadata,
1069
- id: newBase.id,
1070
- validator(opts) {
1071
- return new Validator(this, opts);
1072
- }
1073
- };
1074
- } else throw new Error(`${type$1} is not annotated type`);
1146
+ }
1147
+ this.$type = createAnnotatedTypeNode(newBase.type, metadata, { id: newBase.id });
1075
1148
  return this;
1076
1149
  },
1077
1150
  annotate(key, value, asArray) {
@@ -1089,6 +1162,24 @@ function isPhantomType(def) {
1089
1162
  return def.type.kind === "" && def.type.designType === "phantom";
1090
1163
  }
1091
1164
 
1165
+ //#endregion
1166
+ //#region packages/typescript/src/traverse.ts
1167
+ function forAnnotatedType(def, handlers) {
1168
+ switch (def.type.kind) {
1169
+ case "": {
1170
+ const typed = def;
1171
+ if (handlers.phantom && typed.type.designType === "phantom") return handlers.phantom(typed);
1172
+ return handlers.final(typed);
1173
+ }
1174
+ case "object": return handlers.object(def);
1175
+ case "array": return handlers.array(def);
1176
+ case "union": return handlers.union(def);
1177
+ case "intersection": return handlers.intersection(def);
1178
+ case "tuple": return handlers.tuple(def);
1179
+ default: throw new Error(`Unknown type kind "${def.type.kind}"`);
1180
+ }
1181
+ }
1182
+
1092
1183
  //#endregion
1093
1184
  //#region packages/typescript/src/json-schema.ts
1094
1185
  /**
@@ -1103,10 +1194,10 @@ function isPhantomType(def) {
1103
1194
  const firstObj = items[0].type;
1104
1195
  const candidates = [];
1105
1196
  for (const [propName, propType] of firstObj.props.entries()) if (propType.type.kind === "" && propType.type.value !== undefined) candidates.push(propName);
1106
- const validCandidates = [];
1197
+ let result = null;
1107
1198
  for (const candidate of candidates) {
1108
1199
  const values = new Set();
1109
- const mapping = {};
1200
+ const indexMapping = {};
1110
1201
  let valid = true;
1111
1202
  for (let i = 0; i < items.length; i++) {
1112
1203
  const obj = items[i].type;
@@ -1121,19 +1212,21 @@ function isPhantomType(def) {
1121
1212
  break;
1122
1213
  }
1123
1214
  values.add(val);
1124
- mapping[String(val)] = `#/oneOf/${i}`;
1215
+ indexMapping[String(val)] = i;
1216
+ }
1217
+ if (valid) {
1218
+ if (result) return null;
1219
+ result = {
1220
+ propertyName: candidate,
1221
+ indexMapping
1222
+ };
1125
1223
  }
1126
- if (valid) validCandidates.push({
1127
- propertyName: candidate,
1128
- mapping
1129
- });
1130
1224
  }
1131
- if (validCandidates.length === 1) return validCandidates[0];
1132
- return null;
1225
+ return result;
1133
1226
  }
1134
1227
  function buildJsonSchema(type) {
1135
1228
  const defs = {};
1136
- let isRoot = true;
1229
+ let hasDefs = false;
1137
1230
  const buildObject = (d) => {
1138
1231
  const properties = {};
1139
1232
  const required = [];
@@ -1150,15 +1243,15 @@ function buildJsonSchema(type) {
1150
1243
  return schema$1;
1151
1244
  };
1152
1245
  const build = (def) => {
1153
- if (def.id && def.type.kind === "object" && !isRoot) {
1246
+ if (def.id && def.type.kind === "object" && def !== type) {
1154
1247
  const name = def.id;
1155
1248
  if (!defs[name]) {
1249
+ hasDefs = true;
1156
1250
  defs[name] = {};
1157
1251
  defs[name] = buildObject(def);
1158
1252
  }
1159
1253
  return { $ref: `#/$defs/${name}` };
1160
1254
  }
1161
- isRoot = false;
1162
1255
  const meta = def.metadata;
1163
1256
  return forAnnotatedType(def, {
1164
1257
  phantom() {
@@ -1183,11 +1276,9 @@ function buildJsonSchema(type) {
1183
1276
  if (disc) {
1184
1277
  const oneOf = d.type.items.map(build);
1185
1278
  const mapping = {};
1186
- for (const [val, origPath] of Object.entries(disc.mapping)) {
1187
- const idx = Number.parseInt(origPath.split("/").pop(), 10);
1279
+ for (const [val, idx] of Object.entries(disc.indexMapping)) {
1188
1280
  const item = d.type.items[idx];
1189
- if (item.id && defs[item.id]) mapping[val] = `#/$defs/${item.id}`;
1190
- else mapping[val] = origPath;
1281
+ mapping[val] = item.id && defs[item.id] ? `#/$defs/${item.id}` : `#/oneOf/${idx}`;
1191
1282
  }
1192
1283
  return {
1193
1284
  oneOf,
@@ -1237,7 +1328,7 @@ else schema$1.allOf = (schema$1.allOf || []).concat(patterns.map((p) => ({ patte
1237
1328
  });
1238
1329
  };
1239
1330
  const schema = build(type);
1240
- if (Object.keys(defs).length > 0) return {
1331
+ if (hasDefs) return {
1241
1332
  ...schema,
1242
1333
  $defs: defs
1243
1334
  };
@@ -1261,24 +1352,25 @@ var JsRenderer = class extends BaseRenderer {
1261
1352
  this.writeln("// prettier-ignore-start");
1262
1353
  this.writeln("/* eslint-disable */");
1263
1354
  this.writeln("/* oxlint-disable */");
1355
+ let hasMutatingAnnotate = false;
1356
+ const nodesByName = new Map();
1357
+ for (const node of this.doc.nodes) {
1358
+ if (node.entity === "annotate" && node.isMutating) hasMutatingAnnotate = true;
1359
+ if (node.__typeId !== null && node.__typeId !== undefined && node.id) {
1360
+ const name = node.id;
1361
+ if (!nodesByName.has(name)) nodesByName.set(name, []);
1362
+ nodesByName.get(name).push(node);
1363
+ }
1364
+ }
1365
+ for (const [name, nodes] of nodesByName) if (nodes.length === 1) this.typeIds.set(nodes[0], name);
1366
+ else for (let i = 0; i < nodes.length; i++) this.typeIds.set(nodes[i], `${name}__${i + 1}`);
1264
1367
  const imports = ["defineAnnotatedType as $", "annotate as $a"];
1265
- const hasMutatingAnnotate = this.doc.nodes.some((n) => n.entity === "annotate" && n.isMutating);
1266
1368
  if (hasMutatingAnnotate) imports.push("cloneRefProp as $c");
1267
1369
  const jsonSchemaMode = resolveJsonSchemaMode(this.opts);
1268
1370
  if (jsonSchemaMode === "lazy") imports.push("buildJsonSchema as $$");
1269
1371
  if (this.opts?.exampleData) imports.push("createDataFromAnnotatedType as $e");
1270
1372
  if (jsonSchemaMode === false) imports.push("throwFeatureDisabled as $d");
1271
1373
  this.writeln(`import { ${imports.join(", ")} } from "@atscript/typescript/utils"`);
1272
- const nameCounts = new Map();
1273
- const nodesByName = new Map();
1274
- for (const node of this.doc.nodes) if (node.__typeId !== null && node.__typeId !== undefined && node.id) {
1275
- const name = node.id;
1276
- nameCounts.set(name, (nameCounts.get(name) || 0) + 1);
1277
- if (!nodesByName.has(name)) nodesByName.set(name, []);
1278
- nodesByName.get(name).push(node);
1279
- }
1280
- for (const [name, nodes] of nodesByName) if (nodes.length === 1) this.typeIds.set(nodes[0], name);
1281
- else for (let i = 0; i < nodes.length; i++) this.typeIds.set(nodes[i], `${name}__${i + 1}`);
1282
1374
  }
1283
1375
  buildAdHocMap(annotateNodes) {
1284
1376
  const map = new Map();
@@ -1287,7 +1379,8 @@ else for (let i = 0; i < nodes.length; i++) this.typeIds.set(nodes[i], `${name}_
1287
1379
  const anns = entry.annotations || [];
1288
1380
  if (anns.length > 0) {
1289
1381
  const existing = map.get(path$2);
1290
- map.set(path$2, existing ? [...existing, ...anns] : anns);
1382
+ if (existing) existing.push(...anns);
1383
+ else map.set(path$2, [...anns]);
1291
1384
  }
1292
1385
  }
1293
1386
  return map.size > 0 ? map : null;
@@ -1346,35 +1439,20 @@ else def = def.getDefinition() || def;
1346
1439
  this.renderExampleDataMethod(node);
1347
1440
  }
1348
1441
  renderInterface(node) {
1349
- this.writeln();
1350
- const exported = node.token("export")?.text === "export";
1351
- this.write(exported ? "export " : "");
1352
- this.write(`class ${node.id} `);
1353
- this.blockln("{}");
1354
- this.renderClassStatics(node);
1355
- this.popln();
1356
- this.postAnnotate.push(node);
1357
- this.writeln();
1442
+ this.renderDefinitionClass(node);
1358
1443
  }
1359
1444
  renderType(node) {
1360
- this.writeln();
1361
- const exported = node.token("export")?.text === "export";
1362
- this.write(exported ? "export " : "");
1363
- this.write(`class ${node.id} `);
1364
- this.blockln("{}");
1365
- this.renderClassStatics(node);
1366
- this.popln();
1367
- this.postAnnotate.push(node);
1368
- this.writeln();
1445
+ this.renderDefinitionClass(node);
1369
1446
  }
1370
1447
  renderAnnotate(node) {
1371
1448
  if (node.isMutating) {
1372
1449
  this.postAnnotate.push(node);
1373
1450
  return;
1374
1451
  }
1375
- const targetName = node.targetName;
1376
- const unwound = this.doc.unwindType(targetName);
1377
- if (!unwound?.def) return;
1452
+ if (!this.doc.unwindType(node.targetName)?.def) return;
1453
+ this.renderDefinitionClass(node);
1454
+ }
1455
+ renderDefinitionClass(node) {
1378
1456
  this.writeln();
1379
1457
  const exported = node.token("export")?.text === "export";
1380
1458
  this.write(exported ? "export " : "");
@@ -1532,6 +1610,11 @@ else handle.prop(prop.id, propHandle.$type);
1532
1610
  handle.annotate(a.name, true);
1533
1611
  break;
1534
1612
  }
1613
+ case "expect.array.uniqueItems":
1614
+ case "expect.array.key": {
1615
+ handle.annotate(a.name, { message: a.args[0]?.text });
1616
+ break;
1617
+ }
1535
1618
  default:
1536
1619
  }
1537
1620
  });
@@ -1599,10 +1682,7 @@ else handle.prop(prop.id, propHandle.$type);
1599
1682
  this.writeln(`$("array"${name ? `, ${name}` : ""})`).indent().defineArray(node).unindent();
1600
1683
  return this;
1601
1684
  }
1602
- default: {
1603
- console.log("!!!!!!! UNKNOWN", node.entity);
1604
- return this;
1605
- }
1685
+ default: return this;
1606
1686
  }
1607
1687
  }
1608
1688
  defineConst(node) {
@@ -1814,40 +1894,25 @@ else targetValue = "true";
1814
1894
  this.writeln(`$c(${clone.parentPath}, "${escapeQuotes(clone.propName)}")`);
1815
1895
  }
1816
1896
  }
1817
- for (const { entry, accessors } of entryAccessors) {
1818
- const anns = entry.annotations;
1819
- for (const accessor of accessors) {
1820
- const cleared = new Set();
1821
- for (const an of anns) {
1822
- const { value, multiple } = this.computeAnnotationValue(entry, an);
1823
- if (multiple) {
1824
- if (!cleared.has(an.name)) {
1825
- const spec = this.doc.resolveAnnotation(an.name);
1826
- if (!spec || spec.config.mergeStrategy !== "append") this.writeln(`${accessor}.metadata.delete("${escapeQuotes(an.name)}")`);
1827
- cleared.add(an.name);
1828
- }
1829
- this.writeln(`$a(${accessor}.metadata, "${escapeQuotes(an.name)}", ${value}, true)`);
1830
- } else this.writeln(`$a(${accessor}.metadata, "${escapeQuotes(an.name)}", ${value})`);
1831
- }
1832
- }
1833
- }
1897
+ for (const { entry, accessors } of entryAccessors) for (const accessor of accessors) this.emitMutatingAnnotations(entry, entry.annotations, accessor);
1834
1898
  const topAnnotations = node.annotations;
1835
- if (topAnnotations && topAnnotations.length > 0) {
1836
- const cleared = new Set();
1837
- for (const an of topAnnotations) {
1838
- const { value, multiple } = this.computeAnnotationValue(node, an);
1839
- if (multiple) {
1840
- if (!cleared.has(an.name)) {
1841
- const spec = this.doc.resolveAnnotation(an.name);
1842
- if (!spec || spec.config.mergeStrategy !== "append") this.writeln(`${targetName}.metadata.delete("${escapeQuotes(an.name)}")`);
1843
- cleared.add(an.name);
1844
- }
1845
- this.writeln(`$a(${targetName}.metadata, "${escapeQuotes(an.name)}", ${value}, true)`);
1846
- } else this.writeln(`$a(${targetName}.metadata, "${escapeQuotes(an.name)}", ${value})`);
1847
- }
1848
- }
1899
+ if (topAnnotations && topAnnotations.length > 0) this.emitMutatingAnnotations(node, topAnnotations, targetName);
1849
1900
  this.writeln();
1850
1901
  }
1902
+ emitMutatingAnnotations(node, annotations, accessor) {
1903
+ const cleared = new Set();
1904
+ for (const an of annotations) {
1905
+ const { value, multiple } = this.computeAnnotationValue(node, an);
1906
+ if (multiple) {
1907
+ if (!cleared.has(an.name)) {
1908
+ const spec = this.doc.resolveAnnotation(an.name);
1909
+ if (!spec || spec.config.mergeStrategy !== "append") this.writeln(`${accessor}.metadata.delete("${escapeQuotes(an.name)}")`);
1910
+ cleared.add(an.name);
1911
+ }
1912
+ this.writeln(`$a(${accessor}.metadata, "${escapeQuotes(an.name)}", ${value}, true)`);
1913
+ } else this.writeln(`$a(${accessor}.metadata, "${escapeQuotes(an.name)}", ${value})`);
1914
+ }
1915
+ }
1851
1916
  resolveTargetDef(targetName) {
1852
1917
  const unwound = this.doc.unwindType(targetName);
1853
1918
  if (!unwound?.def) return undefined;