@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.
- package/README.md +7 -2
- package/build/Metadata.d.ts +10 -1
- package/build/Reflection.d.ts +36 -34
- package/build/codegen/api.d.ts +2 -0
- package/build/codegen/cli.cjs +322 -31
- package/build/codegen/cli.cjs.map +1 -1
- package/build/codegen/parser.d.ts +6 -1
- package/build/codegen/resolve.d.ts +25 -0
- package/build/codegen/types.d.ts +2 -0
- package/build/encoder/ChangeTree.d.ts +40 -12
- package/build/encoder/Encoder.d.ts +1 -1
- package/build/encoder/Root.d.ts +9 -0
- package/build/encoder/StateView.d.ts +38 -1
- package/build/encoder/changeTree/inheritedFlags.d.ts +13 -19
- package/build/encoder/changeTree/liveIteration.d.ts +8 -0
- package/build/encoder/changeTree/parentChain.d.ts +30 -8
- package/build/encoder/streaming.d.ts +1 -1
- package/build/index.cjs +3237 -2834
- package/build/index.cjs.map +1 -1
- package/build/index.js +3233 -2830
- package/build/index.mjs +3237 -2834
- package/build/index.mjs.map +1 -1
- package/build/types/HelperTypes.d.ts +3 -5
- package/build/types/TypeContext.d.ts +0 -17
- package/build/types/builder.d.ts +8 -11
- package/build/types/symbols.d.ts +1 -0
- package/package.json +2 -2
- package/src/Metadata.ts +59 -77
- package/src/Reflection.ts +9 -5
- package/src/annotations.ts +19 -13
- package/src/codegen/api.ts +3 -1
- package/src/codegen/cli.ts +5 -2
- package/src/codegen/parser.ts +69 -31
- package/src/codegen/resolve.ts +322 -0
- package/src/codegen/types.ts +4 -1
- package/src/decoder/DecodeOperation.ts +13 -2
- package/src/encoder/ChangeTree.ts +76 -25
- package/src/encoder/EncodeOperation.ts +10 -1
- package/src/encoder/Encoder.ts +52 -2
- package/src/encoder/Root.ts +28 -8
- package/src/encoder/StateView.ts +150 -66
- package/src/encoder/changeTree/inheritedFlags.ts +164 -45
- package/src/encoder/changeTree/liveIteration.ts +24 -3
- package/src/encoder/changeTree/parentChain.ts +72 -15
- package/src/encoder/streaming.ts +2 -1
- package/src/types/HelperTypes.ts +16 -13
- package/src/types/TypeContext.ts +5 -52
- package/src/types/builder.ts +21 -16
- package/src/types/custom/ArraySchema.ts +57 -14
- 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
|
|
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
|
}
|
package/build/types/builder.d.ts
CHANGED
|
@@ -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
|
|
49
|
-
* `undefined extends V`)
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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
|
|
203
|
-
*
|
|
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
|
|
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>;
|
package/build/types/symbols.d.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
package/src/annotations.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/src/codegen/api.ts
CHANGED
|
@@ -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());
|
package/src/codegen/cli.ts
CHANGED
|
@@ -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) {
|
package/src/codegen/parser.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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 {
|