@atscript/typescript 0.1.30 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import path from "path";
2
- import { DEFAULT_FORMAT, isArray, isConst, isGroup, isInterface, isPrimitive, isRef, isStructure } from "@atscript/core";
2
+ import { DEFAULT_FORMAT, flattenInterfaceNode, isArray, isConst, isGroup, isInterface, isPrimitive, isRef, isStructure } from "@atscript/core";
3
3
 
4
4
  //#region packages/typescript/src/codegen/code-printer.ts
5
5
  function _define_property$4(obj, key, value) {
@@ -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
  }
@@ -262,14 +264,16 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
262
264
  if (isGrp) this.write(")");
263
265
  return this.write("[]");
264
266
  }
267
+ if (isPrimitive(def)) return this.write(renderPrimitiveTypeDef(def.config.type));
265
268
  }
266
- renderStructure(struct, asClass) {
269
+ renderStructure(struct, asClass, interfaceNode, filterProps) {
267
270
  this.blockln("{}");
268
271
  const patterns = [];
269
272
  const propsDefs = new Set();
270
- for (const prop of Array.from(struct.props.values())) {
273
+ for (const prop of struct.props.values()) {
274
+ if (filterProps?.has(prop.id)) continue;
271
275
  if (prop.token("identifier")?.pattern) {
272
- patterns.push(prop);
276
+ if (!filterProps) patterns.push(prop);
273
277
  continue;
274
278
  }
275
279
  const phantomType = this.phantomPropType(prop.getDefinition());
@@ -285,33 +289,36 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
285
289
  }
286
290
  if (patterns.length > 0) {
287
291
  this.write(`[key: string]: `);
288
- if (patterns.length > 0) {
289
- for (const prop of patterns) propsDefs.add(this.renderTypeDefString(prop.getDefinition()));
290
- const defs = Array.from(propsDefs);
291
- if (defs.length > 1) {
292
- this.indent();
293
- for (const def of defs) {
294
- this.writeln();
295
- this.write("| ");
296
- def.split("\n").forEach((l) => this.write(l.trim()));
297
- }
298
- 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) {
299
297
  this.writeln();
300
- } else defs[0].split("\n").forEach((l) => this.writeln(l));
301
- }
302
- }
303
- if (asClass) {
304
- this.writeln("static __is_atscript_annotated_type: true");
305
- this.writeln(`static type: TAtscriptTypeObject<keyof ${asClass}, ${asClass}>`);
306
- this.writeln(`static metadata: TMetadataMap<AtscriptMetadata>`);
307
- this.writeln(`static validator: (opts?: Partial<TValidatorOptions>) => Validator<typeof ${asClass}>`);
308
- 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. */");
309
- this.writeln("static toJsonSchema: () => any");
310
- if (!this.opts?.exampleData) this.writeln("/** @deprecated Example Data support is disabled. To enable, set `exampleData: true` in tsPlugin options. */");
311
- this.writeln("static toExampleData?: () => any");
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));
312
304
  }
305
+ if (asClass) this.renderStaticDeclarations(asClass, interfaceNode);
313
306
  this.pop();
314
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
+ }
315
322
  renderInterface(node) {
316
323
  this.writeln();
317
324
  const exported = node.token("export")?.text === "export";
@@ -335,44 +342,16 @@ var TypeRenderer = class TypeRenderer extends BaseRenderer {
335
342
  }
336
343
  if (!firstParentProps && isStructure(fpDef)) firstParentProps = fpDef.props;
337
344
  }
338
- this.renderStructureFiltered(resolved, node.id, firstParentProps);
345
+ this.renderStructure(resolved, node.id, node, firstParentProps);
339
346
  } else this.writeln("{}");
340
347
  } else {
341
348
  this.write(`class ${node.id} `);
342
349
  const struct = node.getDefinition();
343
- if (struct?.entity === "structure") this.renderStructure(struct, node.id);
350
+ if (struct?.entity === "structure") this.renderStructure(struct, node.id, node);
344
351
  else this.writeln("{}");
345
352
  }
