UNPKG

@tamgl/colyseus-schema

Version:

Binary state serializer with delta encoding for games

416 lines (339 loc) 15.9 kB
import * as ts from "typescript"; import * as path from "path"; import {readFileSync} from "fs"; import {IStructure, Class, Interface, Property, Context, Enum} from "./types"; let currentStructure: IStructure; let currentProperty: Property; let globalContext: Context; function defineProperty(property: Property, initializer: any) { if (ts.isIdentifier(initializer)) { property.type = "ref"; property.childType = initializer.text; } else if (initializer.kind == ts.SyntaxKind.ObjectLiteralExpression) { property.type = initializer.properties[0].name.text; property.childType = initializer.properties[0].initializer.text; } else if (initializer.kind == ts.SyntaxKind.ArrayLiteralExpression) { property.type = "array"; property.childType = initializer.elements[0].text; } else { property.type = initializer.text; } } function inspectNode(node: ts.Node, context: Context, decoratorName: string) { switch (node.kind) { case ts.SyntaxKind.ImportClause: const specifier = (node.parent as any).moduleSpecifier; if (specifier && (specifier.text as string).startsWith('.')) { const currentDir = path.dirname(node.getSourceFile().fileName); const pathToImport = path.resolve(currentDir, specifier.text); parseFiles([pathToImport], decoratorName, globalContext); } break; case ts.SyntaxKind.ClassDeclaration: currentStructure = new Class(); const heritageClauses = (node as ts.ClassLikeDeclarationBase).heritageClauses; if (heritageClauses && heritageClauses.length > 0) { (currentStructure as Class).extends = heritageClauses[0].types[0].expression.getText(); } context.addStructure(currentStructure); break; case ts.SyntaxKind.InterfaceDeclaration: // // Only generate Interfaces if it has "Message" on its name. // Example: MyMessage // const interfaceName = (node as ts.TypeParameterDeclaration).name.escapedText.toString(); if (interfaceName.indexOf("Message") !== -1) { currentStructure = new Interface(); currentStructure.name = interfaceName; context.addStructure(currentStructure); } break; case ts.SyntaxKind.EnumDeclaration: const enumName = ( node as ts.EnumDeclaration ).name.escapedText.toString(); currentStructure = new Enum(); currentStructure.name = enumName; context.addStructure(currentStructure); break; case ts.SyntaxKind.ExtendsKeyword: // console.log(node.getText()); break; case ts.SyntaxKind.PropertySignature: if (currentStructure instanceof Interface) { const interfaceDeclaration = node.parent; if ( currentStructure.name !== (interfaceDeclaration as ts.TypeParameterDeclaration).name.escapedText.toString() ) { // skip if property if for a another interface than the one we're interested in. break; } // define a property of an interface const property = new Property(); property.name = (node as any).name.escapedText.toString(); property.type = (node as any).type.getText(); currentStructure.addProperty(property); } break; case ts.SyntaxKind.Identifier: if ( node.getText() === "deprecated" && node.parent.kind !== ts.SyntaxKind.ImportSpecifier ) { currentProperty = new Property(); currentProperty.deprecated = true; break; } if (node.getText() === decoratorName) { const prop: any = node.parent?.parent?.parent; const propDecorator = getDecorators(prop); const hasExpression = prop?.expression?.arguments; const hasDecorator = (propDecorator?.length > 0); /** * neither a `@type()` decorator or `type()` call. skip. */ if (!hasDecorator && !hasExpression) { break; } // using as decorator if (propDecorator) { /** * Calling `@type()` as decorator */ const typeDecorator: any = propDecorator.find((decorator => { return (decorator.expression as any).expression.escapedText === decoratorName; })); if (!typeDecorator) { break; } const typeExpression = typeDecorator.expression; const property = currentProperty || new Property(); property.name = prop.name.escapedText; currentStructure.addProperty(property); const typeArgument = typeExpression.arguments[0]; defineProperty(property, typeArgument); const typeName = prop.type && prop.type.typeName && prop.type.typeName.escapedText; if (typeName && typeName.endsWith('Enum')) { property.enumType = typeName; } } else if ( prop.expression.arguments?.[1] && prop.expression.expression.arguments?.[0] ) { /** * Calling `type()` as a regular method */ const property = currentProperty || new Property(); property.name = prop.expression.arguments[1].text; currentStructure.addProperty(property); const typeArgument = prop.expression.expression.arguments[0]; defineProperty(property, typeArgument); } } else if ( node.getText() === "setFields" && ( node.parent.kind === ts.SyntaxKind.CallExpression || node.parent.kind === ts.SyntaxKind.PropertyAccessExpression ) ) { /** * Metadata.setFields(klassName, { ... }) */ const callExpression = (node.parent.kind === ts.SyntaxKind.PropertyAccessExpression) ? node.parent.parent as ts.CallExpression : node.parent as ts.CallExpression; if (callExpression.kind !== ts.SyntaxKind.CallExpression) { break; } const classNameNode = callExpression.arguments[0]; const className = ts.isClassExpression(classNameNode) ? classNameNode.name?.escapedText.toString() : classNameNode.getText(); // skip if no className is provided if (!className) { break; } if (currentStructure.name !== className) { currentStructure = new Class(); } context.addStructure(currentStructure); (currentStructure as Class).extends = "Schema"; // force extends to Schema currentStructure.name = className; const types = callExpression.arguments[1] as any; for (let i = 0; i < types.properties.length; i++) { const prop = types.properties[i]; const property = currentProperty || new Property(); property.name = prop.name.escapedText; currentStructure.addProperty(property); defineProperty(property, prop.initializer); } } else if ( node.getText() === "defineTypes" && ( node.parent.kind === ts.SyntaxKind.CallExpression || node.parent.kind === ts.SyntaxKind.PropertyAccessExpression ) ) { /** * JavaScript source file (`.js`) * Using `defineTypes()` */ const callExpression = (node.parent.kind === ts.SyntaxKind.PropertyAccessExpression) ? node.parent.parent as ts.CallExpression : node.parent as ts.CallExpression; if (callExpression.kind !== ts.SyntaxKind.CallExpression) { break; } const className = callExpression.arguments[0].getText() currentStructure.name = className; const types = callExpression.arguments[1] as any; for (let i = 0; i < types.properties.length; i++) { const prop = types.properties[i]; const property = currentProperty || new Property(); property.name = prop.name.escapedText; currentStructure.addProperty(property); defineProperty(property, prop.initializer); } } if (node.parent.kind === ts.SyntaxKind.ClassDeclaration) { currentStructure.name = node.getText(); } currentProperty = undefined; break; case ts.SyntaxKind.CallExpression: /** * Defining schema via `schema.schema({ ... })` * - schema.schema({}) * - schema({}) * - ClassName.extends({}) */ if ( ( ( (node as ts.CallExpression).expression?.getText() === "schema.schema" || (node as ts.CallExpression).expression?.getText() === "schema" ) || ( (node as ts.CallExpression).expression?.getText().indexOf(".extends") !== -1 ) ) && (node as ts.CallExpression).arguments[0].kind === ts.SyntaxKind.ObjectLiteralExpression ) { const callExpression = node as ts.CallExpression; let className = callExpression.arguments[1]?.getText(); if (!className && callExpression.parent.kind === ts.SyntaxKind.VariableDeclaration) { className = (callExpression.parent as ts.VariableDeclaration).name?.getText(); } // skip if no className is provided if (!className) { break; } if (currentStructure.name !== className) { currentStructure = new Class(); context.addStructure(currentStructure); } if ((node as ts.CallExpression).expression?.getText().indexOf(".extends") !== -1) { // if it's using `.extends({})` const extendsClass = (node as any).expression?.expression?.escapedText; // skip if no extendsClass is provided if (!extendsClass) { break; } (currentStructure as Class).extends = extendsClass; } else { // if it's using `schema({})` (currentStructure as Class).extends = "Schema"; // force extends to Schema } currentStructure.name = className; const types = callExpression.arguments[0] as any; for (let i = 0; i < types.properties.length; i++) { const prop = types.properties[i]; const property = currentProperty || new Property(); property.name = prop.name.escapedText; currentStructure.addProperty(property); defineProperty(property, prop.initializer); } } break; case ts.SyntaxKind.EnumMember: if (currentStructure instanceof Enum) { const initializer = (node as any).initializer?.text; const name = node.getFirstToken().getText(); const property = currentProperty || new Property(); property.name = name; if (initializer !== undefined) { property.type = initializer; } currentStructure.addProperty(property); currentProperty = undefined; } break; } ts.forEachChild(node, (n) => inspectNode(n, context, decoratorName)); } let parsedFiles: { [filename: string]: boolean }; export function parseFiles( fileNames: string[], decoratorName: string = "type", context: Context = new Context() ) { /** * Re-set globalContext for each test case */ if (globalContext !== context) { parsedFiles = {}; globalContext = context; } fileNames.forEach((fileName) => { let sourceFile: ts.Node; let sourceFileName: string; const fileNameAlternatives = []; if ( !fileName.endsWith(".ts") && !fileName.endsWith(".js") && !fileName.endsWith(".mjs") ) { fileNameAlternatives.push(`${fileName}.ts`); fileNameAlternatives.push(`${fileName}/index.ts`); } else { fileNameAlternatives.push(fileName); } for (let i = 0; i < fileNameAlternatives.length; i++) { try { sourceFileName = path.resolve(fileNameAlternatives[i]); if (parsedFiles[sourceFileName]) { break; } sourceFile = ts.createSourceFile( sourceFileName, readFileSync(sourceFileName).toString(), ts.ScriptTarget.Latest, true ); parsedFiles[sourceFileName] = true; break; } catch (e) { // console.log(`${fileNameAlternatives[i]} => ${e.message}`); } } if (sourceFile) { inspectNode(sourceFile, context, decoratorName); } }); return context.getStructures(); } /** * TypeScript 4.8+ has introduced a change on how to access decorators. * - https://github.com/microsoft/TypeScript/pull/49089 * - https://devblogs.microsoft.com/typescript/announcing-typescript-4-8/#decorators-are-placed-on-modifiers-on-typescripts-syntax-trees */ export function getDecorators(node: ts.Node | null | undefined,): undefined | ts.Decorator[] { if (node == undefined) { return undefined; } // TypeScript 4.7 and below // @ts-ignore if (node.decorators) { return node.decorators; } // TypeScript 4.8 and above // @ts-ignore if (ts.canHaveDecorators && ts.canHaveDecorators(node)) { // @ts-ignore const decorators = ts.getDecorators(node); return decorators ? Array.from(decorators) : undefined; } // @ts-ignore return node.modifiers?.filter(ts.isDecorator); }