@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
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()`, …).
|
|
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 `.
|
|
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
|
|
package/build/Metadata.d.ts
CHANGED
|
@@ -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
|
/**
|
package/build/Reflection.d.ts
CHANGED
|
@@ -71,12 +71,12 @@ export declare const ReflectionField: import("./annotations.js").SchemaWithExten
|
|
|
71
71
|
childPrimitive: FieldBuilder<string, false, false>;
|
|
72
72
|
/** Set only on `t.quantized()` fields (`.optional()` — no auto-instantiated
|
|
73
73
|
* default; its absence is the "not quantized" signal on decode). */
|
|
74
|
-
quantized: FieldBuilder<{
|
|
75
|
-
min
|
|
76
|
-
max
|
|
77
|
-
bits
|
|
78
|
-
mode
|
|
79
|
-
} & Schema<any> & Schema<unknown>, false, true>;
|
|
74
|
+
quantized: FieldBuilder<{
|
|
75
|
+
min: number;
|
|
76
|
+
max: number;
|
|
77
|
+
bits: number;
|
|
78
|
+
mode: number;
|
|
79
|
+
} & {} & Schema<any> & Schema<unknown>, false, true>;
|
|
80
80
|
}, import("./index.js").BuilderInitProps<{
|
|
81
81
|
name: FieldBuilder<string, false, false>;
|
|
82
82
|
type: FieldBuilder<string, false, false>;
|
|
@@ -86,43 +86,45 @@ export declare const ReflectionField: import("./annotations.js").SchemaWithExten
|
|
|
86
86
|
childPrimitive: FieldBuilder<string, false, false>;
|
|
87
87
|
/** Set only on `t.quantized()` fields (`.optional()` — no auto-instantiated
|
|
88
88
|
* default; its absence is the "not quantized" signal on decode). */
|
|
89
|
-
quantized: FieldBuilder<{
|
|
90
|
-
min
|
|
91
|
-
max
|
|
92
|
-
bits
|
|
93
|
-
mode
|
|
94
|
-
} & Schema<any> & Schema<unknown>, false, true>;
|
|
89
|
+
quantized: FieldBuilder<{
|
|
90
|
+
min: number;
|
|
91
|
+
max: number;
|
|
92
|
+
bits: number;
|
|
93
|
+
mode: number;
|
|
94
|
+
} & {} & Schema<any> & Schema<unknown>, false, true>;
|
|
95
95
|
}>, typeof Schema>;
|
|
96
96
|
export type ReflectionField = SchemaType<typeof ReflectionField>;
|
|
97
97
|
export declare const ReflectionType: import("./annotations.js").SchemaWithExtendsConstructor<{
|
|
98
98
|
id: FieldBuilder<number, false, false>;
|
|
99
99
|
extendsId: FieldBuilder<number, false, false>;
|
|
100
|
-
fields: FieldBuilder<ArraySchema<{
|
|
101
|
-
name
|
|
102
|
-
type
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
100
|
+
fields: FieldBuilder<ArraySchema<{
|
|
101
|
+
name: string;
|
|
102
|
+
type: string;
|
|
103
|
+
referencedType: number;
|
|
104
|
+
childPrimitive: string;
|
|
105
|
+
} & {
|
|
106
|
+
quantized?: {
|
|
107
|
+
min: number;
|
|
108
|
+
max: number;
|
|
109
|
+
bits: number;
|
|
110
|
+
mode: number;
|
|
111
|
+
} & {} & Schema<any> & Schema<unknown>;
|
|
111
112
|
} & Schema<any> & Schema<unknown>>, true, false>;
|
|
112
113
|
}, import("./index.js").BuilderInitProps<{
|
|
113
114
|
id: FieldBuilder<number, false, false>;
|
|
114
115
|
extendsId: FieldBuilder<number, false, false>;
|
|
115
|
-
fields: FieldBuilder<ArraySchema<{
|
|
116
|
-
name
|
|
117
|
-
type
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
116
|
+
fields: FieldBuilder<ArraySchema<{
|
|
117
|
+
name: string;
|
|
118
|
+
type: string;
|
|
119
|
+
referencedType: number;
|
|
120
|
+
childPrimitive: string;
|
|
121
|
+
} & {
|
|
122
|
+
quantized?: {
|
|
123
|
+
min: number;
|
|
124
|
+
max: number;
|
|
125
|
+
bits: number;
|
|
126
|
+
mode: number;
|
|
127
|
+
} & {} & Schema<any> & Schema<unknown>;
|
|
126
128
|
} & Schema<any> & Schema<unknown>>, true, false>;
|
|
127
129
|
}>, typeof Schema>;
|
|
128
130
|
export type ReflectionType = SchemaType<typeof ReflectionType>;
|
package/build/codegen/api.d.ts
CHANGED
|
@@ -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;
|
package/build/codegen/cli.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|