@colyseus/schema 5.0.13 → 5.0.19

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.
Files changed (50) hide show
  1. package/README.md +7 -2
  2. package/build/Metadata.d.ts +10 -1
  3. package/build/Reflection.d.ts +36 -34
  4. package/build/codegen/api.d.ts +2 -0
  5. package/build/codegen/cli.cjs +322 -31
  6. package/build/codegen/cli.cjs.map +1 -1
  7. package/build/codegen/parser.d.ts +6 -1
  8. package/build/codegen/resolve.d.ts +25 -0
  9. package/build/codegen/types.d.ts +2 -0
  10. package/build/encoder/ChangeTree.d.ts +40 -12
  11. package/build/encoder/Encoder.d.ts +1 -1
  12. package/build/encoder/Root.d.ts +9 -0
  13. package/build/encoder/StateView.d.ts +38 -1
  14. package/build/encoder/changeTree/inheritedFlags.d.ts +13 -19
  15. package/build/encoder/changeTree/liveIteration.d.ts +8 -0
  16. package/build/encoder/changeTree/parentChain.d.ts +30 -8
  17. package/build/encoder/streaming.d.ts +1 -1
  18. package/build/index.cjs +3237 -2834
  19. package/build/index.cjs.map +1 -1
  20. package/build/index.js +3233 -2830
  21. package/build/index.mjs +3237 -2834
  22. package/build/index.mjs.map +1 -1
  23. package/build/types/HelperTypes.d.ts +3 -5
  24. package/build/types/TypeContext.d.ts +0 -17
  25. package/build/types/builder.d.ts +8 -11
  26. package/build/types/symbols.d.ts +1 -0
  27. package/package.json +2 -2
  28. package/src/Metadata.ts +59 -77
  29. package/src/Reflection.ts +9 -5
  30. package/src/annotations.ts +19 -13
  31. package/src/codegen/api.ts +3 -1
  32. package/src/codegen/cli.ts +5 -2
  33. package/src/codegen/parser.ts +69 -31
  34. package/src/codegen/resolve.ts +322 -0
  35. package/src/codegen/types.ts +4 -1
  36. package/src/decoder/DecodeOperation.ts +13 -2
  37. package/src/encoder/ChangeTree.ts +76 -25
  38. package/src/encoder/EncodeOperation.ts +10 -1
  39. package/src/encoder/Encoder.ts +52 -2
  40. package/src/encoder/Root.ts +28 -8
  41. package/src/encoder/StateView.ts +150 -66
  42. package/src/encoder/changeTree/inheritedFlags.ts +164 -45
  43. package/src/encoder/changeTree/liveIteration.ts +24 -3
  44. package/src/encoder/changeTree/parentChain.ts +72 -15
  45. package/src/encoder/streaming.ts +2 -1
  46. package/src/types/HelperTypes.ts +16 -13
  47. package/src/types/TypeContext.ts +5 -52
  48. package/src/types/builder.ts +21 -16
  49. package/src/types/custom/ArraySchema.ts +57 -14
  50. package/src/types/symbols.ts +3 -0
@@ -70,7 +70,7 @@ export type InferValueType<T> = T extends FieldBuilder<infer V> ? V : T extends
70
70
  stream: infer ChildType;
71
71
  } ? StreamSchema<ChildType> : T extends Constructor ? InstanceType<T> : T extends Record<string | number, string | number> ? T[keyof T] : T extends PrimitiveType ? T : never;
72
72
  type OptionalBuilderKeys<T> = {
73
- [K in keyof T]: T[K] extends FieldBuilder<infer V> ? (undefined extends V ? K : never) : never;
73
+ [K in keyof T]: T[K] extends FieldBuilder<unknown, boolean, infer O extends boolean> ? (O extends true ? K : never) : never;
74
74
  }[keyof T];
75
75
  type RequiredBuilderKeys<T> = Exclude<keyof T, OptionalBuilderKeys<T>>;
