@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.
- package/README.md +11 -5
- package/build/Metadata.d.ts +10 -1
- package/build/annotations.d.ts +6 -5
- 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/decoder/strategy/Callbacks.d.ts +7 -8
- package/build/decoder/strategy/getDecoderStateCallbacks.d.ts +2 -2
- 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 +3232 -2830
- package/build/index.cjs.map +1 -1
- package/build/index.js +3228 -2826
- package/build/index.mjs +3232 -2830
- package/build/index.mjs.map +1 -1
- package/build/types/HelperTypes.d.ts +24 -14
- package/build/types/TypeContext.d.ts +0 -17
- package/build/types/builder.d.ts +1 -5
- package/build/types/symbols.d.ts +1 -0
- package/package.json +9 -8
- package/src/Metadata.ts +59 -77
- package/src/Reflection.ts +9 -5
- package/src/annotations.ts +28 -18
- 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/decoder/strategy/Callbacks.ts +7 -8
- package/src/decoder/strategy/getDecoderStateCallbacks.ts +2 -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 +43 -34
- package/src/types/TypeContext.ts +5 -52
- package/src/types/builder.ts +14 -10
- package/src/types/custom/ArraySchema.ts +57 -14
- package/src/types/symbols.ts +3 -0
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 {
|
|
@@ -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
|
+
}
|
package/src/codegen/types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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> =
|
|
33
|
-
[K in keyof T]: T[K] extends
|
|
34
|
-
}[keyof T] & string
|
|
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
|
|
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
|
|
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
|
|
33
|
+
: T extends CollectionLike<infer K, infer V, infer _>
|
|
34
34
|
? CollectionCallback<K, V>
|
|
35
35
|
: SchemaCallback<T>;
|
|
36
36
|
|