@colyseus/schema 5.0.14 → 5.0.20

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 (54) hide show
  1. package/README.md +11 -5
  2. package/build/Metadata.d.ts +10 -1
  3. package/build/annotations.d.ts +6 -5
  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/decoder/strategy/Callbacks.d.ts +7 -8
  11. package/build/decoder/strategy/getDecoderStateCallbacks.d.ts +2 -2
  12. package/build/encoder/ChangeTree.d.ts +40 -12
  13. package/build/encoder/Encoder.d.ts +1 -1
  14. package/build/encoder/Root.d.ts +9 -0
  15. package/build/encoder/StateView.d.ts +38 -1
  16. package/build/encoder/changeTree/inheritedFlags.d.ts +13 -19
  17. package/build/encoder/changeTree/liveIteration.d.ts +8 -0
  18. package/build/encoder/changeTree/parentChain.d.ts +30 -8
  19. package/build/encoder/streaming.d.ts +1 -1
  20. package/build/index.cjs +3232 -2830
  21. package/build/index.cjs.map +1 -1
  22. package/build/index.js +3228 -2826
  23. package/build/index.mjs +3232 -2830
  24. package/build/index.mjs.map +1 -1
  25. package/build/types/HelperTypes.d.ts +24 -14
  26. package/build/types/TypeContext.d.ts +0 -17
  27. package/build/types/builder.d.ts +1 -5
  28. package/build/types/symbols.d.ts +1 -0
  29. package/package.json +9 -8
  30. package/src/Metadata.ts +59 -77
  31. package/src/Reflection.ts +9 -5
  32. package/src/annotations.ts +28 -18
  33. package/src/codegen/api.ts +3 -1
  34. package/src/codegen/cli.ts +5 -2
  35. package/src/codegen/parser.ts +69 -31
  36. package/src/codegen/resolve.ts +322 -0
  37. package/src/codegen/types.ts +4 -1
  38. package/src/decoder/DecodeOperation.ts +13 -2
  39. package/src/decoder/strategy/Callbacks.ts +7 -8
  40. package/src/decoder/strategy/getDecoderStateCallbacks.ts +2 -2
  41. package/src/encoder/ChangeTree.ts +76 -25
  42. package/src/encoder/EncodeOperation.ts +10 -1
  43. package/src/encoder/Encoder.ts +52 -2
  44. package/src/encoder/Root.ts +28 -8
  45. package/src/encoder/StateView.ts +150 -66
  46. package/src/encoder/changeTree/inheritedFlags.ts +164 -45
  47. package/src/encoder/changeTree/liveIteration.ts +24 -3
  48. package/src/encoder/changeTree/parentChain.ts +72 -15
  49. package/src/encoder/streaming.ts +2 -1
  50. package/src/types/HelperTypes.ts +43 -34
  51. package/src/types/TypeContext.ts +5 -52
  52. package/src/types/builder.ts +14 -10
  53. package/src/types/custom/ArraySchema.ts +57 -14
  54. package/src/types/symbols.ts +3 -0