346
353
  this.writeln();
347
354
  }
348
- /**
349
- * Renders a structure block, optionally filtering out props that exist in a parent.
350
- */ renderStructureFiltered(struct, asClass, filterProps) {
351
- if (!filterProps) return this.renderStructure(struct, asClass);
352
- this.blockln("{}");
353
- for (const prop of Array.from(struct.props.values())) {
354
- if (filterProps.has(prop.id)) continue;
355
- if (prop.token("identifier")?.pattern) continue;
356
- const phantomType = this.phantomPropType(prop.getDefinition());
357
- if (phantomType) {
358
- this.writeln(`// ${prop.id}: ${phantomType}`);
359
- continue;
360
- }
361
- const optional = !!prop.token("optional");
362
- this.write(wrapProp(prop.id), optional ? "?" : "", ": ");
363
- const renderedDef = this.renderTypeDefString(prop.getDefinition());
364
- renderedDef.split("\n").forEach((l) => this.writeln(l));
365
- }
366
- this.writeln("static __is_atscript_annotated_type: true");
367
- this.writeln(`static type: TAtscriptTypeObject<keyof ${asClass}, ${asClass}>`);
368
- this.writeln(`static metadata: TMetadataMap<AtscriptMetadata>`);
369
- this.writeln(`static validator: (opts?: Partial<TValidatorOptions>) => Validator<typeof ${asClass}>`);
370
- 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. */");
371
- this.writeln("static toJsonSchema: () => any");
372
- if (!this.opts?.exampleData) this.writeln("/** @deprecated Example Data support is disabled. To enable, set `exampleData: true` in tsPlugin options. */");
373
- this.writeln("static toExampleData?: () => any");
374
- this.pop();
375
- }
376
355
  renderType(node) {
377
356
  this.writeln();
378
357
  const exported = node.token("export")?.text === "export";
@@ -426,6 +405,87 @@ else if (isPrimitive(realDef)) typeDef = `TAtscriptTypeFinal<${name}>`;
426
405
  this.writeln("const toExampleData: (() => any) | undefined");
427
406
  this.popln();
428
407
  }
