@colyseus/schema 5.0.14 → 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 (47) hide show
  1. package/README.md +7 -2
  2. package/build/Metadata.d.ts +10 -1
  3. package/build/codegen/api.d.ts +2 -0
  4. package/build/codegen/cli.cjs +322 -31
  5. package/build/codegen/cli.cjs.map +1 -1
  6. package/build/codegen/parser.d.ts +6 -1
  7. package/build/codegen/resolve.d.ts +25 -0
  8. package/build/codegen/types.d.ts +2 -0
  9. package/build/encoder/ChangeTree.d.ts +40 -12
  10. package/build/encoder/Encoder.d.ts +1 -1
  11. package/build/encoder/Root.d.ts +9 -0
  12. package/build/encoder/StateView.d.ts +38 -1
  13. package/build/encoder/changeTree/inheritedFlags.d.ts +13 -19
  14. package/build/encoder/changeTree/liveIteration.d.ts +8 -0
  15. package/build/encoder/changeTree/parentChain.d.ts +30 -8
  16. package/build/encoder/streaming.d.ts +1 -1
  17. package/build/index.cjs +3232 -2830
  18. package/build/index.cjs.map +1 -1
  19. package/build/index.js +3228 -2826
  20. package/build/index.mjs +3232 -2830
  21. package/build/index.mjs.map +1 -1
  22. package/build/types/TypeContext.d.ts +0 -17
  23. package/build/types/builder.d.ts +1 -5
  24. package/build/types/symbols.d.ts +1 -0
  25. package/package.json +1 -1
  26. package/src/Metadata.ts +59 -77
  27. package/src/Reflection.ts +9 -5
  28. package/src/annotations.ts +19 -13
  29. package/src/codegen/api.ts +3 -1
  30. package/src/codegen/cli.ts +5 -2
  31. package/src/codegen/parser.ts +69 -31
  32. package/src/codegen/resolve.ts +322 -0
  33. package/src/codegen/types.ts +4 -1
  34. package/src/decoder/DecodeOperation.ts +13 -2
  35. package/src/encoder/ChangeTree.ts +76 -25
  36. package/src/encoder/EncodeOperation.ts +10 -1
  37. package/src/encoder/Encoder.ts +52 -2
  38. package/src/encoder/Root.ts +28 -8
  39. package/src/encoder/StateView.ts +150 -66
  40. package/src/encoder/changeTree/inheritedFlags.ts +164 -45
  41. package/src/encoder/changeTree/liveIteration.ts +24 -3
  42. package/src/encoder/changeTree/parentChain.ts +72 -15
  43. package/src/encoder/streaming.ts +2 -1
  44. package/src/types/TypeContext.ts +5 -52
  45. package/src/types/builder.ts +14 -10
  46. package/src/types/custom/ArraySchema.ts +57 -14
  47. package/src/types/symbols.ts +3 -0