@@ -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 {
@@ -0,0 +1,322 @@
1
+ import * as ts from "typescript";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+
5
+ import { PACKAGE_ROOT } from "./types.js";
6
+
7
+ export interface ResolveOptions {
8
+ /** Explicit `--tsconfig`. When set, nearest-config discovery is skipped. */
9
+ tsconfig?: string;
10
+ }
11
+
12
+ interface ResolvedConfig {
13
+ configFilePath: string;
14
+ options: ts.CompilerOptions;
15
+ cache?: ts.ModuleResolutionCache;
16
+ }
17
+
18
+ interface MatchedPattern {
19
+ substitutions: string[];
20
+ /** What `*` captured, so substitutions can splice it back in. */
21
+ matchedStar: string;
22
+ }
23
+
24
+ const CONFIG_NAMES = ["tsconfig.json", "jsconfig.json"];
25
+
26
+ /** `No inputs were found in config file` — expected, since readDirectory is stubbed. */
27
+ const NO_INPUTS_FOUND = 18003;
28
+
29
+ let configByDir: Map<string, ResolvedConfig | null>;
30
+ let override: ResolvedConfig | null | undefined;
31
+ let resolveOptions: ResolveOptions;
32
+ let warned: Set<string>;
33
+
34
+ reset();
35
+
36
+ function reset() {
37
+ configByDir = new Map();
38
+ override = undefined;
39
+ resolveOptions = {};
40
+ warned = new Set();
41
+ }
42
+
43
+ /**
44
+ * Drop every cached tsconfig lookup. Called once per top-level `parseFiles()`
45
+ * run so a long-lived process can generate for two different projects.
46
+ */
47
+ export function resetResolver(options: ResolveOptions = {}) {
48
+ reset();
49
+ resolveOptions = options;
50
+
51
+ if (options.tsconfig && !fs.existsSync(options.tsconfig)) {
52
+ throw new Error(`--tsconfig: file not found: ${options.tsconfig}`);
53
+ }
54
+ }
55
+
56
+ function warnOnce(key: string, message: string) {
57
+ if (warned.has(key)) { return; }
58
+ warned.add(key);
59
+ console.warn(message);
60
+ }
61
+
62
+ /**
63
+ * `readDirectory` is stubbed on purpose: only `compilerOptions` is wanted here,
64
+ * and letting TypeScript glob the config's `include` set would stat the user's
65
+ * whole project on every config discovered.
66
+ */
67
+ const parseConfigHost: ts.ParseConfigHost = {
68
+ useCaseSensitiveFileNames: ts.sys?.useCaseSensitiveFileNames ?? true,
69
+ readDirectory: () => [],
70
+ fileExists: (fileName) => fs.existsSync(fileName),
71
+ readFile: (fileName) => {
72
+ try {
73
+ return fs.readFileSync(fileName, "utf8");
74
+ } catch (e) {
75
+ if (!(e as any)?.code) { throw e; }
76
+ return undefined;
77
+ }
78
+ },
79
+ };
80
+
81
+ function loadConfig(configFilePath: string): ResolvedConfig | null {
82
+ const { config, error } = ts.readConfigFile(configFilePath, parseConfigHost.readFile);
83
+ if (error) {
84
+ warnOnce(configFilePath,
85
+ `schema-codegen: could not read "${configFilePath}" ` +
86
+ `(${ts.flattenDiagnosticMessageText(error.messageText, " ")}) — ` +
87
+ `its import path aliases will be ignored.`);
88
+ return null;
89
+ }
90
+
91
+ // parseJsonConfigFileContent (not convertCompilerOptionsFromJson) is what
92
+ // applies `extends` chains, `${configDir}` templates, and `pathsBasePath` —
93
+ // the directory of the config that DECLARED `paths`, which in a monorepo is
94
+ // not the directory of the config being loaded.
95
+ const parsed = ts.parseJsonConfigFileContent(
96
+ config,
97
+ parseConfigHost,
98
+ path.dirname(configFilePath),
99
+ undefined,
100
+ configFilePath,
101
+ );
102
+
103
+ const errors = parsed.errors.filter((d) =>
104
+ d.code !== NO_INPUTS_FOUND && d.category === ts.DiagnosticCategory.Error);
105
+
106
+ if (errors.length > 0) {
107
+ warnOnce(configFilePath,
108
+ `schema-codegen: "${configFilePath}" has errors — ` +
109
+ errors.map((d) => ts.flattenDiagnosticMessageText(d.messageText, " ")).join("; "));
110
+ }
111
+
112
+ const options = parsed.options;
113
+ if (!options.paths && !options.baseUrl) { return null; }
114
+
115
+ const getCanonicalFileName = parseConfigHost.useCaseSensitiveFileNames
116
+ ? (f: string) => f
117
+ : (f: string) => f.toLowerCase();
118
+
119
+ return {
120
+ configFilePath,
121
+ options,
122
+ cache: ts.createModuleResolutionCache(
123
+ path.dirname(configFilePath), getCanonicalFileName, options),
124
+ };
125
+ }
126
+
127
+ function getOverrideConfig(): ResolvedConfig | null {
128
+ if (override === undefined) {
129
+ override = loadConfig(path.resolve(resolveOptions.tsconfig));
130
+ if (override === null) {
131
+ warnOnce(`no-aliases:${resolveOptions.tsconfig}`,
132
+ `schema-codegen: "${resolveOptions.tsconfig}" declares no "paths" or ` +
133
+ `"baseUrl" — there are no import aliases to resolve.`);
134
+ }
135
+ }
136
+ return override;
137
+ }
138
+
139
+ /**
140
+ * Nearest `tsconfig.json`/`jsconfig.json` above `containingFile`. Both names are
141
+ * checked at every level: a distant tsconfig.json must not win over an adjacent
142
+ * jsconfig.json. Stops at the first config found even when it declares no
143
+ * aliases — matching `tsc`, a parent project's `paths` do not leak into a child
144
+ * that does not `extends` it.
145
+ */
146
+ function getConfigFor(containingFile: string): ResolvedConfig | null {
147
+ if (resolveOptions.tsconfig) { return getOverrideConfig(); }
148
+
149
+ const dir = path.dirname(containingFile);
150
+ if (configByDir.has(dir)) { return configByDir.get(dir); }
151
+
152
+ let config: ResolvedConfig | null = null;
153
+ const visited: string[] = [];
154
+
155
+ for (let current = dir, parent: string; ; current = parent) {
156
+ visited.push(current);
157
+
158
+ const found = CONFIG_NAMES
159
+ .map((name) => path.join(current, name))
160
+ .find((candidate) => fs.existsSync(candidate));
161
+
162
+ if (found) {
163
+ config = loadConfig(found);
164
+ break;
165
+ }
166
+
167
+ parent = path.dirname(current);
168
+ if (parent === current) { break; }
169
+ }
170
+
171
+ // memoize the whole walk, negatives included
172
+ visited.forEach((visitedDir) => configByDir.set(visitedDir, config));
173
+
174
+ return config;
175
+ }
176
+
177
+ /** Exact patterns win outright; among wildcards the longest prefix wins. */
178
+ function findBestPathPattern(specifier: string, paths: ts.MapLike<string[]>): MatchedPattern | undefined {
179
+ let best: MatchedPattern | undefined;
180
+ let bestPrefixLength = -1;
181
+
182
+ for (const pattern in paths) {
183
+ const star = pattern.indexOf("*");
184
+
185
+ if (star === -1) {
186
+ if (pattern === specifier) {
187
+ return { substitutions: paths[pattern], matchedStar: "" };
188
+ }
189
+ continue;
190
+ }
191
+
192
+ const prefix = pattern.slice(0, star);
193
+ const suffix = pattern.slice(star + 1);
194
+
195
+ if (
196
+ specifier.length >= prefix.length + suffix.length &&
197
+ specifier.startsWith(prefix) &&
198
+ specifier.endsWith(suffix) &&
199
+ prefix.length > bestPrefixLength
200
+ ) {
201
+ bestPrefixLength = prefix.length;
202
+ best = {
203
+ substitutions: paths[pattern],
204
+ matchedStar: specifier.slice(prefix.length, specifier.length - suffix.length),
205
+ };
206
+ }
207
+ }
208
+
209
+ return best;
210
+ }
211
+
212
+ function resolveViaPathsSubstitution(matched: MatchedPattern, options: ts.CompilerOptions): string | undefined {
213
+ // mirrors ts.getPathsBasePath(): `paths` may be declared without a baseUrl,
214
+ // in which case it anchors on the config that declared it
215
+ const base = options.baseUrl ?? (options as any).pathsBasePath ?? process.cwd();
216
+
217
+ for (const substitution of matched.substitutions) {
218
+ const resolved = resolveSourceFile(
219
+ path.resolve(base, substitution.replace("*", matched.matchedStar)));
220
+ if (resolved) { return resolved; }
221
+ }
222
+
223
+ return undefined;
224
+ }
225
+
226
+ const isDeclaration = (fileName: string) => /\.d\.[cm]?ts$/.test(fileName);
227
+ const isInNodeModules = (fileName: string) => fileName.replace(/\\/g, "/").includes("/node_modules/");
228
+
229
+ /**
230
+ * Resolve a non-relative import (`@schemas/Player`, `shared/Player`) to a
231
+ * first-party source file through the tsconfig governing `containingFile`.
232
+ * Returns undefined for npm packages, declaration files, and specifiers no
233
+ * alias covers.
234
+ */
235
+ export function resolveNonRelativeImport(specifier: string, containingFile: string): string | undefined {
236
+ const config = getConfigFor(containingFile);
237
+ if (!config) { return undefined; }
238
+
239
+ const { options } = config;
240
+ const matched = options.paths && findBestPathPattern(specifier, options.paths);
241
+
242
+ // no alias hit and no baseUrl: TypeScript could only find this under
243
+ // node_modules, which costs ~130 failed lookups to prove
244
+ if (!matched && !options.baseUrl) { return undefined; }
245
+
246
+ const resolved = ts.resolveModuleName(
247
+ specifier, containingFile, options, ts.sys, config.cache).resolvedModule;
248
+
249
+ if (resolved) {
250
+ // a deliberate package/typings hit — not ours to parse, and the
251
+ // substitution fallback must not second-guess it
252
+ return (
253
+ resolved.isExternalLibraryImport ||
254
+ isDeclaration(resolved.resolvedFileName) ||
255
+ isInNodeModules(resolved.resolvedFileName)
256
+ ) ? undefined
257
+ : path.resolve(resolved.resolvedFileName);
258
+ }
259
+
260
+ // `.mjs` targets are unresolvable by ts.resolveModuleName in every
261
+ // moduleResolution mode, but schema-codegen parses them
262
+ const viaSubstitution = matched && resolveViaPathsSubstitution(matched, options);
263
+ if (viaSubstitution) { return viaSubstitution; }
264
+
265
+ if (matched) {
266
+ warnOnce(`unresolved:${specifier}`,
267
+ `schema-codegen: '${specifier}' matches a "paths" alias in ` +
268
+ `${config.configFilePath}, but no source file was found for it — ` +
269
+ `schemas it exports will be missing from the generated output.`);
270
+ }
271
+
272
+ return undefined;
273
+ }
274
+
275
+ /** The extension alternatives parseFiles() probes, in order. Pure — no fs. */
276
+ export function sourceFileCandidates(fileName: string): string[] {
277
+ if (
278
+ !fileName.endsWith(".ts") &&
279
+ !fileName.endsWith(".js") &&
280
+ !fileName.endsWith(".mjs")
281
+ ) {
282
+ return [`${fileName}.ts`, `${fileName}/index.ts`];
283
+
284
+ } else if (fileName.endsWith(".js")) {
285
+ // ESM imports often spell a .ts source with a .js extension
286
+ return [fileName, fileName.replace(/\.js$/, ".ts")];
287
+
288
+ } else {
289
+ return [fileName];
290
+ }
291
+ }
292
+
293
+ /** Same probing as parseFiles(), answering "which candidate exists?". */
294
+ export function resolveSourceFile(fileName: string): string | undefined {
295
+ const candidates = sourceFileCandidates(fileName);
296
+
297
+ for (let i = 0; i < candidates.length; i++) {
298
+ const candidate = path.resolve(candidates[i]);
299
+ try {
300
+ // statSync, not existsSync: a directory must fall through to the
301
+ // next candidate, the way readFileSync's EISDIR does
302
+ if (fs.statSync(candidate).isFile()) { return candidate; }
303
+ } catch (e) {
304
+ if (!(e as any)?.code) { throw e; }
305
+ }
306
+ }
307
+
308
+ return undefined;
309
+ }
310
+
311
+ /**
312
+ * The serializer's own source declares wire-internal schemas (`Reflection`,
313
+ * `ReflectionField`, …) that must never reach generated client code.
314
+ */
315
+ export function isOwnPackageSource(fileName: string): boolean {
316
+ const relative = path.relative(PACKAGE_ROOT, fileName);
317
+ return (
318
+ !relative.startsWith("..") &&
319
+ !path.isAbsolute(relative) &&
320
+ (relative.startsWith(`src${path.sep}`) || relative.startsWith(`build${path.sep}`))
321
+ );
322
+ }
@@ -5,7 +5,10 @@ if (typeof(__dirname) === "undefined") {
5
5
  global.__dirname = path.dirname(new URL(import.meta.url).pathname);
6
6
  }
7
7
 
8
- const VERSION = JSON.parse(fs.readFileSync(__dirname + "/../../package.json").toString()).version;
8
+ /** Root of the @colyseus/schema package — `src/codegen/` in dev, `build/codegen/` once bundled. */
9
+ export const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
10
+
11
+ const VERSION = JSON.parse(fs.readFileSync(path.resolve(PACKAGE_ROOT, "package.json")).toString()).version;
9
12
  const COMMENT_HEADER = `
10
13
  THIS FILE HAS BEEN GENERATED AUTOMATICALLY
11
14
  DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING
@@ -240,7 +240,12 @@ export const decodeSchemaOperation: DecodeOperation = function <T extends Schema
240
240
  return DEFINITION_MISMATCH;
241
241
  }
242
242
 
243
- const previousValue = ref[$getByIndex](index);
243
+ // a peer that still carries a @deprecated() field keeps sending it — the
244
+ // bytes must be consumed or the stream desyncs, but the local accessor
245
+ // may throw: read nothing, write nothing, report nothing.
246
+ const isDeprecated = field.deprecated === true;
247
+
248
+ const previousValue = isDeprecated ? undefined : ref[$getByIndex](index);
244
249
  const value = decodeValue(
245
250
  decoder,
246
251
  operation,
@@ -253,6 +258,8 @@ export const decodeSchemaOperation: DecodeOperation = function <T extends Schema
253
258
  allChanges,
254
259
  );
255
260
 
261
+ if (isDeprecated) { return; }
262
+
256
263
  if (value !== null && value !== undefined) {
257
264
  // Write via the generated setter. Bypass to `(ref as any)[$values][index]`
258
265
  // was attempted but only works for @type-decorated classes (which
@@ -430,7 +437,11 @@ export const decodeArray: DecodeOperation = function (
430
437
  return;
431
438
 
432
439
  } else if (operation === OPERATION.REVERSE) {
433
- tgt.reverse();
440
+ // Positional reverse of the decoder's authoritative storage. Don't
441
+ // call `tgt.reverse()` — that's the encoder-side method, and its
442
+ // dirty-tick check would misread the stale recorder a `clone(true)`
443
+ // instance carries.
444
+ tgt.items.reverse();
434
445
  return;
435
446
 
436
447
  } else if (operation === OPERATION.DELETE_BY_REFID) {
@@ -1,5 +1,5 @@
1
1
  import { Metadata } from "../../Metadata.js";
2
- import { Collection, NonFunctionPropNames } from "../../types/HelperTypes.js";
2
+ import { Collection, NonFunctionPropNames, CollectionLike } from "../../types/HelperTypes.js";
3
3
  import type { IRef, Ref } from "../../encoder/ChangeTree.js";
4
4
  import { Decoder } from "../Decoder.js";
5
5
  import { DataChange } from "../DecodeOperation.js";
@@ -25,25 +25,24 @@ type KeyValueCallback<K, V> = (key: K, value: V) => void;
25
25
  type ValueKeyCallback<V, K> = (value: V, key: K) => void;
26
26
  type InstanceChangeCallback = () => void;
27
27
 
28
- // Exclude internal properties from valid property names
29
- type PublicPropNames<T> = Exclude<NonFunctionPropNames<T>, typeof $refId> & string;
28
+ type PublicPropNames<T> = NonFunctionPropNames<T> & string;
30
29
 
31
30
  // Extract only properties that extend Collection
32
- type CollectionPropNames<T> = Exclude<{
33
- [K in keyof T]: T[K] extends Collection<any, any> ? K : never
34
- }[keyof T] & string, typeof $refId>;
31
+ type CollectionPropNames<T> = {
32
+ [K in keyof T]: T[K] extends CollectionLike<any, any> ? K : never
33
+ }[keyof T] & string;
35
34
 
36
35
  // Infer the value type of a collection property
37
36
  type CollectionValueType<T, K extends keyof T> =
38
37
  T[K] extends MapSchema<infer V, any> ? V :
39
38
  T[K] extends ArraySchema<infer V> ? V :
40
- T[K] extends Collection<any, infer V, any> ? V : never;
39
+ T[K] extends CollectionLike<any, infer V, any> ? V : never;
41
40
 
42
41
  // Infer the key type of a collection property
43
42
  type CollectionKeyType<T, K extends keyof T> =
44
43
  T[K] extends MapSchema<any, infer Key> ? Key :
45
44
  T[K] extends ArraySchema<any> ? number :
46
- T[K] extends Collection<infer Key, any, any> ? Key : never;
45
+ T[K] extends CollectionLike<infer Key, any, any> ? Key : never;
47
46
 
48
47
  export class StateCallbackStrategy<TState extends IRef> {
49
48
  protected decoder: Decoder<TState>;
@@ -1,5 +1,5 @@
1
1
  import { Metadata } from "../../Metadata.js";
2
- import { Collection, NonFunctionNonPrimitivePropNames, NonFunctionPropNames } from "../../types/HelperTypes.js";
2
+ import { Collection, NonFunctionNonPrimitivePropNames, NonFunctionPropNames, CollectionLike } from "../../types/HelperTypes.js";
3
3
  import { IRef, Ref } from "../../encoder/ChangeTree.js";
4
4
  import { Decoder } from "../Decoder.js";
5
5
  import { DataChange } from "../DecodeOperation.js";
@@ -30,7 +30,7 @@ export type GetCallbackProxy = SchemaCallbackProxy<any>; // workaround for compa
30
30
 
31
31
  export type CallbackProxy<T> = unknown extends T // is "any"?
32
32
  ? SchemaCallback<T> & CollectionCallback<any, any>
33
- : T extends Collection<infer K, infer V, infer _>
33
+ : T extends CollectionLike<infer K, infer V, infer _>
34
34
  ? CollectionCallback<K, V>
35
35
  : SchemaCallback<T>;
36
36