408
+ /**
409
+ * Checks whether an interface has the `@db.table` annotation.
410
+ *
411
+ * NOTE: Only `@db.table` interfaces get the `__flat` static property.
412
+ * This is intentionally hardcoded — `__flat` exists solely to improve
413
+ * type-safety for filter expressions and `$select`/`$sort` operations
414
+ * in the DB layer. Non-DB interfaces have no use for dot-notation path types.
415
+ */ hasDbTable(node) {
416
+ return !!node.annotations?.some((a) => a.name === "db.table");
417
+ }
418
+ /**
419
+ * Renders the `static __flat` property — a map of all dot-notation paths
420
+ * to their TypeScript value types.
421
+ *
422
+ * This enables type-safe autocomplete for filter keys and `$select`/`$sort`
423
+ * paths when using `AtscriptDbTable`. The `FlatOf<T>` utility type extracts
424
+ * this map at the type level.
425
+ *
426
+ * Special rendering rules:
427
+ * - **Intermediate paths** (structures, arrays of structures) → `never`
428
+ * (prevents `$eq` comparisons, but allows `$select` and `$exists`)
429
+ * - **`@db.json` fields** → `string` (stored as serialized JSON in DB,
430
+ * not individually queryable)
431
+ * - **Leaf fields** → their original TypeScript type
432
+ */ renderFlat(node) {
433
+ const flatMap = flattenInterfaceNode(this.doc, node);
434
+ if (flatMap.size === 0) return;
435
+ this.write("static __flat: ");
436
+ this.blockln("{}");
437
+ for (const [path$1, descriptor] of flatMap) {
438
+ this.write(`"${escapeQuotes(path$1)}"`);
439
+ if (descriptor.optional) this.write("?");
440
+ this.write(": ");
441
+ if (descriptor.intermediate) this.writeln("never");
442
+ else if (descriptor.dbJson) this.writeln("string");
443
+ else {
444
+ const originalDef = descriptor.propNode?.getDefinition();
445
+ const defToRender = originalDef && !(isGroup(descriptor.def) && descriptor.def !== originalDef) ? originalDef : descriptor.def;
446
+ const renderedDef = this.renderTypeDefString(defToRender);
447
+ renderedDef.split("\n").forEach((l) => this.writeln(l));
448
+ }
449
+ }
450
+ this.pop();
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
459
+ */ renderPk(node) {
460
+ let struct;
461
+ if (node.hasExtends) struct = this.doc.resolveInterfaceExtends(node);
462
+ if (!struct) struct = node.getDefinition();
463
+ if (!struct || !isStructure(struct)) return;
464
+ const pkProps = [];
465
+ for (const [name, prop] of struct.props) {
466
+ if (prop.token("identifier")?.pattern) continue;
467
+ if (prop.countAnnotations("meta.id") > 0) pkProps.push({
468
+ name,
469
+ prop
470
+ });
471
+ }
472
+ if (pkProps.length === 0) return;
473
+ this.writeln();
474
+ if (pkProps.length === 1) {
475
+ this.write("static __pk: ");
476
+ const renderedDef = this.renderTypeDefString(pkProps[0].prop.getDefinition());
477
+ renderedDef.split("\n").forEach((l) => this.writeln(l));
478
+ } else {
479
+ this.write("static __pk: ");
480
+ this.blockln("{}");
481
+ for (const { name, prop } of pkProps) {
482
+ this.write(wrapProp(name), ": ");
483
+ const renderedDef = this.renderTypeDefString(prop.getDefinition());
484
+ renderedDef.split("\n").forEach((l) => this.writeln(l));
485
+ }
486
+ this.pop();
487
+ }
488
+ }
429
489
  phantomPropType(def) {
430
490
  if (!def) return undefined;
431
491
  if (isPrimitive(def) && def.config.type === "phantom") return def.id;
@@ -474,24 +534,6 @@ function renderPrimitiveTypeDef(def) {
474
534
  }
475
535
  }
476
536
 
477
- //#endregion
478
- //#region packages/typescript/src/traverse.ts
479
- function forAnnotatedType(def, handlers) {
480
- switch (def.type.kind) {
481
- case "": {
482
- const typed = def;
483
- if (handlers.phantom && typed.type.designType === "phantom") return handlers.phantom(typed);
484
- return handlers.final(typed);
485
- }
486
- case "object": return handlers.object(def);
487
- case "array": return handlers.array(def);
488
- case "union": return handlers.union(def);
489
- case "intersection": return handlers.intersection(def);
490
- case "tuple": return handlers.tuple(def);
491
- default: throw new Error(`Unknown type kind "${def.type.kind}"`);
492
- }
493
- }
494
-
495
537
  //#endregion
496
538
  //#region packages/typescript/src/validator.ts
497
539
  function _define_property$1(obj, key, value) {
@@ -507,28 +549,35 @@ else obj[key] = value;
507
549
  const regexCache = new Map();
508
550
  var Validator = class {
509
551
  isLimitExceeded() {
510
- if (this.stackErrors.length > 0) return this.stackErrors[this.stackErrors.length - 1].length >= this.opts.errorLimit;
552
+ if (this.stackErrors.length > 0) {
553
+ const top = this.stackErrors[this.stackErrors.length - 1];
554
+ return top !== null && top.length >= this.opts.errorLimit;
555
+ }
511
556
  return this.errors.length >= this.opts.errorLimit;
512
557
  }
513
558
  push(name) {
514
559
  this.stackPath.push(name);
515
- this.stackErrors.push([]);
560
+ this.stackErrors.push(null);
561
+ this.cachedPath = this.stackPath.length <= 1 ? "" : this.stackPath[1] + (this.stackPath.length > 2 ? "." + this.stackPath.slice(2).join(".") : "");
516
562
  }
517
563
  pop(saveErrors) {
518
564
  this.stackPath.pop();
519
565
  const popped = this.stackErrors.pop();
520
- if (saveErrors && popped?.length) popped.forEach((error) => {
521
- this.error(error.message, error.path, error.details);
522
- });
566
+ if (saveErrors && popped !== null && popped !== undefined && popped.length > 0) for (const err of popped) this.error(err.message, err.path, err.details);
567
+ this.cachedPath = this.stackPath.length <= 1 ? "" : this.stackPath[1] + (this.stackPath.length > 2 ? "." + this.stackPath.slice(2).join(".") : "");
523
568
  return popped;
524
569
  }
525
570
  clear() {
526
- this.stackErrors[this.stackErrors.length - 1] = [];
571
+ this.stackErrors[this.stackErrors.length - 1] = null;
527
572
  }
528
573
  error(message, path$1, details) {
529
- const errors = this.stackErrors[this.stackErrors.length - 1] || this.errors;
574
+ let errors = this.stackErrors[this.stackErrors.length - 1];
575
+ if (!errors) if (this.stackErrors.length > 0) {
576
+ errors = [];
577
+ this.stackErrors[this.stackErrors.length - 1] = errors;
578
+ } else errors = this.errors;
530
579
  const error = {
531
- path: path$1 || this.path,
580
+ path: path$1 || this.cachedPath,
532
581
  message
533
582
  };
534
583
  if (details?.length) error.details = details;
@@ -548,9 +597,10 @@ var Validator = class {
548
597
  * @returns `true` if the value matches the type definition.
549
598
  * @throws {ValidatorError} When validation fails and `safe` is not `true`.
550
599
  */ validate(value, safe, context) {
551
- this.push("");
552
600
  this.errors = [];
553
601
  this.stackErrors = [];
602
+ this.stackPath = [""];
603
+ this.cachedPath = "";
554
604
  this.context = context;
555
605
  const passed = this.validateSafe(this.def, value);
556
606
  this.pop(!passed);
@@ -564,7 +614,7 @@ var Validator = class {
564
614
  validateSafe(def, value) {
565
615
  if (this.isLimitExceeded()) return false;
566
616
  if (!isAnnotatedType(def)) throw new Error("Can not validate not-annotated type");
567
- if (typeof this.opts.replace === "function") def = this.opts.replace(def, this.path);
617
+ if (typeof this.opts.replace === "function") def = this.opts.replace(def, this.cachedPath);
568
618
  if (def.optional && value === undefined) return true;
569
619
  for (const plugin of this.opts.plugins) {
570
620
  const result = plugin(this, def, value);
@@ -573,18 +623,21 @@ var Validator = class {
573
623
  return this.validateAnnotatedType(def, value);
574
624
  }
575
625
  get path() {
576
- return this.stackPath.slice(1).join(".");
626
+ return this.cachedPath;
577
627
  }
578
628
  validateAnnotatedType(def, value) {
579
- return forAnnotatedType(def, {
580
- final: (d) => this.validatePrimitive(d, value),
581
- phantom: () => true,
582
- object: (d) => this.validateObject(d, value),
583
- array: (d) => this.validateArray(d, value),
584
- union: (d) => this.validateUnion(d, value),
585
- intersection: (d) => this.validateIntersection(d, value),
586
- tuple: (d) => this.validateTuple(d, value)
587
- });
629
+ switch (def.type.kind) {
630
+ case "": {
631
+ if (def.type.designType === "phantom") return true;
632
+ return this.validatePrimitive(def, value);
633
+ }
634
+ case "object": return this.validateObject(def, value);
635
+ case "array": return this.validateArray(def, value);
636
+ case "union": return this.validateUnion(def, value);
637
+ case "intersection": return this.validateIntersection(def, value);
638
+ case "tuple": return this.validateTuple(def, value);
639
+ default: throw new Error(`Unknown type kind "${def.type.kind}"`);
640
+ }
588
641
  }
589
642
  validateUnion(def, value) {
590
643
  let i = 0;
@@ -648,6 +701,30 @@ var Validator = class {
648
701
  return false;
649
702
  }
650
703
  }
704
+ const uniqueItems = def.metadata.get("expect.array.uniqueItems");
705
+ if (uniqueItems) {
706
+ const separator = "▼↩";
707
+ const seen = new Set();
708
+ const keyProps = new Set();
709
+ if (def.type.of.type.kind === "object") {
710
+ for (const [key, val] of def.type.of.type.props.entries()) if (val.metadata.get("expect.array.key")) keyProps.add(key);
711
+ }
712
+ for (let idx = 0; idx < value.length; idx++) {
713
+ const item = value[idx];
714
+ let key;
715
+ if (keyProps.size > 0) {
716
+ key = "";
717
+ for (const prop of keyProps) key += JSON.stringify(item[prop]) + separator;
718
+ } else key = JSON.stringify(item);
719
+ if (seen.has(key)) {
720
+ this.push(String(idx));
721
+ this.error(uniqueItems.message || "Duplicate items are not allowed");
722
+ this.pop(true);
723
+ return false;
724
+ }
725
+ seen.add(key);
726
+ }
727
+ }
651
728
  let i = 0;
652
729
  let passed = true;
653
730
  for (const item of value) {
@@ -669,21 +746,20 @@ var Validator = class {
669
746
  let passed = true;
670
747
  const valueKeys = new Set(Object.keys(value));
671
748
  const typeKeys = new Set();
672
- const skipList = new Set();
749
+ let skipList;
673
750
  if (this.opts.skipList) {
674
- const path$1 = this.stackPath.length > 1 ? `${this.path}.` : "";
675
- this.opts.skipList.forEach((item) => {
676
- if (item.startsWith(path$1)) {
677
- const key = item.slice(path$1.length);
678
- skipList.add(key);
679
- valueKeys.delete(key);
680
- }
681
- });
751
+ const path$1 = this.stackPath.length > 1 ? `${this.cachedPath}.` : "";
752
+ for (const item of this.opts.skipList) if (item.startsWith(path$1)) {
753
+ const key = item.slice(path$1.length);
754
+ if (!skipList) skipList = new Set();
755
+ skipList.add(key);
756
+ valueKeys.delete(key);
757
+ }
682
758
  }
683
759
  let partialFunctionMatched = false;
684
- if (typeof this.opts.partial === "function") partialFunctionMatched = this.opts.partial(def, this.path);
760
+ if (typeof this.opts.partial === "function") partialFunctionMatched = this.opts.partial(def, this.cachedPath);
685
761
  for (const [key, item] of def.type.props.entries()) {
686
- if (skipList.has(key) || isPhantomType(item)) continue;
762
+ if (skipList && skipList.has(key) || isPhantomType(item)) continue;
687
763
  typeKeys.add(key);
688
764
  if (value[key] === undefined) {
689
765
  if (partialFunctionMatched || this.opts.partial === "deep" || this.opts.partial === true && this.stackPath.length <= 1) continue;
@@ -704,19 +780,21 @@ else {
704
780
  def: propDef
705
781
  });
706
782
  if (matched.length > 0) {
783
+ this.push(key);
707
784
  let keyPassed = false;
708
- for (const { def: def$1 } of matched) if (this.validateSafe(def$1, value[key])) {
709
- this.pop(false);
710
- keyPassed = true;
711
- break;
785
+ for (const { def: propDef } of matched) {
786
+ if (this.validateSafe(propDef, value[key])) {
787
+ keyPassed = true;
788
+ break;
789
+ }
790
+ this.clear();
712
791
  }
713
792
  if (!keyPassed) {
714
- this.push(key);
715
793
  this.validateSafe(matched[0].def, value[key]);
716
794
  this.pop(true);
717
795
  passed = false;
718
796
  if (this.isLimitExceeded()) return false;
719
- }
797
+ } else this.pop(false);
720
798
  } else if (this.opts.unknownProps !== "ignore") {
721
799
  if (this.opts.unknownProps === "error") {
722
800
  this.push(key);
@@ -869,11 +947,13 @@ else {
869
947
  /** Validation errors collected during the last {@link validate} call. */ _define_property$1(this, "errors", void 0);
870
948
  _define_property$1(this, "stackErrors", void 0);
871
949
  _define_property$1(this, "stackPath", void 0);
950
+ _define_property$1(this, "cachedPath", void 0);
872
951
  _define_property$1(this, "context", void 0);
873
952
  this.def = def;
874
953
  this.errors = [];
875
954
  this.stackErrors = [];
876
955
  this.stackPath = [];
956
+ this.cachedPath = "";
877
957
  this.opts = {
878
958
  partial: false,
879
959
  unknownProps: "error",
@@ -1017,6 +1097,24 @@ function isPhantomType(def) {
1017
1097
  return def.type.kind === "" && def.type.designType === "phantom";
1018
1098
  }
1019
1099
 
1100
+ //#endregion
1101
+ //#region packages/typescript/src/traverse.ts
1102
+ function forAnnotatedType(def, handlers) {
1103
+ switch (def.type.kind) {
1104
+ case "": {
1105
+ const typed = def;
1106
+ if (handlers.phantom && typed.type.designType === "phantom") return handlers.phantom(typed);
1107
+ return handlers.final(typed);
1108
+ }
1109
+ case "object": return handlers.object(def);
1110
+ case "array": return handlers.array(def);
1111
+ case "union": return handlers.union(def);
1112
+ case "intersection": return handlers.intersection(def);
1113
+ case "tuple": return handlers.tuple(def);
1114
+ default: throw new Error(`Unknown type kind "${def.type.kind}"`);
1115
+ }
1116
+ }
1117
+
1020
1118
  //#endregion
1021
1119
  //#region packages/typescript/src/json-schema.ts
1022
1120
  /**
@@ -1189,24 +1287,25 @@ var JsRenderer = class extends BaseRenderer {
1189
1287
  this.writeln("// prettier-ignore-start");
1190
1288
  this.writeln("/* eslint-disable */");
1191
1289
  this.writeln("/* oxlint-disable */");
1290
+ let hasMutatingAnnotate = false;
1291
+ const nodesByName = new Map();
1292
+ for (const node of this.doc.nodes) {
1293
+ if (node.entity === "annotate" && node.isMutating) hasMutatingAnnotate = true;
1294
+ if (node.__typeId !== null && node.__typeId !== undefined && node.id) {
1295
+ const name = node.id;
1296
+ if (!nodesByName.has(name)) nodesByName.set(name, []);
1297
+ nodesByName.get(name).push(node);
1298
+ }
1299
+ }
1300
+ for (const [name, nodes] of nodesByName) if (nodes.length === 1) this.typeIds.set(nodes[0], name);
1301
+ else for (let i = 0; i < nodes.length; i++) this.typeIds.set(nodes[i], `${name}__${i + 1}`);
1192
1302
  const imports = ["defineAnnotatedType as $", "annotate as $a"];
1193
- const hasMutatingAnnotate = this.doc.nodes.some((n) => n.entity === "annotate" && n.isMutating);
1194
1303
  if (hasMutatingAnnotate) imports.push("cloneRefProp as $c");
1195
1304
  const jsonSchemaMode = resolveJsonSchemaMode(this.opts);
1196
1305
  if (jsonSchemaMode === "lazy") imports.push("buildJsonSchema as $$");
1197
1306
  if (this.opts?.exampleData) imports.push("createDataFromAnnotatedType as $e");
1198
1307
  if (jsonSchemaMode === false) imports.push("throwFeatureDisabled as $d");
1199
1308
  this.writeln(`import { ${imports.join(", ")} } from "@atscript/typescript/utils"`);
1200
- const nameCounts = new Map();
1201
- const nodesByName = new Map();
1202
- for (const node of this.doc.nodes) if (node.__typeId !== null && node.__typeId !== undefined && node.id) {
1203
- const name = node.id;
1204
- nameCounts.set(name, (nameCounts.get(name) || 0) + 1);
1205
- if (!nodesByName.has(name)) nodesByName.set(name, []);
1206
- nodesByName.get(name).push(node);
1207
- }
1208
- for (const [name, nodes] of nodesByName) if (nodes.length === 1) this.typeIds.set(nodes[0], name);
1209
- else for (let i = 0; i < nodes.length; i++) this.typeIds.set(nodes[i], `${name}__${i + 1}`);
1210
1309
  }
1211
1310
  buildAdHocMap(annotateNodes) {
1212
1311
  const map = new Map();
@@ -1215,7 +1314,8 @@ else for (let i = 0; i < nodes.length; i++) this.typeIds.set(nodes[i], `${name}_
1215
1314
  const anns = entry.annotations || [];
1216
1315
  if (anns.length > 0) {
1217
1316
  const existing = map.get(path$1);
1218
- map.set(path$1, existing ? [...existing, ...anns] : anns);
1317
+ if (existing) existing.push(...anns);
1318
+ else map.set(path$1, [...anns]);
1219
1319
  }
1220
1320
  }
1221
1321
  return map.size > 0 ? map : null;
@@ -1274,35 +1374,20 @@ else def = def.getDefinition() || def;
1274
1374
  this.renderExampleDataMethod(node);
1275
1375
  }
1276
1376
  renderInterface(node) {
1277
- this.writeln();
1278
- const exported = node.token("export")?.text === "export";
1279
- this.write(exported ? "export " : "");
1280
- this.write(`class ${node.id} `);
1281
- this.blockln("{}");
1282
- this.renderClassStatics(node);
1283
- this.popln();
1284
- this.postAnnotate.push(node);
1285
- this.writeln();
1377
+ this.renderDefinitionClass(node);
1286
1378
  }
1287
1379
  renderType(node) {
1288
- this.writeln();
1289
- const exported = node.token("export")?.text === "export";
1290
- this.write(exported ? "export " : "");
1291
- this.write(`class ${node.id} `);
1292
- this.blockln("{}");
1293
- this.renderClassStatics(node);
1294
- this.popln();
1295
- this.postAnnotate.push(node);
1296
- this.writeln();
1380
+ this.renderDefinitionClass(node);
1297
1381
  }
1298
1382
  renderAnnotate(node) {
1299
1383
  if (node.isMutating) {
1300
1384
  this.postAnnotate.push(node);
1301
1385
  return;
1302
1386
  }
1303
- const targetName = node.targetName;
1304
- const unwound = this.doc.unwindType(targetName);
1305
- if (!unwound?.def) return;
1387
+ if (!this.doc.unwindType(node.targetName)?.def) return;
1388
+ this.renderDefinitionClass(node);
1389
+ }
1390
+ renderDefinitionClass(node) {
1306
1391
  this.writeln();
1307
1392
  const exported = node.token("export")?.text === "export";
1308
1393
  this.write(exported ? "export " : "");
@@ -1460,6 +1545,11 @@ else handle.prop(prop.id, propHandle.$type);
1460
1545
  handle.annotate(a.name, true);
1461
1546
  break;
1462
1547
  }
1548
+ case "expect.array.uniqueItems":
1549
+ case "expect.array.key": {
1550
+ handle.annotate(a.name, { message: a.args[0]?.text });
1551
+ break;
1552
+ }
1463
1553
  default:
1464
1554
  }
1465
1555
  });
@@ -1527,10 +1617,7 @@ else handle.prop(prop.id, propHandle.$type);
1527
1617
  this.writeln(`$("array"${name ? `, ${name}` : ""})`).indent().defineArray(node).unindent();
1528
1618
  return this;
1529
1619
  }
1530
- default: {
1531
- console.log("!!!!!!! UNKNOWN", node.entity);
1532
- return this;
1533
- }
1620
+ default: return this;
1534
1621
  }
1535
1622
  }
1536
1623
  defineConst(node) {
@@ -1742,40 +1829,25 @@ else targetValue = "true";
1742
1829
  this.writeln(`$c(${clone.parentPath}, "${escapeQuotes(clone.propName)}")`);
1743
1830
  }
1744
1831
  }
1745
- for (const { entry, accessors } of entryAccessors) {
1746
- const anns = entry.annotations;
1747
- for (const accessor of accessors) {
1748
- const cleared = new Set();
1749
- for (const an of anns) {
1750
- const { value, multiple } = this.computeAnnotationValue(entry, an);
1751
- if (multiple) {
1752
- if (!cleared.has(an.name)) {
1753
- const spec = this.doc.resolveAnnotation(an.name);
1754
- if (!spec || spec.config.mergeStrategy !== "append") this.writeln(`${accessor}.metadata.delete("${escapeQuotes(an.name)}")`);
1755
- cleared.add(an.name);
1756
- }
1757
- this.writeln(`$a(${accessor}.metadata, "${escapeQuotes(an.name)}", ${value}, true)`);
1758
- } else this.writeln(`$a(${accessor}.metadata, "${escapeQuotes(an.name)}", ${value})`);
1759
- }
1760
- }
1761
- }
1832
+ for (const { entry, accessors } of entryAccessors) for (const accessor of accessors) this.emitMutatingAnnotations(entry, entry.annotations, accessor);
1762
1833
  const topAnnotations = node.annotations;
1763
- if (topAnnotations && topAnnotations.length > 0) {
1764
- const cleared = new Set();
1765
- for (const an of topAnnotations) {
1766
- const { value, multiple } = this.computeAnnotationValue(node, an);
1767
- if (multiple) {
1768
- if (!cleared.has(an.name)) {
1769
- const spec = this.doc.resolveAnnotation(an.name);
1770
- if (!spec || spec.config.mergeStrategy !== "append") this.writeln(`${targetName}.metadata.delete("${escapeQuotes(an.name)}")`);
1771
- cleared.add(an.name);
1772
- }
1773
- this.writeln(`$a(${targetName}.metadata, "${escapeQuotes(an.name)}", ${value}, true)`);
1774
- } else this.writeln(`$a(${targetName}.metadata, "${escapeQuotes(an.name)}", ${value})`);
1775
- }
1776
- }
1834
+ if (topAnnotations && topAnnotations.length > 0) this.emitMutatingAnnotations(node, topAnnotations, targetName);
1777
1835
  this.writeln();
1778
1836
  }
1837
+ emitMutatingAnnotations(node, annotations, accessor) {
1838
+ const cleared = new Set();
1839
+ for (const an of annotations) {
1840
+ const { value, multiple } = this.computeAnnotationValue(node, an);
1841
+ if (multiple) {
1842
+ if (!cleared.has(an.name)) {
1843
+ const spec = this.doc.resolveAnnotation(an.name);
1844
+ if (!spec || spec.config.mergeStrategy !== "append") this.writeln(`${accessor}.metadata.delete("${escapeQuotes(an.name)}")`);
1845
+ cleared.add(an.name);
1846
+ }
1847
+ this.writeln(`$a(${accessor}.metadata, "${escapeQuotes(an.name)}", ${value}, true)`);
1848
+ } else this.writeln(`$a(${accessor}.metadata, "${escapeQuotes(an.name)}", ${value})`);
1849
+ }
1850
+ }
1779
1851
  resolveTargetDef(targetName) {
1780
1852
  const unwound = this.doc.unwindType(targetName);
1781
1853
  if (!unwound?.def) return undefined;