76
76
  export type InferSchemaInstanceType<T> = {
@@ -89,12 +89,10 @@ export type NonFunctionNonPrimitivePropNames<T> = {
89
89
  }[keyof T];
90
90
  type ToJSONValue<U> = U extends Schema ? ToJSON<U> : PrimitiveStringToType<U>;
91
91
  type ToJSONField<X> = X extends MapSchema<infer U> ? Record<string, ToJSONValue<U>> : X extends Map<string, infer U> ? Record<string, ToJSONValue<U>> : X extends ArraySchema<infer U> ? ToJSONValue<U>[] : X extends SetSchema<infer U> ? ToJSONValue<U>[] : X extends CollectionSchema<infer U> ? ToJSONValue<U>[] : X extends Schema ? ToJSON<X> : X;
92
- type ToJSONRequiredKeys<T> = {
93
- [K in keyof T]-?: undefined extends T[K] ? never : K;
94
- }[keyof T];
95
92
  type ToJSONOptionalKeys<T> = {
96
- [K in keyof T]-?: undefined extends T[K] ? K : never;
93
+ [K in keyof T]-?: undefined extends {} ? ({} extends Pick<T, K> ? K : never) : (undefined extends T[K] ? K : never);
97
94
  }[keyof T];
95
+ type ToJSONRequiredKeys<T> = Exclude<keyof T, ToJSONOptionalKeys<T>>;
98
96
  export type ToJSON<T> = NonFunctionProps<{
99
97
  [K in ToJSONRequiredKeys<T>]: ToJSONField<T[K]>;
100
98
  } & {
@@ -5,18 +5,6 @@ export declare class TypeContext {
5
5
  };
6
6
  schemas: Map<typeof Schema, number>;
7
7
  hasFilters: boolean;
8
- parentFiltered: {
9
- [typeIdAndParentIndex: string]: boolean;
10
- };
11
- /**
12
- * True iff `parentFiltered` has at least one entry. Flipped on by
13
- * `registerFilteredByParent` and read in `checkInheritedFlags` as a
14
- * cheap gate to skip the string-keyed `parentFiltered[key]` lookup
15
- * when no class has registered filter inheritance via ancestry — the
16
- * common case when @view tags exist only on sibling fields, not
17
- * along any attachment chain.
18
- */
19
- hasParentFilteredEntries: boolean;
20
8
  /**
21
9
  * For inheritance support
22
10
  * Keeps track of which classes extends which. (parent -> children)
@@ -31,10 +19,5 @@ export declare class TypeContext {
31
19
  add(schema: typeof Schema, typeid?: number): boolean;
32
20
  getTypeId(klass: typeof Schema): number;
33
21
  private discoverTypes;
34
- /**
35
- * Keep track of which classes have filters applied.
36
- * Format: `${typeid}-${parentTypeid}-${parentIndex}`
37
- */
38
- private registerFilteredByParent;
39
22
  debug(): string;
40
23
  }
@@ -45,10 +45,11 @@ export type BuilderOf<T> = FieldBuilder<T>;
45
45
  * Schema ref whose `initialize` takes zero args.
46
46
  * - `IsOptional` is a compile-time brand for `.optional()`. Both
47
47
  * `HasDefault` and `IsOptional` make the field omittable in
48
- * `BuilderInitProps<T>`. A separate brand (rather than reading
49
- * `undefined extends V`) sidesteps a TypeScript quirk where
50
- * class-generic-inferred `V` resolves `undefined extends V` as `true`
51
- * even for non-undefined types.
48
+ * `BuilderInitProps<T>`; `IsOptional` alone marks the instance property
49
+ * `?:`. A separate brand (rather than reading `undefined extends V`)
50
+ * keeps both correct for consumers compiling with
51
+ * `strictNullChecks: false`, where `undefined extends V` is true for
52
+ * every V.
52
53
  *
53
54
  * schema() reads the internal configuration via `toDefinition()` and wires
54
55
  * up metadata through the existing pipeline.
@@ -199,8 +200,8 @@ export declare function isBuilder(value: any): value is FieldBuilder<any>;
199
200
  * Two call signatures, NOT a defaulted generic `<T extends TBase = TBase>`: the
200
201
  * bare form must return a CONCRETE `FieldBuilder<TBase>` so `schema({ x:
201
202
  * t.number() })` still infers `x: number`. A defaulted free type parameter gets
202
- * captured as `any` during `schema()`'s self-referential field inference (and
203
- * `undefined extends any` then flips every field optional).
203
+ * captured as `any` during `schema()`'s self-referential field inference,
204
+ * degrading every field's value type to `any`.
204
205
  *
205
206
  * NOTE: the refinement is a TYPE-LEVEL assertion, not a runtime guarantee — the
206
207
  * wire still carries the codec's full range and the DECODER writes whatever
@@ -212,26 +213,22 @@ interface PrimitiveFactory<TBase> {
212
213
  (): FieldBuilder<TBase>;
213
214
  <T extends TBase>(): FieldBuilder<T>;
214
215
  }