package/README.md CHANGED
@@ -102,7 +102,7 @@ The `@type()` decorator uses legacy decorators — enable them in your `tsconfig
102
102
 
103
103
  ### Declaration:
104
104
 
105
- Each primitive type is declared through its `t.*` factory (`t.string()`, `t.uint8()`, …). The string names above remain valid as collection child types (`t.array("string")`).
105
+ Each primitive type is declared through its `t.*` factory (`t.string()`, `t.uint8()`, …). Collection elements take the type **name**, not a builder: `t.array("string")`, never `t.array(t.string())`.
106
106
 
107
107
  #### Primitive types (`string`, `number`, `boolean`, etc)
108
108
 
@@ -293,7 +293,7 @@ up-to-date version of the schema definitions.
293
293
  ## Limitations and best practices
294
294
 
295
295
  - Each `Schema` structure can hold up to `64` fields. If you need more fields, use nested structures.
296
- - Fields tagged with `.view()`, `.unreliable()`, or `.static()` at field indexes `≥ 32` use a slower per-mutation classification path (linear scan over the tagged-field list instead of a single bitwise op). For schemas with more than 32 fields, declare frequently-mutated tagged fields earlier so they fall in the bitmask fast path.
296
+ - Fields tagged with `.view()`, `.unreliable()`, or `.fullStateOnly()` at field indexes `≥ 32` use a slower per-mutation classification path (linear scan over the tagged-field list instead of a single bitwise op). For schemas with more than 32 fields, declare frequently-mutated tagged fields earlier so they fall in the bitmask fast path.
297
297
  - Schemas with `≤ 8` fields store per-field operation bytes inline in two numbers (no allocation per instance). Schemas with `> 8` fields allocate a small `Uint8Array` per instance for op storage. The difference is only material when allocating thousands of instances per tick — prefer narrower nested structures in that regime.
298
298
  - `NaN` or `null` numbers are encoded as `0`
299
299
  - `null` strings are encoded as `""`
@@ -333,6 +333,11 @@ schema-codegen ./schemas/State.ts --output ./haxe-project/ --haxe
333
333
  | `--bundle` | Bundle all generated files into a single file |
334
334
  | `--namespace` | Generate namespace/package on output code |
335
335
  | `--decorator` | Custom name for `@type` decorator to scan for |
336
+ | `--tsconfig` | `tsconfig.json` to resolve import path aliases with (default: the nearest `tsconfig.json`/`jsconfig.json` above each source file) |
337
+
338
+ Imports are followed to discover related schemas, including bare specifiers
339
+ mapped by `compilerOptions.paths`/`baseUrl` and barrel files that re-export
340
+ them. Imports of installed packages are not followed.
336
341
 
337
342
  ### Bundle Mode
338
343
 
@@ -1,6 +1,6 @@
1
1
  import { DefinitionType } from "./annotations.js";
2
2
  import { TypeDefinition } from "./types/registry.js";
3
- import { $descriptors, $encoders, $fieldIndexesByViewTag, $numFields, $refTypeFieldIndexes, $fullStateOnlyFieldIndexes, $streamFieldIndexes, $streamPriorities, $patchOnlyFieldIndexes, $unreliableFieldIndexes, $viewFieldIndexes } from "./types/symbols.js";
3
+ import { $descriptors, $encoders, $fieldIndexesByViewTag, $numFields, $refTypeFieldIndexes, $fullStateOnlyFieldIndexes, $fullSyncSkipIndexes, $streamFieldIndexes, $streamPriorities, $patchOnlyFieldIndexes, $unreliableFieldIndexes, $viewFieldIndexes } from "./types/symbols.js";
4
4
  /**
5
5
  * Field indexes ride in the low 6 bits of the operation byte
6
6
  * (`(index | operation) & 255`), which leaves room for 0..63. Index 63 is
@@ -42,6 +42,9 @@ export type Metadata = {
42
42
  {
43
43
  [$patchOnlyFieldIndexes]: number[];
44
44
  } & // all field indexes tagged with @patchOnly (not persisted to snapshots)
45
+ {
46
+ [$fullSyncSkipIndexes]: number[];
47
+ } & // @patchOnly ∪ @deprecated() — never read during full sync
45
48
  {
46
49
  [$fullStateOnlyFieldIndexes]: number[];
47
50
  } & // all field indexes tagged @fullStateOnly / .fullStateOnly() (not tracked after assignment)
@@ -84,6 +87,12 @@ export declare const Metadata: {
84
87
  setTag(metadata: Metadata, fieldName: string, tag: number): void;
85
88
  setUnreliable(metadata: Metadata, fieldName: string): void;
86
89
  setPatchOnly(metadata: Metadata, fieldName: string): void;
90
+ /**
91
+ * `@deprecated()` bookkeeping: the field keeps its wire index (so peers
92
+ * that still carry it stay compatible) but is excluded from full sync —
93
+ * its accessor may throw — and hidden from `for..in` consumers.
94
+ */
95
+ setDeprecated(metadata: Metadata, fieldName: string): void;
87
96
  setFullStateOnly(metadata: Metadata, fieldName: string): void;
88
97
  setStream(metadata: Metadata, fieldName: string): void;
89
98
  /**
@@ -5,5 +5,7 @@ export interface GenerateOptions {
5
5
  decorator?: string;
6
6
  namespace?: string;
7
7
  bundle?: boolean;
8
+ /** Overrides the nearest-tsconfig lookup used to resolve import path aliases. */
9
+ tsconfig?: string;
8
10
  }
9
11
  export declare function generate(targetId: string, options: GenerateOptions): void;
@@ -69,7 +69,9 @@ var argv = (sargs) => {
69
69
  if (typeof (__dirname) === "undefined") {
70
70
  global.__dirname = path__namespace.dirname(new URL((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))).pathname);
71
71
  }
72
- const VERSION = JSON.parse(fs__namespace.readFileSync(__dirname + "/../../package.json").toString()).version;
72
+ /** Root of the @colyseus/schema package — `src/codegen/` in dev, `build/codegen/` once bundled. */
73
+ const PACKAGE_ROOT = path__namespace.resolve(__dirname, "..", "..");
74
+ const VERSION = JSON.parse(fs__namespace.readFileSync(path__namespace.resolve(PACKAGE_ROOT, "package.json")).toString()).version;
73
75
  const COMMENT_HEADER = `
