Skip to content

Commit

Permalink
Merge pull request #9 from playcanvas/typescript
Browse files Browse the repository at this point in the history
TypeScript Support
  • Loading branch information
marklundin authored Sep 26, 2024
2 parents 1d01c9e + 830244c commit e1876a2
Show file tree
Hide file tree
Showing 12 changed files with 507 additions and 177 deletions.
15 changes: 9 additions & 6 deletions src/parsers/attribute-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export class AttributeParser {
let value = null;

// we don't need to serialize the value for arrays
const serializer = !array && this.typeSerializerMap.get(type);
const serializer = !array && this.typeSerializerMap.get(typeName);
if (serializer) {
try {
value = serializer(node.initializer ?? node, this.typeChecker);
Expand All @@ -192,8 +192,8 @@ export class AttributeParser {
let members = [];

// Check if there's a type annotation directly on the variable declaration
if (ts.isVariableDeclaration(node) && node.type) {
typeNode = node.type;
if (node.type) {
typeNode = node;
} else {
// Check for JSDoc annotations
const jsDocs = ts.getJSDocTags(node);
Expand All @@ -203,10 +203,13 @@ export class AttributeParser {
}
}

if (typeNode && ts.isTypeReferenceNode(typeNode.type)) {
// Also consider the elementType
const type = typeNode && (typeNode.type.elementType ?? typeNode.type);

if (typeNode && ts.isTypeReferenceNode(type)) {

// resolve the symbol of the type
let symbol = this.typeChecker.getSymbolAtLocation(typeNode.type.typeName);
let symbol = this.typeChecker.getSymbolAtLocation(type.typeName);

// Resolve aliases, which are common with imports
if (symbol && symbol.flags & ts.SymbolFlags.Alias) {
Expand All @@ -222,7 +225,7 @@ export class AttributeParser {

// Check if the declaration is a TypeScript enum
if (ts.isEnumDeclaration(declaration)) {
members = declaration.members.map(member => member.name.getText());
members = declaration.members.map(member => ({ [member.name.getText()]: member.initializer.text }));
}

// Additionally check for JSDoc enum tag
Expand Down
7 changes: 4 additions & 3 deletions src/parsers/script-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ const SUPPORTED_BLOCK_TAGS = new Map([
['precision', 'number'],
['size', 'number'],
['step', 'number'],
['title', 'string']
['title', 'string'],
['default', 'any']
]);

/**
Expand Down Expand Up @@ -195,7 +196,7 @@ const mapAttributesToOutput = (attribute) => {
}

// set the default value
if (attribute.value !== undefined) attribute.default = attribute.value;
if (attribute.value !== undefined) attribute.default = attribute.default ?? attribute.value;

// Curve Attributes specifically should not expose a default value if it's an empty array
if (attribute.type === 'curve' && Array.isArray(attribute.value) && attribute.value.length === 0) {
Expand Down Expand Up @@ -251,7 +252,7 @@ export class ScriptParser {
const typeName = this.typeChecker.typeToString(type);
const serializer = SUPPORTED_INITIALIZABLE_TYPE_NAMES.get(typeName);
if (serializer) {
this.typeSerializerMap.set(type, serializer);
this.typeSerializerMap.set(typeName, serializer);
}
});

Expand Down
38 changes: 34 additions & 4 deletions src/utils/ts-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,15 +208,15 @@ function getSuperClasses(node, typeChecker) {
export function getJSDocCommentRanges(node, text, typeChecker) {
const commentRanges = [];

if (ts.isClassDeclaration(node)) {
if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
// get an array of the class an all parent classed
const heritageChain = getSuperClasses(node, typeChecker);

// iterate over the heritance chain
heritageChain.forEach((classNode) => {
// for each class iterate over it's class members
classNode.members.forEach((member) => {
if (ts.isPropertyDeclaration(member) || ts.isSetAccessor(member)) {
if (ts.isPropertyDeclaration(member) || ts.isSetAccessor(member) || ts.isPropertySignature(member)) {
const memberName = member.name && ts.isIdentifier(member.name) ? member.name.text : 'unnamed';
const ranges = getLeadingBlockCommentRanges(member, text);
if (ranges.length > 0) {
Expand Down Expand Up @@ -310,6 +310,31 @@ export function isEnum(node) {
return false;
}

/**
* Determines the primitive type for enums, or falls back to the actual type name.
*
* @param {ts.Type} type - The type to inspect.
* @param {ts.TypeChecker} typeChecker - The TypeScript type checker.
* @returns {'string' | 'boolean' | 'number' | null} - The primitive type of the enum or the type's name.
*/
export function getPrimitiveEnumType(type, typeChecker) {
// Check if the type is an enum type
if (!type.symbol?.declarations?.some(decl => ts.isEnumDeclaration(decl))) return null;

// Get the type of enum members
const enumMembers = type.symbol.declarations[0].members;
const firstMemberValue = typeChecker.getConstantValue(enumMembers[0]);

const validEnumType = [
'number',
'string',
'boolean'
];

const typeOf = typeof firstMemberValue;
return validEnumType.includes(typeOf) ? typeOf : null;
}

/**
* Gets the inferred type of a TypeScript node.
*
Expand All @@ -322,7 +347,7 @@ export function getType(node, typeChecker) {
const type = typeChecker.getTypeAtLocation(node);
const array = typeChecker.isArrayType(type);
const actualType = array ? typeChecker.getElementTypeOfArrayType(type) : type;
const name = typeChecker.typeToString(actualType);
const name = getPrimitiveEnumType(actualType, typeChecker) ?? typeChecker.typeToString(actualType);

return { type: actualType, name, array };
}
Expand Down Expand Up @@ -489,10 +514,15 @@ const resolvePropertyAccess = (node, typeChecker) => {
if (ts.isPropertyAssignment(declaration) && declaration.initializer) {
return getLiteralValue(declaration.initializer, typeChecker);
}

if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
return getLiteralValue(declaration.initializer, typeChecker);
}
// Handle other kinds of declarations if needed

if (ts.isEnumMember(declaration)) {
return declaration.initializer ? getLiteralValue(declaration.initializer, typeChecker) : declaration.name.getText();
}

}
}

Expand Down
24 changes: 24 additions & 0 deletions test/fixtures/asset.invalid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// eslint-disable-next-line
import { Script, Asset } from 'playcanvas';

class Example extends Script {
/**
* @attribute
* @resource nothing
*/
a: Asset;

/**
* @attribute
* @resource 1
*/
b : Asset;

/**
* @attribute
* @resource
*/
c : Asset;
}

export { Example };
21 changes: 21 additions & 0 deletions test/fixtures/asset.valid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// eslint-disable-next-line
import { Script, Asset } from 'playcanvas';

class Example extends Script {
/** @attribute */
a : Asset;

/**
* @attribute
* @resource texture
*/
b : Asset;

/**
* @attribute
* @resource container
*/
c : Asset[];
}

export { Example };
20 changes: 2 additions & 18 deletions test/fixtures/enum.valid.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Script, Vec3 } from 'playcanvas';
import { Script } from 'playcanvas';

/**
* @enum {number}
Expand All @@ -19,16 +19,6 @@ const StringEnum = {
C: 'c'
};

/**
* @enum {Vec3}
*/
// eslint-disable-next-line
const Vec3Enum = {
A: new Vec3(1, 2, 3),
B: new Vec3(4, 5, 6),
C: new Vec3(7, 8, 9)
};

/**
* @enum {NumberEnum}
*/
Expand Down Expand Up @@ -65,17 +55,11 @@ class Example extends Script {
*/
h;

/**
* @attribute
* @type {Vec3Enum}
*/
i;

/**
* @attribute
* @type {NumberNumberEnum}
*/
j = 2;
i = 2;
}

export { Example };
49 changes: 49 additions & 0 deletions test/fixtures/enum.valid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Script, Vec3 } from 'playcanvas';

enum NumberEnum {
A = 13,
B = 14,
C = 23
};

enum StringEnum {
A = 'a',
B = 'b',
C = 'c'
};

enum NumberNumberEnum {
A = NumberEnum.A,
B = NumberEnum.B,
C = NumberEnum.C
};

class Example extends Script {
/**
* @attribute
*/
e : NumberEnum = NumberEnum.A;

/**
* @attribute
*/
f : NumberEnum = 1;

/**
* @attribute
* @size 2
*/
g : NumberEnum[];

/**
* @attribute
*/
h : StringEnum;

/**
* @attribute
*/
i : NumberNumberEnum = 2;
}

export { Example };
72 changes: 72 additions & 0 deletions test/fixtures/json.valid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Script, Vec3 } from 'playcanvas';

interface Folder {
/**
* @attribute
*/
a: boolean;

/**
* @attribute
* @default 10
*/
b: number;

/**
* @attribute
* @default hello
*/
c : string;

/**
* @attribute
*/
d : Vec3[];
}

interface NestedFolder {
/**
* @attribute
*/
x : Folder;
}

/**
* @interface
*/
class Example extends Script {
/**
* @attribute
*/
f : Folder;

/**
* @attribute
* @size 2
*/
g : Folder[];

/**
* @attribute
*/
h : NestedFolder;

/**
* @attribute
*/
i = { a: true, b: 10, c: 'hello', d: [new Vec3(1, 2, 3)] };

/**
* @attribute
*/
l : { x: number, y: number };

/**
* @attribute
*/
m : Example;

n = 10;
}

export { Example };
Loading

0 comments on commit e1876a2

Please sign in to comment.