215
- export type ChildType = RawPrimitiveType | Constructor<Schema> | FieldBuilder<any>;
216
+ export type ChildType = RawPrimitiveType | Constructor<Schema>;
216
217
  interface ArrayFactory {
217
218
  <C extends Constructor<Schema>>(child: C): FieldBuilder<ArraySchema<InstanceType<C>>, true, false>;
218
219
  <P extends RawPrimitiveType>(child: P): FieldBuilder<ArraySchema<InferValueType<P>>, true, false>;
219
- <V>(child: FieldBuilder<V>): FieldBuilder<ArraySchema<V>, true, false>;
220
220
  }
221
221
  interface MapFactory {
222
222
  <C extends Constructor<Schema>>(child: C): FieldBuilder<MapSchema<InstanceType<C>>, true, false>;
223
223
  <P extends RawPrimitiveType>(child: P): FieldBuilder<MapSchema<InferValueType<P>>, true, false>;
224
- <V>(child: FieldBuilder<V>): FieldBuilder<MapSchema<V>, true, false>;
225
224
  }
226
225
  interface SetFactory {
227
226
  <C extends Constructor<Schema>>(child: C): FieldBuilder<SetSchema<InstanceType<C>>, true, false>;
228
227
  <P extends RawPrimitiveType>(child: P): FieldBuilder<SetSchema<InferValueType<P>>, true, false>;
229
- <V>(child: FieldBuilder<V>): FieldBuilder<SetSchema<V>, true, false>;
230
228
  }
231
229
  interface CollectionFactory {
232
230
  <C extends Constructor<Schema>>(child: C): FieldBuilder<CollectionSchema<InstanceType<C>>, true, false>;
233
231
  <P extends RawPrimitiveType>(child: P): FieldBuilder<CollectionSchema<InferValueType<P>>, true, false>;
234
- <V>(child: FieldBuilder<V>): FieldBuilder<CollectionSchema<V>, true, false>;
235
232
  }