74
76
  THIS FILE HAS BEEN GENERATED AUTOMATICALLY
75
77
  DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING
@@ -217,6 +219,262 @@ function getInheritanceTree(klass, allClasses, includeSelf = true) {
217
219
  ];
218
220
  }
219
221
 
222
+ const CONFIG_NAMES = ["tsconfig.json", "jsconfig.json"];
223
+ /** `No inputs were found in config file` — expected, since readDirectory is stubbed. */
224
+ const NO_INPUTS_FOUND = 18003;
225
+ let configByDir;
226
+ let override;
227
+ let resolveOptions;
228
+ let warned;
229
+ reset();
230
+ function reset() {
231
+ configByDir = new Map();
232
+ override = undefined;
233
+ resolveOptions = {};
234
+ warned = new Set();
235
+ }
236
+ /**
237
+ * Drop every cached tsconfig lookup. Called once per top-level `parseFiles()`
238
+ * run so a long-lived process can generate for two different projects.
239
+ */
240
+ function resetResolver(options = {}) {
241
+ reset();
242
+ resolveOptions = options;
243
+ if (options.tsconfig && !fs__namespace.existsSync(options.tsconfig)) {
244
+ throw new Error(`--tsconfig: file not found: ${options.tsconfig}`);
245
+ }
246
+ }
247
+ function warnOnce(key, message) {
248
+ if (warned.has(key)) {
249
+ return;
250
+ }
251
+ warned.add(key);
252
+ console.warn(message);
253
+ }
254
+ /**
255
+ * `readDirectory` is stubbed on purpose: only `compilerOptions` is wanted here,
256
+ * and letting TypeScript glob the config's `include` set would stat the user's
257
+ * whole project on every config discovered.
258
+ */
259
+ const parseConfigHost = {
260
+ useCaseSensitiveFileNames: ts__namespace.sys?.useCaseSensitiveFileNames ?? true,
261
+ readDirectory: () => [],
262
+ fileExists: (fileName) => fs__namespace.existsSync(fileName),
263
+ readFile: (fileName) => {
264
+ try {
265
+ return fs__namespace.readFileSync(fileName, "utf8");
266
+ }
267
+ catch (e) {
268
+ if (!e?.code) {
269
+ throw e;
270
+ }
271
+ return undefined;
272
+ }
273
+ },
274
+ };
275
+ function loadConfig(configFilePath) {
276
+ const { config, error } = ts__namespace.readConfigFile(configFilePath, parseConfigHost.readFile);
277
+ if (error) {
278
+ warnOnce(configFilePath, `schema-codegen: could not read "${configFilePath}" ` +
279
+ `(${ts__namespace.flattenDiagnosticMessageText(error.messageText, " ")}) — ` +
280
+ `its import path aliases will be ignored.`);
281
+ return null;
282
+ }
283
+ // parseJsonConfigFileContent (not convertCompilerOptionsFromJson) is what
284
+ // applies `extends` chains, `${configDir}` templates, and `pathsBasePath` —
285
+ // the directory of the config that DECLARED `paths`, which in a monorepo is
286
+ // not the directory of the config being loaded.
287
+ const parsed = ts__namespace.parseJsonConfigFileContent(config, parseConfigHost, path__namespace.dirname(configFilePath), undefined, configFilePath);
288
+ const errors = parsed.errors.filter((d) => d.code !== NO_INPUTS_FOUND && d.category === ts__namespace.DiagnosticCategory.Error);
289
+ if (errors.length > 0) {
290
+ warnOnce(configFilePath, `schema-codegen: "${configFilePath}" has errors — ` +
291
+ errors.map((d) => ts__namespace.flattenDiagnosticMessageText(d.messageText, " ")).join("; "));
292
+ }
293
+ const options = parsed.options;
294
+ if (!options.paths && !options.baseUrl) {
295
+ return null;
296
+ }
297
+ const getCanonicalFileName = parseConfigHost.useCaseSensitiveFileNames
298
+ ? (f) => f
299
+ : (f) => f.toLowerCase();
300
+ return {
301
+ configFilePath,
302
+ options,
303
+ cache: ts__namespace.createModuleResolutionCache(path__namespace.dirname(configFilePath), getCanonicalFileName, options),
304
+ };
305
+ }
306
+ function getOverrideConfig() {
307
+ if (override === undefined) {
308
+ override = loadConfig(path__namespace.resolve(resolveOptions.tsconfig));
309
+ if (override === null) {
310
+ warnOnce(`no-aliases:${resolveOptions.tsconfig}`, `schema-codegen: "${resolveOptions.tsconfig}" declares no "paths" or ` +
311
+ `"baseUrl" — there are no import aliases to resolve.`);
312
+ }
313
+ }
314
+ return override;
315
+ }
316
+ /**
317
+ * Nearest `tsconfig.json`/`jsconfig.json` above `containingFile`. Both names are
318
+ * checked at every level: a distant tsconfig.json must not win over an adjacent
319
+ * jsconfig.json. Stops at the first config found even when it declares no
320
+ * aliases — matching `tsc`, a parent project's `paths` do not leak into a child
321
+ * that does not `extends` it.
322
+ */
323
+ function getConfigFor(containingFile) {
324
+ if (resolveOptions.tsconfig) {
325
+ return getOverrideConfig();
326
+ }
327
+ const dir = path__namespace.dirname(containingFile);
328
+ if (configByDir.has(dir)) {
329
+ return configByDir.get(dir);
330
+ }
331
+ let config = null;
332
+ const visited = [];
333
+ for (let current = dir, parent;; current = parent) {
334
+ visited.push(current);
335
+ const found = CONFIG_NAMES
336
+ .map((name) => path__namespace.join(current, name))
337
+ .find((candidate) => fs__namespace.existsSync(candidate));
338
+ if (found) {
339
+ config = loadConfig(found);
340
+ break;
341
+ }
342
+ parent = path__namespace.dirname(current);
343
+ if (parent === current) {
344
+ break;
345
+ }
346
+ }
347
+ // memoize the whole walk, negatives included
348
+ visited.forEach((visitedDir) => configByDir.set(visitedDir, config));
349
+ return config;
350
+ }
351
+ /** Exact patterns win outright; among wildcards the longest prefix wins. */
352
+ function findBestPathPattern(specifier, paths) {
353
+ let best;
354
+ let bestPrefixLength = -1;
355
+ for (const pattern in paths) {
356
+ const star = pattern.indexOf("*");
357
+ if (star === -1) {
358
+ if (pattern === specifier) {
359
+ return { substitutions: paths[pattern], matchedStar: "" };
360
+ }
361
+ continue;
362
+ }
363
+ const prefix = pattern.slice(0, star);
364
+ const suffix = pattern.slice(star + 1);
365
+ if (specifier.length >= prefix.length + suffix.length &&
366
+ specifier.startsWith(prefix) &&
367
+ specifier.endsWith(suffix) &&
368
+ prefix.length > bestPrefixLength) {
369
+ bestPrefixLength = prefix.length;
370
+ best = {
371
+ substitutions: paths[pattern],
372
+ matchedStar: specifier.slice(prefix.length, specifier.length - suffix.length),
373
+ };
374
+ }
375
+ }
376
+ return best;
377
+ }
378
+ function resolveViaPathsSubstitution(matched, options) {
379
+ // mirrors ts.getPathsBasePath(): `paths` may be declared without a baseUrl,
380
+ // in which case it anchors on the config that declared it
381
+ const base = options.baseUrl ?? options.pathsBasePath ?? process.cwd();
382
+ for (const substitution of matched.substitutions) {
383
+ const resolved = resolveSourceFile(path__namespace.resolve(base, substitution.replace("*", matched.matchedStar)));
384
+ if (resolved) {
385
+ return resolved;
386
+ }
387
+ }
388
+ return undefined;
389
+ }
390
+ const isDeclaration = (fileName) => /\.d\.[cm]?ts$/.test(fileName);
391
+ const isInNodeModules = (fileName) => fileName.replace(/\\/g, "/").includes("/node_modules/");
392
+ /**
393
+ * Resolve a non-relative import (`@schemas/Player`, `shared/Player`) to a
394
+ * first-party source file through the tsconfig governing `containingFile`.
395
+ * Returns undefined for npm packages, declaration files, and specifiers no
396
+ * alias covers.
397
+ */
398
+ function resolveNonRelativeImport(specifier, containingFile) {
399
+ const config = getConfigFor(containingFile);
400
+ if (!config) {
401
+ return undefined;
402
+ }
403
+ const { options } = config;
404
+ const matched = options.paths && findBestPathPattern(specifier, options.paths);
405
+ // no alias hit and no baseUrl: TypeScript could only find this under
406
+ // node_modules, which costs ~130 failed lookups to prove
407
+ if (!matched && !options.baseUrl) {
408
+ return undefined;
409
+ }
410
+ const resolved = ts__namespace.resolveModuleName(specifier, containingFile, options, ts__namespace.sys, config.cache).resolvedModule;
411
+ if (resolved) {
412
+ // a deliberate package/typings hit — not ours to parse, and the
413
+ // substitution fallback must not second-guess it
414
+ return (resolved.isExternalLibraryImport ||
415
+ isDeclaration(resolved.resolvedFileName) ||
416
+ isInNodeModules(resolved.resolvedFileName)) ? undefined
417
+ : path__namespace.resolve(resolved.resolvedFileName);
418
+ }
419
+ // `.mjs` targets are unresolvable by ts.resolveModuleName in every
420
+ // moduleResolution mode, but schema-codegen parses them
421
+ const viaSubstitution = matched && resolveViaPathsSubstitution(matched, options);
422
+ if (viaSubstitution) {
423
+ return viaSubstitution;
424
+ }
425
+ if (matched) {
426
+ warnOnce(`unresolved:${specifier}`, `schema-codegen: '${specifier}' matches a "paths" alias in ` +
427
+ `${config.configFilePath}, but no source file was found for it — ` +
428
+ `schemas it exports will be missing from the generated output.`);
429
+ }
430
+ return undefined;
431
+ }
432
+ /** The extension alternatives parseFiles() probes, in order. Pure — no fs. */
433
+ function sourceFileCandidates(fileName) {
434
+ if (!fileName.endsWith(".ts") &&
435
+ !fileName.endsWith(".js") &&
436
+ !fileName.endsWith(".mjs")) {
437
+ return [`${fileName}.ts`, `${fileName}/index.ts`];
438
+ }
439
+ else if (fileName.endsWith(".js")) {
440
+ // ESM imports often spell a .ts source with a .js extension
441
+ return [fileName, fileName.replace(/\.js$/, ".ts")];
442
+ }
443
+ else {
444
+ return [fileName];
445
+ }
446
+ }
447
+ /** Same probing as parseFiles(), answering "which candidate exists?". */
448
+ function resolveSourceFile(fileName) {
449
+ const candidates = sourceFileCandidates(fileName);
450
+ for (let i = 0; i < candidates.length; i++) {
451
+ const candidate = path__namespace.resolve(candidates[i]);
452
+ try {
453
+ // statSync, not existsSync: a directory must fall through to the
454
+ // next candidate, the way readFileSync's EISDIR does
455
+ if (fs__namespace.statSync(candidate).isFile()) {
456
+ return candidate;
457
+ }
458
+ }
459
+ catch (e) {
460
+ if (!e?.code) {
461
+ throw e;
462
+ }
463
+ }
464
+ }
465
+ return undefined;
466
+ }
467
+ /**
468
+ * The serializer's own source declares wire-internal schemas (`Reflection`,
469
+ * `ReflectionField`, …) that must never reach generated client code.
470
+ */
471
+ function isOwnPackageSource(fileName) {
472
+ const relative = path__namespace.relative(PACKAGE_ROOT, fileName);
473
+ return (!relative.startsWith("..") &&
474
+ !path__namespace.isAbsolute(relative) &&
475
+ (relative.startsWith(`src${path__namespace.sep}`) || relative.startsWith(`build${path__namespace.sep}`)));
476
+ }
477
+
220
478
  let currentStructure;