236
233
  interface StreamFactory {
237
234
  <C extends Constructor<Schema>>(child: C): FieldBuilder<StreamSchema<InstanceType<C>>, true, false>;
@@ -80,6 +80,7 @@ export declare const $viewFieldIndexes = "~__viewFieldIndexes";
80
80
  export declare const $fieldIndexesByViewTag = "$__fieldIndexesByViewTag";
81
81
  export declare const $unreliableFieldIndexes = "~__unreliableFieldIndexes";
82
82
  export declare const $patchOnlyFieldIndexes = "~__patchOnlyFieldIndexes";
83
+ export declare const $fullSyncSkipIndexes = "~__fullSyncSkipIndexes";
83
84
  export declare const $fullStateOnlyFieldIndexes = "~__fullStateOnlyFieldIndexes";
84
85
  export declare const $streamFieldIndexes = "~__streamFieldIndexes";
85
86
  export declare const $streamPriorities = "~__streamPriorities";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/schema",
3
- "version": "5.0.13",
3
+ "version": "5.0.19",
4
4
  "description": "Binary state serializer with delta encoding for games",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  "watch": "tsc -p tsconfig.build.json -w",
19
19
  "typecheck:build": "tsc -p tsconfig.build.json --noEmit",
20
20
  "test": "npm run test:types && tsx --tsconfig tsconfig.test.json ./node_modules/mocha/bin/mocha.js \"test/*.test.ts\" \"test/**/*.test.ts\"",
21
- "test:types": "tsc -p tsconfig.build.json && tsc -p tsconfig.types.json",
21
+ "test:types": "tsc -p tsconfig.build.json && tsc -p tsconfig.types.json && tsc -p tsconfig.types-nostrict.json",
22
22
  "test:exports": "node test/verify-exports.mjs",
23
23
  "typecheck": "tsc -p tsconfig.test.json",
24
24
  "coverage": "c8 npm run test",
package/src/Metadata.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { DefinitionType, getPropertyDescriptor } from "./annotations.js";
2
2
  import { Schema } from "./Schema.js";
3
3
  import { getType, registeredTypes, TypeDefinition } from "./types/registry.js";
4
- import { $decoder, $descriptors, $encoder, $encoders, $fieldIndexesByViewTag, $numFields, $refTypeFieldIndexes, $fullStateOnlyFieldIndexes, $streamFieldIndexes, $streamPriorities, $track, $patchOnlyFieldIndexes, $unreliableFieldIndexes, $viewFieldIndexes } from "./types/symbols.js";
4
+ import { $decoder, $descriptors, $encoder, $encoders, $fieldIndexesByViewTag, $numFields, $refTypeFieldIndexes, $fullStateOnlyFieldIndexes, $fullSyncSkipIndexes, $streamFieldIndexes, $streamPriorities, $track, $patchOnlyFieldIndexes, $unreliableFieldIndexes, $viewFieldIndexes } from "./types/symbols.js";
5
5
  import { ARRAY_STREAM_NOT_SUPPORTED } from "./encoder/streaming.js";
6
6
  import { encode } from "./encoding/encode.js";
7
7
  import { TypeContext } from "./types/TypeContext.js";
@@ -37,6 +37,8 @@ export type Metadata =
37
37
  { [$refTypeFieldIndexes]: number[]; } & // all field indexes containing Ref types (Schema, ArraySchema, MapSchema, etc)
38
38
  { [$unreliableFieldIndexes]: number[]; } & // all field indexes tagged with @unreliable
39
39
  { [$patchOnlyFieldIndexes]: number[]; } & // all field indexes tagged with @patchOnly (not persisted to snapshots)
40
+ { [$fullSyncSkipIndexes]: number[]; } & // @patchOnly ∪ @deprecated() — never read during full sync
41
+
40
42
  { [$fullStateOnlyFieldIndexes]: number[]; } & // all field indexes tagged @fullStateOnly / .fullStateOnly() (not tracked after assignment)
41
43
  { [$streamFieldIndexes]: number[]; } & // all field indexes holding a t.stream(...) collection
42
44
  { [$streamPriorities]: { [field: number]: (view: any, element: any) => number }; } & // per-stream-field priority callback declared at schema definition time
@@ -113,6 +115,30 @@ function isTSEnum(_enum: any) {
113
115
  return false;
114
116
  }
115
117
 
118
+ // Copied parent → subclass on Metadata.initialize, so each class owns its list.
119
+ const INHERITED_ARRAY_KEYS = [
120
+ $refTypeFieldIndexes,
121
+ $unreliableFieldIndexes,
122
+ $patchOnlyFieldIndexes,
123
+ $fullSyncSkipIndexes,
124
+ $fullStateOnlyFieldIndexes,
125
+ $streamFieldIndexes,
126
+ $encoders,
127
+ ];
128
+
129
+ /** Append to a non-enumerable metadata index list, creating it on first use. */
130
+ function pushIndexList(metadata: any, key: string, index: number) {
131
+ if (!metadata[key]) {
132
+ Object.defineProperty(metadata, key, {
133
+ value: [],
134
+ enumerable: false,
135
+ configurable: true,
136
+ writable: true,
137
+ });
138
+ }
139
+ metadata[key].push(index);
140
+ }
141
+
116
142
  export const Metadata = {
117
143
 
118
144
  addField(metadata: any, index: number, name: string, type: DefinitionType, descriptor?: PropertyDescriptor) {
@@ -302,15 +328,26 @@ export const Metadata = {
302
328
  }
303
329
  metadata[index].patchOnly = true;
304
330
 
305
- if (!metadata[$patchOnlyFieldIndexes]) {
306
- Object.defineProperty(metadata, $patchOnlyFieldIndexes, {
307
- value: [],
308
- enumerable: false,
309
- configurable: true,
310
- writable: true,
311
- });
312
- }
313
- metadata[$patchOnlyFieldIndexes].push(index);
331
+ pushIndexList(metadata, $patchOnlyFieldIndexes, index);
332
+ pushIndexList(metadata, $fullSyncSkipIndexes, index); // not persisted to snapshots
333
+ },
334
+
335
+ /**
336
+ * `@deprecated()` bookkeeping: the field keeps its wire index (so peers
337
+ * that still carry it stay compatible) but is excluded from full sync —
338
+ * its accessor may throw — and hidden from `for..in` consumers.
339
+ */
340
+ setDeprecated(metadata: Metadata, fieldName: string) {
341
+ const index = metadata[fieldName];
342
+ metadata[index].deprecated = true;
343
+
344
+ pushIndexList(metadata, $fullSyncSkipIndexes, index);
345
+
346
+ Object.defineProperty(metadata, index, {
347
+ value: metadata[index],
348
+ enumerable: false,
349
+ configurable: true
350
+ });
314
351
  },
315
352
 
316
353
  setFullStateOnly(metadata: Metadata, fieldName: string) {
@@ -324,15 +361,7 @@ export const Metadata = {
324
361
  }
325
362
  metadata[index].fullStateOnly = true;
326
363
 
327
- if (!metadata[$fullStateOnlyFieldIndexes]) {
328
- Object.defineProperty(metadata, $fullStateOnlyFieldIndexes, {
329
- value: [],
330
- enumerable: false,
331
- configurable: true,
332
- writable: true,
333
- });
334
- }
335
- metadata[$fullStateOnlyFieldIndexes].push(index);
364
+ pushIndexList(metadata, $fullStateOnlyFieldIndexes, index);
336
365
  },
337
366
 
338
367
  setStream(metadata: Metadata, fieldName: string) {
@@ -510,54 +539,17 @@ export const Metadata = {
510
539
  });
511
540
  }
512
541
 
513
- // $refTypeFieldIndexes
514
- if (parentMetadata[$refTypeFieldIndexes] !== undefined) {
515
- Object.defineProperty(metadata, $refTypeFieldIndexes, {
516
- value: [...parentMetadata[$refTypeFieldIndexes]],
517
- enumerable: false,
518
- configurable: true,
519
- writable: true,
520
- });
521
- }
522
-
523
- // $unreliableFieldIndexes
524
- if (parentMetadata[$unreliableFieldIndexes] !== undefined) {
525
- Object.defineProperty(metadata, $unreliableFieldIndexes, {
526
- value: [...parentMetadata[$unreliableFieldIndexes]],
527
- enumerable: false,
528
- configurable: true,
529
- writable: true,
530
- });
531
- }
532
-
533
- // $patchOnlyFieldIndexes
534
- if (parentMetadata[$patchOnlyFieldIndexes] !== undefined) {
535
- Object.defineProperty(metadata, $patchOnlyFieldIndexes, {
536
- value: [...parentMetadata[$patchOnlyFieldIndexes]],
537
- enumerable: false,
538
- configurable: true,
539
- writable: true,
540
- });
541
- }
542
-
543
- // $fullStateOnlyFieldIndexes
544
- if (parentMetadata[$fullStateOnlyFieldIndexes] !== undefined) {
545
- Object.defineProperty(metadata, $fullStateOnlyFieldIndexes, {
546
- value: [...parentMetadata[$fullStateOnlyFieldIndexes]],
547
- enumerable: false,
548
- configurable: true,
549
- writable: true,
550
- });
551
- }
552
-
553
- // $streamFieldIndexes
554
- if (parentMetadata[$streamFieldIndexes] !== undefined) {
555
- Object.defineProperty(metadata, $streamFieldIndexes, {
556
- value: [...parentMetadata[$streamFieldIndexes]],
557
- enumerable: false,
558
- configurable: true,
559
- writable: true,
560
- });
542
+ // per-class arrays the subclass extends independently
543
+ for (const key of INHERITED_ARRAY_KEYS) {
544
+ const list = parentMetadata[key] as unknown as any[] | undefined;
545
+ if (list !== undefined) {
546
+ Object.defineProperty(metadata, key, {
547
+ value: [...list],
548
+ enumerable: false,
549
+ configurable: true,
550
+ writable: true,
551
+ });
552
+ }
561
553
  }
562
554
 
563
555
  // $descriptors
@@ -567,16 +559,6 @@ export const Metadata = {
567
559
  configurable: true,
568
560
  writable: true,
569
561
  });
570
-
571
- // $encoders
572
- if (parentMetadata[$encoders] !== undefined) {
573
- Object.defineProperty(metadata, $encoders, {
574
- value: [...parentMetadata[$encoders]],
575
- enumerable: false,
576
- configurable: true,
577
- writable: true,
578
- });
579
- }
580
562
  }