221
479
  let currentProperty;
222
480
  let globalContext;
@@ -224,10 +482,12 @@ let defineTypesWarned = false;
224
482
  const BUILDER_COLLECTION_KINDS = new Set(["array", "map", "set", "collection"]);
225
483
  /**
226
484
  * For a t.*().chain().calls() expression, walk down to the base `t.X(...)`
227
- * call and return its method name and first argument. Returns null if the
485
+ * call and return its method name, first argument, and the names of the
486
+ * chained modifiers (`.view()`, `.deprecated()`, …). Returns null if the
228
487
  * node does not look like a builder chain.
229
488
  */
230
489
  function extractBuilderBase(node) {
490
+ const modifiers = new Set();
231
491
  let current = node;
232
492
  while (true) {
233
493
  const expr = current.expression;
@@ -235,13 +495,14 @@ function extractBuilderBase(node) {
235
495
  return null;
236
496
  }
237
497
  if (ts__namespace.isCallExpression(expr.expression)) {
238
- // Chained modifier, e.g. .default() / .view() — walk deeper.
498
+ modifiers.add(expr.name.text);
239
499
  current = expr.expression;
240
500
  continue;
241
501
  }
242
502
  return {
243
503
  methodName: expr.name.text,
244
504
  firstArg: current.arguments[0],
505
+ modifiers,
245
506
  };
246
507
  }
247
508
  }