581
563
  }
582
564
 
package/src/Reflection.ts CHANGED
@@ -149,9 +149,15 @@ Reflection.encode = function (encoder: Encoder, it: Iterator = { offset: 0 }) {
149
149
  // if metadata is the same reference as the parent class - it means the class has no own metadata
150
150
  //
151
151
  if (metadata !== inheritFrom[Symbol.metadata]) {
152
- for (const fieldIndex in metadata) {
153
- const index = Number(fieldIndex);
154
- const fieldName = metadata[index].name;
152
+ // Walk by index rather than `for…in`: `@deprecated()` makes its
153
+ // metadata slot non-enumerable, and dropping it from the payload
154
+ // shifts every later field down one wire index on the peer.
155
+ const numFields = (metadata[$numFields] ?? -1) as number;
156
+ for (let index = 0; index <= numFields; index++) {
157
+ const field = metadata[index];
158
+ if (field === undefined) { continue; }
159
+
160
+ const fieldName = field.name;
155
161
 
156
162
  // skip fields from parent classes
157
163
  if (!Object.prototype.hasOwnProperty.call(metadata, fieldName)) {
@@ -163,8 +169,6 @@ Reflection.encode = function (encoder: Encoder, it: Iterator = { offset: 0 }) {
163
169
 
164
170
  let fieldType: string;
165
171
 
166
- const field = metadata[index];
167
-
168
172
  if (typeof (field.type) === "string") {
169
173
  fieldType = field.type;
170
174
 
@@ -477,12 +477,27 @@ function makeCollectionSetter(
477
477
 
478
478
  if (value !== undefined && value !== null) {
479
479
  // automatic Array → ArraySchema / Map → MapSchema conversion.
480
+ // `$childType` goes on before populating — push()/set() gate
481
+ // their `assertInstanceType` on it.
480
482
  if (isArrayKlass && !(value instanceof ArraySchema)) {
481
- value = new ArraySchema(...value);
483
+ const array: any = new ArraySchema();
484
+ array[$childType] = type;
485
+ array.push(...value);
486
+ value = array;
487
+
482
488
  } else if (isMapKlass && !(value instanceof MapSchema)) {
483
- value = new MapSchema(value);
489
+ const map: any = new MapSchema();
490
+ map[$childType] = type;
491
+ if (value instanceof Map) {
492
+ value.forEach((v, k) => map.set(k, v));
493
+ } else {
494
+ for (const k in value) { map.set(k, value[k]); }
495
+ }
496
+ value = map;
497
+
498
+ } else {
499
+ value[$childType] = type;
484
500
  }
485
- value[$childType] = type;
486
501
 
487
502
  const changeTree = this[$changes];
488
503
  const ctor = this.constructor as typeof Schema;
@@ -569,9 +584,7 @@ export function getPropertyDescriptor(
569
584
  export function deprecated(throws: boolean = true): PropertyDecorator {
570
585
  return function (klass: typeof Schema, field: string) {
571
586
  const metadata = Metadata.initialize(klass.constructor as typeof Schema);
572
- const fieldIndex = metadata[field];
573
-
574
- metadata[fieldIndex].deprecated = true;
587
+ Metadata.setDeprecated(metadata, field);
575
588
 
576
589
  if (throws) {
577
590
  metadata[$descriptors] ??= {};
@@ -584,13 +597,6 @@ export function deprecated(throws: boolean = true): PropertyDecorator {
584
597
  // Override accessor on the prototype so deprecated throws at access.
585
598
  Object.defineProperty(klass, field, metadata[$descriptors][field]);
586
599
  }
587
-
588
- // flag metadata[field] as non-enumerable
589
- Object.defineProperty(metadata, fieldIndex, {
590
- value: metadata[fieldIndex],
591
- enumerable: false,
592
- configurable: true
593
- });
594
600
  }
595
601
  }
596
602
 
@@ -24,6 +24,8 @@ export interface GenerateOptions {
24
24
  decorator?: string;
25
25
  namespace?: string;
26
26
  bundle?: boolean;
27
+ /** Overrides the nearest-tsconfig lookup used to resolve import path aliases. */
28
+ tsconfig?: string;
27
29
  }
28
30
 
29
31
  export function generate(targetId: string, options: GenerateOptions) {
@@ -53,7 +55,7 @@ export function generate(targetId: string, options: GenerateOptions) {
53
55
  return acc;
54
56
  }, [])
55
57
 
56
- const structures = parseFiles(options.files, options.decorator);
58
+ const structures = parseFiles(options.files, options.decorator, undefined, { tsconfig: options.tsconfig });
57
59
 
58
60
  // Post-process classes before generating
59
61
  structures.classes.forEach(klass => klass.postProcessing());
@@ -21,7 +21,9 @@ ${Object.
21
21
 
22
22
  Optional:
23
23
  --namespace: generate namespace on output code
24
- --decorator: custom name for @type decorator to scan for`);
24
+ --decorator: custom name for @type decorator to scan for
25
+ --tsconfig: tsconfig.json to resolve import path aliases with
26
+ (default: nearest tsconfig.json/jsconfig.json above each source file)`);
25
27
  process.exit(exitCode);
26
28
  }
27
29
 
@@ -49,7 +51,8 @@ try {
49
51
  decorator: args.decorator,
50
52
  output: args.output,
51
53
  namespace: args.namespace,
52
- bundle: args.bundle
54
+ bundle: args.bundle,
55
+ tsconfig: args.tsconfig,
53
56
  });
54
57
 
55
58
  } catch (e) {
@@ -2,6 +2,7 @@ import * as ts from "typescript";
2
2
  import * as path from "path";
3
3
  import { readFileSync } from "fs";
4
4
  import { IStructure, Class, Interface, Property, Context, Enum, QuantizedProperty } from "./types.js";
5
+ import { ResolveOptions, isOwnPackageSource, resetResolver, resolveNonRelativeImport, resolveSourceFile, sourceFileCandidates } from "./resolve.js";
5
6
 
6
7
  let currentStructure: IStructure;
7
8
  let currentProperty: Property;
@@ -14,10 +15,12 @@ const BUILDER_COLLECTION_KINDS = new Set(["array", "map", "set", "collection"]);
14
15
 
15
16
  /**
16
17
  * For a t.*().chain().calls() expression, walk down to the base `t.X(...)`
17
- * call and return its method name and first argument. Returns null if the
18
+ * call and return its method name, first argument, and the names of the
19
+ * chained modifiers (`.view()`, `.deprecated()`, …). Returns null if the
18
20
  * node does not look like a builder chain.
19
21
  */
20
- function extractBuilderBase(node: ts.CallExpression): { methodName: string, firstArg?: ts.Expression } | null {
22
+ function extractBuilderBase(node: ts.CallExpression): { methodName: string, firstArg?: ts.Expression, modifiers: Set<string> } | null {
23
+ const modifiers = new Set<string>();
21
24
  let current: ts.CallExpression = node;
22
25
  while (true) {
23
26
  const expr = current.expression;
@@ -25,13 +28,14 @@ function extractBuilderBase(node: ts.CallExpression): { methodName: string, firs
25
28
  return null;
26
29
  }
27
30
  if (ts.isCallExpression(expr.expression)) {
28
- // Chained modifier, e.g. .default() / .view() — walk deeper.
31
+ modifiers.add(expr.name.text);
29
32
  current = expr.expression;
30
33
  continue;
31
34
  }
32
35
  return {
33
36
  methodName: expr.name.text,
34
37
  firstArg: current.arguments[0],
38
+ modifiers,
35
39
  };
36
40
  }
37
41
  }
@@ -126,10 +130,27 @@ function defineProperty(property: Property, initializer: any) {
126
130
  if (ts.isCallExpression(initializer)) {
127
131
  const base = extractBuilderBase(initializer);
128
132
  if (base) {
133
+ // same as `@deprecated()`: `.deprecated(false)` still marks the field
134
+ if (base.modifiers.has("deprecated")) {
135
+ property.deprecated = true;
136
+ }
129
137
  if (BUILDER_COLLECTION_KINDS.has(base.methodName)) {
130
138
  property.type = base.methodName;
131
139
  if (base.firstArg) {
132
- property.childType = (base.firstArg as any).text ?? base.firstArg.getText();
140
+ // see through `(x)`, `x as any`, `x satisfies T`
141
+ let childArg: ts.Expression = base.firstArg;
142
+ while (ts.isParenthesizedExpression(childArg) || ts.isAsExpression(childArg) || ts.isSatisfiesExpression(childArg)) {
143
+ childArg = childArg.expression;
144
+ }
145
+ if (ts.isCallExpression(childArg)) {
146
+ // mirrors the runtime guard in builder.ts resolveChild()
147
+ const inner = extractBuilderBase(childArg);
148
+ const hint = (inner && !BUILDER_COLLECTION_KINDS.has(inner.methodName) && inner.methodName !== "ref" && inner.methodName !== "quantized")
149
+ ? `use the type name instead: t.${base.methodName}("${inner.methodName}")`
150
+ : `collections accept a Schema class or a primitive type name ("string", "number", …)`;
151
+ throw new Error(`schema-codegen: field '${property.name}': a t.* builder is not a valid element type — ${hint}.`);
152
+ }
153
+ property.childType = (childArg as any).text ?? childArg.getText();
133
154
  }
134
155
  } else if (base.methodName === "ref") {
135
156
  property.type = "ref";
@@ -169,15 +190,36 @@ function defineProperty(property: Property, initializer: any) {
169
190
  }
170
191
  }
171
192
 
193
+ function followModuleSpecifier(
194
+ specifier: ts.Expression | undefined,
195
+ currentFile: string,
196
+ decoratorName: string,
197
+ ) {
198
+ const moduleName: string | undefined = (specifier as ts.StringLiteral)?.text;
199
+ if (!moduleName) { return; } // `export { x }` — no module to follow
200
+
201
+ const resolved = (moduleName.startsWith("."))
202
+ ? resolveSourceFile(path.resolve(path.dirname(currentFile), moduleName))
203
+ // may be a tsconfig `paths`/`baseUrl` alias onto first-party source;
204
+ // npm packages are filtered out by the resolver
205
+ : resolveNonRelativeImport(moduleName, currentFile);
206
+
207
+ if (resolved && !isOwnPackageSource(resolved)) {
208
+ parseFiles([resolved], decoratorName, globalContext);
209
+ }
210
+ }
211
+
172
212
  function inspectNode(node: ts.Node, context: Context, decoratorName: string) {
173
213
  switch (node.kind) {
174
- case ts.SyntaxKind.ImportClause:
175
- const specifier = (node.parent as any).moduleSpecifier;
176
- if (specifier && (specifier.text as string).startsWith('.')) {
177
- const currentDir = path.dirname(node.getSourceFile().fileName);
178
- const pathToImport = path.resolve(currentDir, specifier.text);
179
- parseFiles([pathToImport], decoratorName, globalContext);
180
- }
214
+ case ts.SyntaxKind.ImportDeclaration:
215
+ case ts.SyntaxKind.ExportDeclaration:
216
+ // ExportDeclaration too: path aliases usually point at a barrel
217
+ // (`@schemas` -> `schemas/index.ts` -> `export * from "./Player"`).
218
+ followModuleSpecifier(
219
+ (node as ts.ImportDeclaration | ts.ExportDeclaration).moduleSpecifier,
220
+ node.getSourceFile().fileName,
221
+ decoratorName,
222
+ );
181
223
  break;
182
224
 
183
225
  case ts.SyntaxKind.ClassDeclaration:
@@ -478,7 +520,10 @@ function inspectNode(node: ts.Node, context: Context, decoratorName: string) {
478
520
  if (prop.kind === ts.SyntaxKind.MethodDeclaration) continue;
479
521
  if (!prop.initializer) continue;
480
522
 
481
- const property = currentProperty || new Property();
523
+ // never inherit `currentProperty`: it's the decorator path's
524
+ // carry-over from a visited `deprecated` identifier, and a
525
+ // trailing `.deprecated()` chain can leave it set
526
+ const property = new Property();
482
527
  property.name = prop.name.escapedText;
483
528
 
484
529
  currentStructure.addProperty(property);
@@ -508,10 +553,15 @@ function inspectNode(node: ts.Node, context: Context, decoratorName: string) {
508
553
 
509
554
  let parsedFiles: { [filename: string]: boolean };
510
555
 
556
+ /**
557
+ * `options` is only honored for a top-level call (one passing a fresh
558
+ * `Context`) — the recursive import walk reuses the run's resolver state.
559
+ */
511
560
  export function parseFiles(
512
561
  fileNames: string[],
513
562
  decoratorName: string = "type",
514
- context: Context = new Context()
563
+ context: Context = new Context(),
564
+ options?: ResolveOptions,
515
565
  ) {
516
566
  if (typeof ts.createSourceFile !== "function") {
517
567
  // typescript@7+ (native) no longer ships the JS compiler API
@@ -527,30 +577,18 @@ export function parseFiles(
527
577
  if (globalContext !== context) {
528
578
  parsedFiles = {};
529
579
  globalContext = context;
580
+ // a structure left over from a previous run would make the
581
+ // `currentStructure?.name !== className` guard skip re-registering it
582
+ currentStructure = undefined;
583
+ currentProperty = undefined;
584
+ resetResolver(options);
530
585
  }
531
586
 
532
587
  fileNames.forEach((fileName) => {
533
588
  let sourceFile: ts.Node;
534
589
  let sourceFileName: string;
535
590
 
536
- const fileNameAlternatives = [];
537
-
538
- if (
539
- !fileName.endsWith(".ts") &&
540
- !fileName.endsWith(".js") &&
541
- !fileName.endsWith(".mjs")
542
- ) {
543
- fileNameAlternatives.push(`${fileName}.ts`);
544
- fileNameAlternatives.push(`${fileName}/index.ts`);
545
-
546
- } else if (fileName.endsWith(".js")) {
547
- // Handle .js extensions by also trying .ts (ESM imports often use .js extension)
548
- fileNameAlternatives.push(fileName);
549
- fileNameAlternatives.push(fileName.replace(/\.js$/, ".ts"));
550
-
551
- } else {
552
- fileNameAlternatives.push(fileName);
553
- }
591
+ const fileNameAlternatives = sourceFileCandidates(fileName);
554
592
 
555
593
  for (let i = 0; i < fileNameAlternatives.length; i++) {
556
594
  try {