@@ -340,10 +601,27 @@ function defineProperty(property, initializer) {
340
601
  if (ts__namespace.isCallExpression(initializer)) {
341
602
  const base = extractBuilderBase(initializer);
342
603
  if (base) {
604
+ // same as `@deprecated()`: `.deprecated(false)` still marks the field
605
+ if (base.modifiers.has("deprecated")) {
606
+ property.deprecated = true;
607
+ }
343
608
  if (BUILDER_COLLECTION_KINDS.has(base.methodName)) {
344
609
  property.type = base.methodName;
345
610
  if (base.firstArg) {
346
- property.childType = base.firstArg.text ?? base.firstArg.getText();
611
+ // see through `(x)`, `x as any`, `x satisfies T`
612
+ let childArg = base.firstArg;
613
+ while (ts__namespace.isParenthesizedExpression(childArg) || ts__namespace.isAsExpression(childArg) || ts__namespace.isSatisfiesExpression(childArg)) {
614
+ childArg = childArg.expression;
615
+ }
616
+ if (ts__namespace.isCallExpression(childArg)) {
617
+ // mirrors the runtime guard in builder.ts resolveChild()
618
+ const inner = extractBuilderBase(childArg);
619
+ const hint = (inner && !BUILDER_COLLECTION_KINDS.has(inner.methodName) && inner.methodName !== "ref" && inner.methodName !== "quantized")
620
+ ? `use the type name instead: t.${base.methodName}("${inner.methodName}")`
621
+ : `collections accept a Schema class or a primitive type name ("string", "number", …)`;
622
+ throw new Error(`schema-codegen: field '${property.name}': a t.* builder is not a valid element type — ${hint}.`);
623
+ }
624
+ property.childType = childArg.text ?? childArg.getText();
347
625
  }
348
626
  }
349
627
  else if (base.methodName === "ref") {
@@ -385,15 +663,27 @@ function defineProperty(property, initializer) {
385
663
  property.type = initializer.text;
386
664
  }
387
665
  }
666
+ function followModuleSpecifier(specifier, currentFile, decoratorName) {
667
+ const moduleName = specifier?.text;
668
+ if (!moduleName) {
669
+ return;
670
+ } // `export { x }` — no module to follow
671
+ const resolved = (moduleName.startsWith("."))
672
+ ? resolveSourceFile(path__namespace.resolve(path__namespace.dirname(currentFile), moduleName))
673
+ // may be a tsconfig `paths`/`baseUrl` alias onto first-party source;
674
+ // npm packages are filtered out by the resolver
675
+ : resolveNonRelativeImport(moduleName, currentFile);
676
+ if (resolved && !isOwnPackageSource(resolved)) {
677
+ parseFiles([resolved], decoratorName, globalContext);
678
+ }
679
+ }
388
680
  function inspectNode(node, context, decoratorName) {
389
681
  switch (node.kind) {
390
- case ts__namespace.SyntaxKind.ImportClause:
391
- const specifier = node.parent.moduleSpecifier;
392
- if (specifier && specifier.text.startsWith('.')) {
393
- const currentDir = path__namespace.dirname(node.getSourceFile().fileName);
394
- const pathToImport = path__namespace.resolve(currentDir, specifier.text);
395
- parseFiles([pathToImport], decoratorName, globalContext);
396
- }
682
+ case ts__namespace.SyntaxKind.ImportDeclaration:
683
+ case ts__namespace.SyntaxKind.ExportDeclaration:
684
+ // ExportDeclaration too: path aliases usually point at a barrel
685
+ // (`@schemas` -> `schemas/index.ts` -> `export * from "./Player"`).
686
+ followModuleSpecifier(node.moduleSpecifier, node.getSourceFile().fileName, decoratorName);
397
687
  break;
398
688
  case ts__namespace.SyntaxKind.ClassDeclaration:
399
689
  currentStructure = new Class();
@@ -638,7 +928,10 @@ function inspectNode(node, context, decoratorName) {
638
928
  continue;
639
929
  if (!prop.initializer)
640
930
  continue;
641
- const property = currentProperty || new Property();
931
+ // never inherit `currentProperty`: it's the decorator path's
932
+ // carry-over from a visited `deprecated` identifier, and a
933
+ // trailing `.deprecated()` chain can leave it set
934
+ const property = new Property();
642
935
  property.name = prop.name.escapedText;
643
936
  currentStructure.addProperty(property);
644
937
  defineProperty(property, prop.initializer);
@@ -662,7 +955,11 @@ function inspectNode(node, context, decoratorName) {
662
955
  ts__namespace.forEachChild(node, (n) => inspectNode(n, context, decoratorName));
663
956
  }
664
957
  let parsedFiles;
665
- function parseFiles(fileNames, decoratorName = "type", context = new Context()) {
958
+ /**
959
+ * `options` is only honored for a top-level call (one passing a fresh
960
+ * `Context`) — the recursive import walk reuses the run's resolver state.
961
+ */
962
+ function parseFiles(fileNames, decoratorName = "type", context = new Context(), options) {
666
963
  if (typeof ts__namespace.createSourceFile !== "function") {
667
964
  // typescript@7+ (native) no longer ships the JS compiler API
668
965
  throw new Error(`schema-codegen requires the TypeScript compiler API, which the installed "typescript@${ts__namespace.version}" package does not provide.\n` +
@@ -674,25 +971,16 @@ function parseFiles(fileNames, decoratorName = "type", context = new Context())
674
971
  if (globalContext !== context) {
675
972
  parsedFiles = {};
676
973
  globalContext = context;
974
+ // a structure left over from a previous run would make the
975
+ // `currentStructure?.name !== className` guard skip re-registering it
976
+ currentStructure = undefined;
977
+ currentProperty = undefined;
978
+ resetResolver(options);
677
979
  }
678
980
  fileNames.forEach((fileName) => {
679
981
  let sourceFile;
680
982
  let sourceFileName;
681
- const fileNameAlternatives = [];
682
- if (!fileName.endsWith(".ts") &&
683
- !fileName.endsWith(".js") &&
684
- !fileName.endsWith(".mjs")) {
685
- fileNameAlternatives.push(`${fileName}.ts`);
686
- fileNameAlternatives.push(`${fileName}/index.ts`);
687
- }
688
- else if (fileName.endsWith(".js")) {
689
- // Handle .js extensions by also trying .ts (ESM imports often use .js extension)
690
- fileNameAlternatives.push(fileName);
691
- fileNameAlternatives.push(fileName.replace(/\.js$/, ".ts"));
692
- }
693
- else {
694
- fileNameAlternatives.push(fileName);
695
- }
983
+ const fileNameAlternatives = sourceFileCandidates(fileName);
696
984
  for (let i = 0; i < fileNameAlternatives.length; i++) {
697
985
  try {
698
986
  sourceFileName = path__namespace.resolve(fileNameAlternatives[i]);
@@ -2754,7 +3042,7 @@ function generate(targetId, options) {
2754
3042
  }
2755
3043
  return acc;
2756
3044
  }, []);
2757
- const structures = parseFiles(options.files, options.decorator);
3045
+ const structures = parseFiles(options.files, options.decorator, undefined, { tsconfig: options.tsconfig });
2758
3046
  // Post-process classes before generating
2759
3047
  structures.classes.forEach(klass => klass.postProcessing());
2760
3048
  if (options.bundle && generator.renderBundle) {
@@ -2802,7 +3090,9 @@ ${Object.
2802
3090
 
2803
3091
  Optional:
2804
3092
  --namespace: generate namespace on output code
2805
- --decorator: custom name for @type decorator to scan for`);
3093
+ --decorator: custom name for @type decorator to scan for
3094
+ --tsconfig: tsconfig.json to resolve import path aliases with
3095
+ (default: nearest tsconfig.json/jsconfig.json above each source file)`);
2806
3096
  process.exit(exitCode);
2807
3097
  }
2808
3098
  const args = argv(process.argv.slice(2));
@@ -2826,7 +3116,8 @@ try {
2826
3116
  decorator: args.decorator,
2827
3117
  output: args.output,
2828
3118
  namespace: args.namespace,
2829
- bundle: args.bundle
3119
+ bundle: args.bundle,
3120
+ tsconfig: args.tsconfig,
2830
3121
  });
2831
3122
  }
2832
3123
  catch (e) {