@playcanvas/attribute-parser 1.0.8 → 1.2.0

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/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
7
7
  },
8
- "version": "1.0.8",
8
+ "version": "1.2.0",
9
9
  "dependencies": {
10
10
  "@microsoft/tsdoc": "^0.15.0",
11
11
  "@playcanvas/eslint-config": "^1.7.4",
@@ -4,7 +4,7 @@ import * as ts from 'typescript';
4
4
  import { ParsingError } from './parsing-error.js';
5
5
  import { hasTag } from '../utils/attribute-utils.js';
6
6
  import { parseTag, validateTag } from '../utils/tag-utils.js';
7
- import { extractTextFromDocNode, getLeadingBlockCommentRanges, getType } from '../utils/ts-utils.js';
7
+ import { extractTextFromDocNode, getLeadingBlockCommentRanges, getLiteralValue, getType } from '../utils/ts-utils.js';
8
8
 
9
9
  /**
10
10
  * A class to parse JSDoc comments and extract attribute metadata.
@@ -167,10 +167,10 @@ export class AttributeParser {
167
167
  let value = null;
168
168
 
169
169
  // we don't need to serialize the value for arrays
170
- const serializer = !array && this.typeSerializerMap.get(type);
170
+ const serializer = !array && this.typeSerializerMap.get(typeName);
171
171
  if (serializer) {
172
172
  try {
173
- value = serializer(node.initializer ?? node);
173
+ value = serializer(node.initializer ?? node, this.typeChecker);
174
174
  } catch (error) {
175
175
  errors.push(error);
176
176
  return;
@@ -192,8 +192,8 @@ export class AttributeParser {
192
192
  let members = [];
193
193
 
194
194
  // Check if there's a type annotation directly on the variable declaration
195
- if (ts.isVariableDeclaration(node) && node.type) {
196
- typeNode = node.type;
195
+ if (node.type) {
196
+ typeNode = node;
197
197
  } else {
198
198
  // Check for JSDoc annotations
199
199
  const jsDocs = ts.getJSDocTags(node);
@@ -203,10 +203,13 @@ export class AttributeParser {
203
203
  }
204
204
  }
205
205
 
206
- if (typeNode && ts.isTypeReferenceNode(typeNode.type)) {
206
+ // Also consider the elementType
207
+ const type = typeNode && (typeNode.type.elementType ?? typeNode.type);
208
+
209
+ if (typeNode && ts.isTypeReferenceNode(type)) {
207
210
 
208
211
  // resolve the symbol of the type
209
- let symbol = this.typeChecker.getSymbolAtLocation(typeNode.type.typeName);
212
+ let symbol = this.typeChecker.getSymbolAtLocation(type.typeName);
210
213
 
211
214
  // Resolve aliases, which are common with imports
212
215
  if (symbol && symbol.flags & ts.SymbolFlags.Alias) {
@@ -222,7 +225,7 @@ export class AttributeParser {
222
225
 
223
226
  // Check if the declaration is a TypeScript enum
224
227
  if (ts.isEnumDeclaration(declaration)) {
225
- members = declaration.members.map(member => member.name.getText());
228
+ members = declaration.members.map(member => ({ [member.name.getText()]: member.initializer.text }));
226
229
  }
227
230
 
228
231
  // Additionally check for JSDoc enum tag
@@ -256,19 +259,7 @@ export class AttributeParser {
256
259
 
257
260
  if (ts.isPropertyAssignment(property)) {
258
261
  const name = property.name && ts.isIdentifier(property.name) && property.name.text;
259
- let value;
260
-
261
- const node = property.initializer;
262
-
263
- // Enums can only contain primitives (string|number|boolean)
264
- if (ts.isNumericLiteral(node)) {
265
- value = parseFloat(node.getText());
266
- } else if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) {
267
- value = node.kind === ts.SyntaxKind.TrueKeyword;
268
- } else {
269
- value = node.getText();
270
- }
271
-
262
+ const value = getLiteralValue(property.initializer, this.typeChecker);
272
263
  members.push({ [name]: value });
273
264
  }
274
265
  });
@@ -5,7 +5,7 @@ import { AttributeParser } from './attribute-parser.js';
5
5
  import { ParsingError } from './parsing-error.js';
6
6
  import { hasTag } from '../utils/attribute-utils.js';
7
7
  import { zipArrays } from '../utils/generic-utils.js';
8
- import { flatMapAnyNodes, getJSDocCommentRanges, parseArrayLiteral, parseBooleanNode, parseFloatNode, parseStringNode } from '../utils/ts-utils.js';
8
+ import { flatMapAnyNodes, getJSDocCommentRanges, getLiteralValue, parseArrayLiteral, parseFloatNode } from '../utils/ts-utils.js';
9
9
 
10
10
  /**
11
11
  * @typedef {object} Attribute
@@ -77,9 +77,9 @@ const SUPPORTED_INITIALIZABLE_TYPE_NAMES = new Map([
77
77
  ['Vec3', createNumberArgumentParser('Vec3', [0, 0, 0])],
78
78
  ['Vec4', createNumberArgumentParser('Vec4', [0, 0, 0, 0])],
79
79
  ['Color', createNumberArgumentParser('Color', [1, 1, 1, 1])],
80
- ['number', parseFloatNode],
81
- ['string', parseStringNode],
82
- ['boolean', parseBooleanNode]
80
+ ['number', getLiteralValue],
81
+ ['string', getLiteralValue],
82
+ ['boolean', getLiteralValue]
83
83
  ]);
84
84
 
85
85
  /**
@@ -147,7 +147,8 @@ const SUPPORTED_BLOCK_TAGS = new Map([
147
147
  ['precision', 'number'],
148
148
  ['size', 'number'],
149
149
  ['step', 'number'],
150
- ['title', 'string']
150
+ ['title', 'string'],
151
+ ['default', 'any']
151
152
  ]);
152
153
 
153
154
  /**
@@ -187,8 +188,15 @@ const mapAttributesToOutput = (attribute) => {
187
188
  // remove enum if it's empty
188
189
  if (attribute.enum.length === 0) delete attribute.enum;
189
190
 
191
+ // If the attribute has no default value then set it
192
+ if (attribute.value === undefined) {
193
+ if (attribute.type === 'string') attribute.value = '';
194
+ if (attribute.type === 'number') attribute.value = 0;
195
+ if (attribute.type === 'boolean') attribute.value = false;
196
+ }
197
+
190
198
  // set the default value
191
- if (attribute.value !== undefined) attribute.default = attribute.value;
199
+ if (attribute.value !== undefined) attribute.default = attribute.default ?? attribute.value;
192
200
 
193
201
  // Curve Attributes specifically should not expose a default value if it's an empty array
194
202
  if (attribute.type === 'curve' && Array.isArray(attribute.value) && attribute.value.length === 0) {
@@ -244,7 +252,7 @@ export class ScriptParser {
244
252
  const typeName = this.typeChecker.typeToString(type);
245
253
  const serializer = SUPPORTED_INITIALIZABLE_TYPE_NAMES.get(typeName);
246
254
  if (serializer) {
247
- this.typeSerializerMap.set(type, serializer);
255
+ this.typeSerializerMap.set(typeName, serializer);
248
256
  }
249
257
  });
250
258
 
@@ -1,3 +1,5 @@
1
+ import { DiagnosticCategory } from 'typescript';
2
+
1
3
  import { parseNumber, parseStringToNumericalArray } from './ts-utils.js';
2
4
 
3
5
  /**
@@ -35,31 +37,37 @@ export function parseTag(input = '') {
35
37
 
36
38
  /**
37
39
  * Validates that a tag value matches the expected type
38
- * @throws {Error} - If the tag value is not the expected type
39
40
  *
40
- * @param {String} value - The string representation of the value to type check
41
- * @param {string} typeAnnotation - The expected type
42
- * @param {import('@typescript/vfs').VirtualTypeScriptEnvironment} env - The environment to validate in
43
- * @returns {boolean} - Whether the tag value is valid
41
+ * @param {String} value - The value to be validated.
42
+ * @param {String} typeAnnotation - The TypeScript type annotation as a string.
43
+ * @param {import('@typescript/vfs').VirtualTypeScriptEnvironment} env - The environment containing the language service and file creation utilities.
44
+ * @throws Will throw an error if the value does not conform to the typeAnnotation.
45
+ * @returns {true} if validation passes without type errors.
44
46
  */
45
47
  export function validateTag(value, typeAnnotation, env) {
46
-
48
+ const virtualFileName = '/___virtual__.ts';
47
49
  const sourceText = `let a: ${typeAnnotation} = ${value};`;
48
- env.createFile('/___virtual__.ts', sourceText);
49
50
 
50
- // Get the program and check for semantic errors
51
- const program = env.languageService.getProgram();
52
- const errors = program.getSemanticDiagnostics();
51
+ // Create or overwrite the virtual file with the new source text
52
+ env.createFile(virtualFileName, sourceText);
53
+
54
+ // Retrieve the language service from the environment
55
+ const languageService = env.languageService;
53
56
 
54
- // Filter against the type errors we're concerned with
55
- const typeErrors = errors.filter(error => error.category === 1 && error.code === 2322);
57
+ // Fetch semantic diagnostics only for the virtual file
58
+ const errors = languageService.getSemanticDiagnostics(virtualFileName);
56
59
 
57
- // return first error
58
- const typeError = typeErrors[0];
60
+ // Filter for type assignment errors (Error Code 2322: Type 'X' is not assignable to type 'Y')
61
+ const typeErrors = errors.filter(
62
+ error => error.code === 2322 && error.category === DiagnosticCategory.Error
63
+ );
59
64
 
60
- if (typeError) {
61
- throw new Error(`${typeError.messageText}`);
65
+ // If any type error is found, throw an error with the diagnostic message
66
+ if (typeErrors.length > 0) {
67
+ const errorMessage = typeErrors[0].messageText.toString();
68
+ throw new Error(`Type Validation Error: ${errorMessage}`);
62
69
  }
63
70
 
71
+ // If no type errors are found, return true indicating successful validation
64
72
  return true;
65
73
  }
@@ -208,7 +208,7 @@ function getSuperClasses(node, typeChecker) {
208
208
  export function getJSDocCommentRanges(node, text, typeChecker) {
209
209
  const commentRanges = [];
210
210
 
211
- if (ts.isClassDeclaration(node)) {
211
+ if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
212
212
  // get an array of the class an all parent classed
213
213
  const heritageChain = getSuperClasses(node, typeChecker);
214
214
 
@@ -216,7 +216,7 @@ export function getJSDocCommentRanges(node, text, typeChecker) {
216
216
  heritageChain.forEach((classNode) => {
217
217
  // for each class iterate over it's class members
218
218
  classNode.members.forEach((member) => {
219
- if (ts.isPropertyDeclaration(member) || ts.isSetAccessor(member)) {
219
+ if (ts.isPropertyDeclaration(member) || ts.isSetAccessor(member) || ts.isPropertySignature(member)) {
220
220
  const memberName = member.name && ts.isIdentifier(member.name) ? member.name.text : 'unnamed';
221
221
  const ranges = getLeadingBlockCommentRanges(member, text);
222
222
  if (ranges.length > 0) {
@@ -310,6 +310,31 @@ export function isEnum(node) {
310
310
  return false;
311
311
  }
312
312
 
313
+ /**
314
+ * Determines the primitive type for enums, or falls back to the actual type name.
315
+ *
316
+ * @param {ts.Type} type - The type to inspect.
317
+ * @param {ts.TypeChecker} typeChecker - The TypeScript type checker.
318
+ * @returns {'string' | 'boolean' | 'number' | null} - The primitive type of the enum or the type's name.
319
+ */
320
+ export function getPrimitiveEnumType(type, typeChecker) {
321
+ // Check if the type is an enum type
322
+ if (!type.symbol?.declarations?.some(decl => ts.isEnumDeclaration(decl))) return null;
323
+
324
+ // Get the type of enum members
325
+ const enumMembers = type.symbol.declarations[0].members;
326
+ const firstMemberValue = typeChecker.getConstantValue(enumMembers[0]);
327
+
328
+ const validEnumType = [
329
+ 'number',
330
+ 'string',
331
+ 'boolean'
332
+ ];
333
+
334
+ const typeOf = typeof firstMemberValue;
335
+ return validEnumType.includes(typeOf) ? typeOf : null;
336
+ }
337
+
313
338
  /**
314
339
  * Gets the inferred type of a TypeScript node.
315
340
  *
@@ -322,7 +347,7 @@ export function getType(node, typeChecker) {
322
347
  const type = typeChecker.getTypeAtLocation(node);
323
348
  const array = typeChecker.isArrayType(type);
324
349
  const actualType = array ? typeChecker.getElementTypeOfArrayType(type) : type;
325
- const name = typeChecker.typeToString(actualType);
350
+ const name = getPrimitiveEnumType(actualType, typeChecker) ?? typeChecker.typeToString(actualType);
326
351
 
327
352
  return { type: actualType, name, array };
328
353
  }
@@ -461,6 +486,142 @@ export const parseBooleanNode = (node) => {
461
486
  return node.kind === ts.SyntaxKind.TrueKeyword;
462
487
  };
463
488
 
489
+ function resolveIdentifier(node, typeChecker) {
490
+ const symbol = typeChecker.getSymbolAtLocation(node);
491
+ if (symbol && symbol.declarations) {
492
+ for (const declaration of symbol.declarations) {
493
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
494
+ return getLiteralValue(declaration.initializer, typeChecker);
495
+ }
496
+ // Handle other kinds of declarations if needed
497
+ }
498
+ }
499
+ return undefined;
500
+ }
501
+
502
+ /**
503
+ * Resolve the value of a property access expression. Limited to simple cases like
504
+ * object literals and variable declarations.
505
+ *
506
+ * @param {import('typescript').Node} node - The property access expression node
507
+ * @param {import('typescript')} typeChecker - The TypeScript type checker
508
+ * @returns {any} - The resolved value of the property access
509
+ */
510
+ const resolvePropertyAccess = (node, typeChecker) => {
511
+ const symbol = typeChecker.getSymbolAtLocation(node);
512
+ if (symbol && symbol.declarations) {
513
+ for (const declaration of symbol.declarations) {
514
+ if (ts.isPropertyAssignment(declaration) && declaration.initializer) {
515
+ return getLiteralValue(declaration.initializer, typeChecker);
516
+ }
517
+
518
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
519
+ return getLiteralValue(declaration.initializer, typeChecker);
520
+ }
521
+
522
+ if (ts.isEnumMember(declaration)) {
523
+ return declaration.initializer ? getLiteralValue(declaration.initializer, typeChecker) : declaration.name.getText();
524
+ }
525
+
526
+ }
527
+ }
528
+
529
+ // If symbol not found directly, attempt to resolve the object first
530
+ const objValue = getLiteralValue(node.expression, typeChecker);
531
+ if (objValue && typeof objValue === 'object') {
532
+ const propName = node.name.text;
533
+ return objValue[propName];
534
+ }
535
+
536
+ return undefined;
537
+ };
538
+
539
+ /**
540
+ * Evaluates unary prefixes like +, -, !, ~, and returns the result.
541
+ * @param {import('typescript').Node} node - The AST node to evaluate
542
+ * @param {import('typescript').TypeChecker} typeChecker - The TypeScript type checker
543
+ * @returns {number | boolean | undefined} - The result of the evaluation
544
+ */
545
+ const evaluatePrefixUnaryExpression = (node, typeChecker) => {
546
+ const operandValue = getLiteralValue(node.operand, typeChecker);
547
+ if (operandValue !== undefined) {
548
+ switch (node.operator) {
549
+ case ts.SyntaxKind.PlusToken:
550
+ return +operandValue;
551
+ case ts.SyntaxKind.MinusToken:
552
+ return -operandValue;
553
+ case ts.SyntaxKind.ExclamationToken:
554
+ return !operandValue;
555
+ case ts.SyntaxKind.TildeToken:
556
+ return ~operandValue;
557
+ }
558
+ }
559
+ return undefined;
560
+ };
561
+
562
+ function handleObjectLiteral(node, typeChecker) {
563
+ const obj = {};
564
+ node.properties.forEach((prop) => {
565
+ if (ts.isPropertyAssignment(prop)) {
566
+ const key = prop.name.getText();
567
+ const value = getLiteralValue(prop.initializer, typeChecker);
568
+ obj[key] = value;
569
+ } else if (ts.isShorthandPropertyAssignment(prop)) {
570
+ const key = prop.name.getText();
571
+ const value = resolveIdentifier(prop.name, typeChecker);
572
+ obj[key] = value;
573
+ }
574
+ });
575
+ return obj;
576
+ }
577
+
578
+ /**
579
+ * Attempts to extract a literal value from a TypeScript node. This function
580
+ * supports various types of literals and expressions, including object literals,
581
+ * array literals, identifiers, and unary expressions.
582
+ *
583
+ * @param {import('typescript').Node} node - The AST node to evaluate
584
+ * @param {import('typescript').TypeChecker} typeChecker - The TypeScript type checker
585
+ * @returns {any} - The extracted literal value
586
+ */
587
+ export function getLiteralValue(node, typeChecker) {
588
+ if (!node) return undefined;
589
+
590
+ if (ts.isLiteralExpression(node) || ts.isBooleanLiteral(node)) {
591
+ if (ts.isStringLiteral(node)) {
592
+ return node.text;
593
+ }
594
+ if (ts.isNumericLiteral(node)) {
595
+ return Number(node.text);
596
+ }
597
+ if (node.kind === ts.SyntaxKind.TrueKeyword) {
598
+ return true;
599
+ }
600
+ if (node.kind === ts.SyntaxKind.FalseKeyword) {
601
+ return false;
602
+ }
603
+ }
604
+
605
+ switch (node.kind) {
606
+ case ts.SyntaxKind.NullKeyword:
607
+ return null;
608
+ case ts.SyntaxKind.ArrayLiteralExpression:
609
+ return (node).elements.map(element => getLiteralValue(element, typeChecker));
610
+ case ts.SyntaxKind.ObjectLiteralExpression:
611
+ return handleObjectLiteral(node, typeChecker);
612
+ case ts.SyntaxKind.Identifier:
613
+ return resolveIdentifier(node, typeChecker);
614
+ case ts.SyntaxKind.PropertyAccessExpression:
615
+ return resolvePropertyAccess(node, typeChecker);
616
+ case ts.SyntaxKind.ParenthesizedExpression:
617
+ return getLiteralValue((node).expression, typeChecker);
618
+ case ts.SyntaxKind.PrefixUnaryExpression:
619
+ return evaluatePrefixUnaryExpression(node, typeChecker);
620
+ default:
621
+ return undefined;
622
+ }
623
+ }
624
+
464
625
  /**
465
626
  * If the given node is a string literal, returns the parsed string.
466
627
  * @param {import('typescript').Node} node - The node to check
@@ -0,0 +1,24 @@
1
+ // eslint-disable-next-line
2
+ import { Script, Asset } from 'playcanvas';
3
+
4
+ class Example extends Script {
5
+ /**
6
+ * @attribute
7
+ * @resource nothing
8
+ */
9
+ a: Asset;
10
+
11
+ /**
12
+ * @attribute
13
+ * @resource 1
14
+ */
15
+ b : Asset;
16
+
17
+ /**
18
+ * @attribute
19
+ * @resource
20
+ */
21
+ c : Asset;
22
+ }
23
+
24
+ export { Example };
@@ -0,0 +1,21 @@
1
+ // eslint-disable-next-line
2
+ import { Script, Asset } from 'playcanvas';
3
+
4
+ class Example extends Script {
5
+ /** @attribute */
6
+ a : Asset;
7
+
8
+ /**
9
+ * @attribute
10
+ * @resource texture
11
+ */
12
+ b : Asset;
13
+
14
+ /**
15
+ * @attribute
16
+ * @resource container
17
+ */
18
+ c : Asset[];
19
+ }
20
+
21
+ export { Example };
@@ -1,12 +1,12 @@
1
- import { Script, Vec3 } from 'playcanvas';
1
+ import { Script } from 'playcanvas';
2
2
 
3
3
  /**
4
4
  * @enum {number}
5
5
  */
6
6
  const NumberEnum = {
7
- A: 0,
8
- B: 1,
9
- C: 2
7
+ A: 13,
8
+ B: 14,
9
+ C: 23
10
10
  };
11
11
 
12
12
  /**
@@ -19,16 +19,6 @@ const StringEnum = {
19
19
  C: 'c'
20
20
  };
21
21
 
22
- /**
23
- * @enum {Vec3}
24
- */
25
- // eslint-disable-next-line
26
- const Vec3Enum = {
27
- A: new Vec3(1, 2, 3),
28
- B: new Vec3(4, 5, 6),
29
- C: new Vec3(7, 8, 9)
30
- };
31
-
32
22
  /**
33
23
  * @enum {NumberEnum}
34
24
  */
@@ -44,7 +34,7 @@ class Example extends Script {
44
34
  * @attribute
45
35
  * @type {NumberEnum}
46
36
  */
47
- e;
37
+ e = NumberEnum.A;
48
38
 
49
39
  /**
50
40
  * @attribute
@@ -65,17 +55,11 @@ class Example extends Script {
65
55
  */
66
56
  h;
67
57
 
68
- /**
69
- * @attribute
70
- * @type {Vec3Enum}
71
- */
72
- i;
73
-
74
58
  /**
75
59
  * @attribute
76
60
  * @type {NumberNumberEnum}
77
61
  */
78
- j = 2;
62
+ i = 2;
79
63
  }
80
64
 
81
65
  export { Example };
@@ -0,0 +1,49 @@
1
+ import { Script, Vec3 } from 'playcanvas';
2
+
3
+ enum NumberEnum {
4
+ A = 13,
5
+ B = 14,
6
+ C = 23
7
+ };
8
+
9
+ enum StringEnum {
10
+ A = 'a',
11
+ B = 'b',
12
+ C = 'c'
13
+ };
14
+
15
+ enum NumberNumberEnum {
16
+ A = NumberEnum.A,
17
+ B = NumberEnum.B,
18
+ C = NumberEnum.C
19
+ };
20
+
21
+ class Example extends Script {
22
+ /**
23
+ * @attribute
24
+ */
25
+ e : NumberEnum = NumberEnum.A;
26
+
27
+ /**
28
+ * @attribute
29
+ */
30
+ f : NumberEnum = 1;
31
+
32
+ /**
33
+ * @attribute
34
+ * @size 2
35
+ */
36
+ g : NumberEnum[];
37
+
38
+ /**
39
+ * @attribute
40
+ */
41
+ h : StringEnum;
42
+
43
+ /**
44
+ * @attribute
45
+ */
46
+ i : NumberNumberEnum = 2;
47
+ }
48
+
49
+ export { Example };
@@ -0,0 +1,72 @@
1
+ import { Script, Vec3 } from 'playcanvas';
2
+
3
+ interface Folder {
4
+ /**
5
+ * @attribute
6
+ */
7
+ a: boolean;
8
+
9
+ /**
10
+ * @attribute
11
+ * @default 10
12
+ */
13
+ b: number;
14
+
15
+ /**
16
+ * @attribute
17
+ * @default hello
18
+ */
19
+ c : string;
20
+
21
+ /**
22
+ * @attribute
23
+ */
24
+ d : Vec3[];
25
+ }
26
+
27
+ interface NestedFolder {
28
+ /**
29
+ * @attribute
30
+ */
31
+ x : Folder;
32
+ }
33
+
34
+ /**
35
+ * @interface
36
+ */
37
+ class Example extends Script {
38
+ /**
39
+ * @attribute
40
+ */
41
+ f : Folder;
42
+
43
+ /**
44
+ * @attribute
45
+ * @size 2
46
+ */
47
+ g : Folder[];
48
+
49
+ /**
50
+ * @attribute
51
+ */
52
+ h : NestedFolder;
53
+
54
+ /**
55
+ * @attribute
56
+ */
57
+ i = { a: true, b: 10, c: 'hello', d: [new Vec3(1, 2, 3)] };
58
+
59
+ /**
60
+ * @attribute
61
+ */
62
+ l : { x: number, y: number };
63
+
64
+ /**
65
+ * @attribute
66
+ */
67
+ m : Example;
68
+
69
+ n = 10;
70
+ }
71
+
72
+ export { Example };
@@ -8,13 +8,17 @@ export const MyEnum = { value: 0 };
8
8
  class Example extends Script {
9
9
  /**
10
10
  * @attribute
11
- * @type {boolean}
11
+ * @precision 1
12
+ * @type {number}
12
13
  */
13
- a = false;
14
+ a;
14
15
 
15
16
  initialize() {
16
17
  confetti();
17
18
  new TWEEN.Tween({ x: 0 }).to({ x: 100 }, 1000).start();
19
+
20
+ // This is an intentional type error, but the parser should ignore these
21
+ this.a = 'string';
18
22
  }
19
23
  }
20
24
 
@@ -3,48 +3,57 @@ import { describe, it, before } from 'mocha';
3
3
 
4
4
  import { parseAttributes } from '../../utils.js';
5
5
 
6
- describe('VALID: Asset attribute', function () {
7
- let data;
8
- before(async function () {
9
- data = await parseAttributes('./asset.valid.js');
10
- });
11
-
12
- it('only results should exist', function () {
13
- expect(data).to.exist;
14
- expect(data[0]).to.not.be.empty;
15
- expect(data[1]).to.be.empty;
16
- });
17
-
18
- it('Example: should exist without errors', function () {
19
- expect(data[0]?.example).to.exist;
20
- expect(data[0].example.attributes).to.not.be.empty;
21
- expect(data[0].example.errors).to.be.empty;
22
- });
23
-
24
- it('a: should be a asset attribute', function () {
25
- expect(data[0].example.attributes.a).to.exist;
26
- expect(data[0].example.attributes.a.name).to.equal('a');
27
- expect(data[0].example.attributes.a.type).to.equal('asset');
28
- expect(data[0].example.attributes.a.array).to.equal(false);
29
- expect(data[0].example.attributes.a.default).to.equal(null);
30
- });
31
-
32
- it('b: should be a asset attribute with a texture resource', function () {
33
- expect(data[0].example.attributes.b).to.exist;
34
- expect(data[0].example.attributes.b.name).to.equal('b');
35
- expect(data[0].example.attributes.b.type).to.equal('asset');
36
- expect(data[0].example.attributes.b.array).to.equal(false);
37
- expect(data[0].example.attributes.b.default).to.equal(null);
38
- expect(data[0].example.attributes.b.resource).to.equal('texture');
39
- });
6
+ function runTests(fileName) {
7
+
8
+ const isTS = fileName.endsWith('.ts');
9
+ const script = isTS ? 'TS' : 'JS';
10
+
11
+ describe(`${script}: VALID: Asset attribute`, function () {
12
+ let data;
13
+ before(async function () {
14
+ data = await parseAttributes(fileName);
15
+ });
16
+
17
+ it('only results should exist', function () {
18
+ expect(data).to.exist;
19
+ expect(data[0]).to.not.be.empty;
20
+ expect(data[1]).to.be.empty;
21
+ });
22
+
23
+ it('Example: should exist without errors', function () {
24
+ expect(data[0]?.example).to.exist;
25
+ expect(data[0].example.attributes).to.not.be.empty;
26
+ expect(data[0].example.errors).to.be.empty;
27
+ });
28
+
29
+ it('a: should be a asset attribute', function () {
30
+ expect(data[0].example.attributes.a).to.exist;
31
+ expect(data[0].example.attributes.a.name).to.equal('a');
32
+ expect(data[0].example.attributes.a.type).to.equal('asset');
33
+ expect(data[0].example.attributes.a.array).to.equal(false);
34
+ expect(data[0].example.attributes.a.default).to.equal(null);
35
+ });
36
+
37
+ it('b: should be a asset attribute with a texture resource', function () {
38
+ expect(data[0].example.attributes.b).to.exist;
39
+ expect(data[0].example.attributes.b.name).to.equal('b');
40
+ expect(data[0].example.attributes.b.type).to.equal('asset');
41
+ expect(data[0].example.attributes.b.array).to.equal(false);
42
+ expect(data[0].example.attributes.b.default).to.equal(null);
43
+ expect(data[0].example.attributes.b.resource).to.equal('texture');
44
+ });
45
+
46
+ it('c: should be a asset attribute array with a container resource', function () {
47
+ expect(data[0].example.attributes.c).to.exist;
48
+ expect(data[0].example.attributes.c.name).to.equal('c');
49
+ expect(data[0].example.attributes.c.type).to.equal('asset');
50
+ expect(data[0].example.attributes.c.array).to.equal(true);
51
+ expect(data[0].example.attributes.c.default).to.equal(null);
52
+ expect(data[0].example.attributes.c.resource).to.equal('container');
53
+ });
40
54
 
41
- it('c: should be a asset attribute array with a container resource', function () {
42
- expect(data[0].example.attributes.c).to.exist;
43
- expect(data[0].example.attributes.c.name).to.equal('c');
44
- expect(data[0].example.attributes.c.type).to.equal('asset');
45
- expect(data[0].example.attributes.c.array).to.equal(true);
46
- expect(data[0].example.attributes.c.default).to.equal(null);
47
- expect(data[0].example.attributes.c.resource).to.equal('container');
48
55
  });
56
+ }
49
57
 
50
- });
58
+ runTests('./asset.valid.js');
59
+ runTests('./asset.valid.ts');
@@ -3,47 +3,55 @@ import { describe, it, before } from 'mocha';
3
3
 
4
4
  import { parseAttributes } from '../../utils.js';
5
5
 
6
- describe('VALID: Checkbox attribute', function () {
7
- let data;
8
- before(async function () {
9
- data = await parseAttributes('./checkbox.valid.js');
10
- });
11
-
12
- it('only results should exist', function () {
13
- expect(data).to.exist;
14
- expect(data[0]).to.not.be.empty;
15
- expect(data[1]).to.be.empty;
16
- });
17
-
18
- it('Example: should exist without errors', function () {
19
- expect(data[0]?.example).to.exist;
20
- expect(data[0].example.attributes).to.not.be.empty;
21
- expect(data[0].example.errors).to.be.empty;
22
- });
23
-
24
- it('a: should be a checkbox attribute', function () {
25
- expect(data[0].example.attributes.a).to.exist;
26
- expect(data[0].example.attributes.a.name).to.equal('a');
27
- expect(data[0].example.attributes.a.type).to.equal('boolean');
28
- expect(data[0].example.attributes.a.array).to.equal(false);
29
- expect(data[0].example.attributes.a.default).to.equal(false);
30
- });
31
-
32
- it('b: should be a checkbox attribute with a default value', function () {
33
- expect(data[0].example.attributes.b).to.exist;
34
- expect(data[0].example.attributes.b.name).to.equal('b');
35
- expect(data[0].example.attributes.b.type).to.equal('boolean');
36
- expect(data[0].example.attributes.b.array).to.equal(false);
37
- expect(data[0].example.attributes.b.default).to.equal(true);
38
- });
6
+ function runTests(fileName) {
7
+
8
+ const isTS = fileName.endsWith('.ts');
9
+ const script = isTS ? 'TS' : 'JS';
10
+
11
+ describe(`${script}: VALID: Checkbox attribute`, function () {
12
+ let data;
13
+ before(async function () {
14
+ data = await parseAttributes(fileName);
15
+ });
16
+
17
+ it('only results should exist', function () {
18
+ expect(data).to.exist;
19
+ expect(data[0]).to.not.be.empty;
20
+ expect(data[1]).to.be.empty;
21
+ });
22
+
23
+ it('Example: should exist without errors', function () {
24
+ expect(data[0]?.example).to.exist;
25
+ expect(data[0].example.attributes).to.not.be.empty;
26
+ expect(data[0].example.errors).to.be.empty;
27
+ });
28
+
29
+ it('a: should be a checkbox attribute', function () {
30
+ expect(data[0].example.attributes.a).to.exist;
31
+ expect(data[0].example.attributes.a.name).to.equal('a');
32
+ expect(data[0].example.attributes.a.type).to.equal('boolean');
33
+ expect(data[0].example.attributes.a.array).to.equal(false);
34
+ expect(data[0].example.attributes.a.default).to.equal(false);
35
+ });
36
+
37
+ it('b: should be a checkbox attribute with a default value', function () {
38
+ expect(data[0].example.attributes.b).to.exist;
39
+ expect(data[0].example.attributes.b.name).to.equal('b');
40
+ expect(data[0].example.attributes.b.type).to.equal('boolean');
41
+ expect(data[0].example.attributes.b.array).to.equal(false);
42
+ expect(data[0].example.attributes.b.default).to.equal(true);
43
+ });
44
+
45
+ it('c: should be a checkbox attribute array with a size', function () {
46
+ expect(data[0].example.attributes.c).to.exist;
47
+ expect(data[0].example.attributes.c.name).to.equal('c');
48
+ expect(data[0].example.attributes.c.type).to.equal('boolean');
49
+ expect(data[0].example.attributes.c.array).to.equal(true);
50
+ expect(data[0].example.attributes.c.size).to.equal(2);
51
+ expect(data[0].example.attributes.c.default).equal(null);
52
+ });
39
53
 
40
- it('c: should be a checkbox attribute array with a size', function () {
41
- expect(data[0].example.attributes.c).to.exist;
42
- expect(data[0].example.attributes.c.name).to.equal('c');
43
- expect(data[0].example.attributes.c.type).to.equal('boolean');
44
- expect(data[0].example.attributes.c.array).to.equal(true);
45
- expect(data[0].example.attributes.c.size).to.equal(2);
46
- expect(data[0].example.attributes.c.default).equal(null);
47
54
  });
55
+ }
48
56
 
49
- });
57
+ runTests('./checkbox.valid.js');
@@ -3,71 +3,76 @@ import { describe, it, before } from 'mocha';
3
3
 
4
4
  import { parseAttributes } from '../../utils.js';
5
5
 
6
- describe('VALID: Enum attribute', function () {
7
- let data;
8
- before(async function () {
9
- data = await parseAttributes('./enum.valid.js');
10
- });
6
+ function runTests(fileName) {
11
7
 
12
- it('only results should exist', function () {
13
- expect(data).to.exist;
14
- expect(data[0]).to.not.be.empty;
15
- expect(data[1]).to.be.empty;
16
- });
8
+ const isTS = fileName.endsWith('.ts');
9
+ const script = isTS ? 'TS' : 'JS';
17
10
 
18
- it('Example: should exist without errors', function () {
19
- expect(data[0]?.example).to.exist;
20
- expect(data[0].example.attributes).to.not.be.empty;
21
- expect(data[0].example.errors).to.be.empty;
22
- });
11
+ describe(`${script}: VALID: Enum attribute`, function () {
12
+ let data;
13
+ before(async function () {
14
+ data = await parseAttributes(fileName);
15
+ });
23
16
 
24
- it('e: should be a enum attribute', function () {
25
- expect(data[0].example.attributes.e).to.exist;
26
- expect(data[0].example.attributes.e.name).to.equal('e');
27
- expect(data[0].example.attributes.e.type).to.equal('number');
28
- expect(data[0].example.attributes.e.array).to.equal(false);
29
- expect(data[0].example.attributes.e.default).to.equal(0);
30
- });
17
+ it('only results should exist', function () {
18
+ expect(data).to.exist;
19
+ expect(data[0]).to.not.be.empty;
20
+ expect(data[1]).to.be.empty;
21
+ });
31
22
 
32
- it('f: should be a enum attribute with a default value', function () {
33
- expect(data[0].example.attributes.f).to.exist;
34
- expect(data[0].example.attributes.f.name).to.equal('f');
35
- expect(data[0].example.attributes.f.type).to.equal('number');
36
- expect(data[0].example.attributes.f.array).to.equal(false);
37
- expect(data[0].example.attributes.f.default).to.equal(1);
38
- });
23
+ it('Example: should exist without errors', function () {
24
+ expect(data[0]?.example).to.exist;
25
+ expect(data[0].example.errors).to.be.empty;
26
+ expect(data[0].example.attributes).to.not.be.empty;
27
+ });
39
28
 
40
- it('g: should be a enum attribute array with a size', function () {
41
- expect(data[0].example.attributes.g).to.exist;
42
- expect(data[0].example.attributes.g.name).to.equal('g');
43
- expect(data[0].example.attributes.g.type).to.equal('number');
44
- expect(data[0].example.attributes.g.array).to.equal(true);
45
- expect(data[0].example.attributes.g.size).to.equal(2);
46
- expect(data[0].example.attributes.g.default).equal(null);
47
- });
29
+ it('e: should be a enum attribute', function () {
30
+ expect(data[0].example.attributes.e).to.exist;
31
+ expect(data[0].example.attributes.e.name).to.equal('e');
32
+ expect(data[0].example.attributes.e.type).to.equal('number');
33
+ expect(data[0].example.attributes.e.array).to.equal(false);
34
+ expect(data[0].example.attributes.e.default).to.equal(13);
35
+ });
48
36
 
49
- it('h: should be a enum attribute', function () {
50
- expect(data[0].example.attributes.h).to.exist;
51
- expect(data[0].example.attributes.h.name).to.equal('h');
52
- expect(data[0].example.attributes.h.type).to.equal('string');
53
- expect(data[0].example.attributes.h.array).to.equal(false);
54
- expect(data[0].example.attributes.h.default).to.equal('');
55
- });
37
+ it('f: should be a enum attribute with a default value', function () {
38
+ expect(data[0].example.attributes.f).to.exist;
39
+ expect(data[0].example.attributes.f.name).to.equal('f');
40
+ expect(data[0].example.attributes.f.type).to.equal('number');
41
+ expect(data[0].example.attributes.f.array).to.equal(false);
42
+ expect(data[0].example.attributes.f.default).to.equal(1);
43
+ });
56
44
 
57
- it('i: should be a enum attribute with a default value', function () {
58
- expect(data[0].example.attributes.i).to.exist;
59
- expect(data[0].example.attributes.i.name).to.equal('i');
60
- expect(data[0].example.attributes.i.type).to.equal('vec3');
61
- expect(data[0].example.attributes.i.array).to.equal(false);
62
- expect(data[0].example.attributes.i.default).to.eql([0, 0, 0]);
63
- });
45
+ it('g: should be a enum attribute array with a size', function () {
46
+ expect(data[0].example.attributes.g).to.exist;
47
+ expect(data[0].example.attributes.g.name).to.equal('g');
48
+ expect(data[0].example.attributes.g.type).to.equal('number');
49
+ expect(data[0].example.attributes.g.array).to.equal(true);
50
+ expect(data[0].example.attributes.g.size).to.equal(2);
51
+ expect(data[0].example.attributes.g.default).equal(null);
52
+ });
53
+
54
+ it('h: should be a enum attribute', function () {
55
+ expect(data[0].example.attributes.h).to.exist;
56
+ expect(data[0].example.attributes.h.name).to.equal('h');
57
+ expect(data[0].example.attributes.h.type).to.equal('string');
58
+ expect(data[0].example.attributes.h.array).to.equal(false);
59
+ expect(data[0].example.attributes.h.enum).to.be.an('array').with.lengthOf(3);
60
+ expect(data[0].example.attributes.h.enum[0]).to.deep.equal({ A: 'a' });
61
+ expect(data[0].example.attributes.h.enum[1]).to.deep.equal({ B: 'b' });
62
+ expect(data[0].example.attributes.h.enum[2]).to.deep.equal({ C: 'c' });
63
+ expect(data[0].example.attributes.h.default).to.equal('');
64
+ });
65
+
66
+ it('j: should be a enum attribute array with a size', function () {
67
+ expect(data[0].example.attributes.i).to.exist;
68
+ expect(data[0].example.attributes.i.name).to.equal('i');
69
+ expect(data[0].example.attributes.i.type).to.equal('number');
70
+ expect(data[0].example.attributes.i.array).to.equal(false);
71
+ expect(data[0].example.attributes.i.default).equal(2);
72
+ });
64
73
 
65
- it('j: should be a enum attribute array with a size', function () {
66
- expect(data[0].example.attributes.j).to.exist;
67
- expect(data[0].example.attributes.j.name).to.equal('j');
68
- expect(data[0].example.attributes.j.type).to.equal('number');
69
- expect(data[0].example.attributes.j.array).to.equal(false);
70
- expect(data[0].example.attributes.j.default).equal(2);
71
74
  });
75
+ }
72
76
 
73
- });
77
+ runTests('./enum.valid.js');
78
+ runTests('./enum.valid.ts');
@@ -3,7 +3,8 @@ import { describe, it, before } from 'mocha';
3
3
 
4
4
  import { parseAttributes } from '../../utils.js';
5
5
 
6
- describe('VALID: JSON attribute', function () {
6
+ describe('JS: VALID: Asset attribute', function () {
7
+
7
8
  let data;
8
9
  before(async function () {
9
10
  data = await parseAttributes('./json.valid.js');
@@ -147,3 +148,130 @@ describe('VALID: JSON attribute', function () {
147
148
  expect(data[0].example.attributes.o).to.not.exist;
148
149
  });
149
150
  });
151
+
152
+
153
+ describe('TS: VALID: Asset attribute', function () {
154
+
155
+ let data;
156
+ before(async function () {
157
+ data = await parseAttributes('./json.valid.ts');
158
+ });
159
+
160
+ it('only results should exist', function () {
161
+ expect(data).to.exist;
162
+ expect(data[0]).to.not.be.empty;
163
+ expect(data[1]).to.be.empty;
164
+ });
165
+
166
+ it('Example: should exist without errors', function () {
167
+ expect(data[0]?.example).to.exist;
168
+ expect(data[0].example.attributes).to.not.be.empty;
169
+ expect(data[0].example.errors).to.be.empty;
170
+ });
171
+
172
+ it('f: should be a json attribute', function () {
173
+ expect(data[0].example.attributes.f).to.exist;
174
+ expect(data[0].example.attributes.f.name).to.equal('f');
175
+ expect(data[0].example.attributes.f.type).to.equal('json');
176
+ expect(data[0].example.attributes.f.array).to.equal(false);
177
+ expect(data[0].example.attributes.f.default).to.equal(null);
178
+
179
+ expect(data[0].example.attributes.f.schema).to.exist;
180
+ });
181
+
182
+ it('f[a]: should be a boolean attribute', function () {
183
+ expect(data[0].example.attributes.f.schema[0].name).to.equal('a');
184
+ expect(data[0].example.attributes.f.schema[0].type).to.equal('boolean');
185
+ expect(data[0].example.attributes.f.schema[0].array).to.equal(false);
186
+ expect(data[0].example.attributes.f.schema[0].default).to.equal(false);
187
+ });
188
+
189
+ it('f[b]: should be a numeric attribute', function () {
190
+ expect(data[0].example.attributes.f.schema[1].name).to.equal('b');
191
+ expect(data[0].example.attributes.f.schema[1].type).to.equal('number');
192
+ expect(data[0].example.attributes.f.schema[1].array).to.equal(false);
193
+ expect(data[0].example.attributes.f.schema[1].default).to.equal(10);
194
+ });
195
+
196
+ it('f[c]: should be a string attribute', function () {
197
+ expect(data[0].example.attributes.f.schema[2].name).to.equal('c');
198
+ expect(data[0].example.attributes.f.schema[2].type).to.equal('string');
199
+ expect(data[0].example.attributes.f.schema[2].array).to.equal(false);
200
+ expect(data[0].example.attributes.f.schema[2].default).to.equal('hello');
201
+ });
202
+
203
+ it('f[d]: should be a vec3 attribute array', function () {
204
+ expect(data[0].example.attributes.f.schema[3].name).to.equal('d');
205
+ expect(data[0].example.attributes.f.schema[3].type).to.equal('vec3');
206
+ expect(data[0].example.attributes.f.schema[3].array).to.equal(true);
207
+ expect(data[0].example.attributes.f.schema[3].default).to.eql(null);
208
+ });
209
+
210
+ it('g: should be a json attribute array with a size', function () {
211
+ expect(data[0].example.attributes.g).to.exist;
212
+ expect(data[0].example.attributes.g.name).to.equal('g');
213
+ expect(data[0].example.attributes.g.type).to.equal('json');
214
+ expect(data[0].example.attributes.g.array).to.equal(true);
215
+ expect(data[0].example.attributes.g.size).to.equal(2);
216
+ expect(data[0].example.attributes.g.default).to.eql(null);
217
+
218
+ expect(data[0].example.attributes.g.schema).to.exist;
219
+ });
220
+
221
+ it('h: should be a nested json attribute (parent)', function () {
222
+ expect(data[0].example.attributes.h).to.exist;
223
+ expect(data[0].example.attributes.h.name).to.equal('h');
224
+ expect(data[0].example.attributes.h.type).to.equal('json');
225
+ expect(data[0].example.attributes.h.array).to.equal(false);
226
+ expect(data[0].example.attributes.h.default).to.eql(null);
227
+
228
+ expect(data[0].example.attributes.h.schema).to.exist;
229
+ });
230
+
231
+ it('h[x]: should be a nested json attribute (child)', function () {
232
+ expect(data[0].example.attributes.h.schema[0].name).to.equal('x');
233
+ expect(data[0].example.attributes.h.schema[0].type).to.equal('json');
234
+ expect(data[0].example.attributes.h.schema[0].array).to.equal(false);
235
+ expect(data[0].example.attributes.h.schema[0].default).to.eql(null);
236
+
237
+ expect(data[0].example.attributes.h.schema[0].schema).to.exist;
238
+ });
239
+
240
+ it('i: should be an inline default json attribute', function () {
241
+ expect(data[0].example.attributes.i).to.exist;
242
+ expect(data[0].example.attributes.i.name).to.equal('i');
243
+ expect(data[0].example.attributes.i.type).to.equal('json');
244
+ expect(data[0].example.attributes.i.array).to.equal(false);
245
+ expect(data[0].example.attributes.i.default).to.eql(null);
246
+
247
+ expect(data[0].example.attributes.i.schema).to.exist;
248
+ });
249
+
250
+ it('l: should be an inline type json attribute', function () {
251
+ expect(data[0].example.attributes.l).to.exist;
252
+ expect(data[0].example.attributes.l.name).to.equal('l');
253
+ expect(data[0].example.attributes.l.type).to.equal('json');
254
+ expect(data[0].example.attributes.l.array).to.equal(false);
255
+ expect(data[0].example.attributes.l.default).to.eql(null);
256
+
257
+ expect(data[0].example.attributes.l.schema).to.exist;
258
+ });
259
+
260
+ it('m: should be a self reference json attribute', function () {
261
+ expect(data[0].example.attributes.m).to.exist;
262
+ expect(data[0].example.attributes.m.name).to.equal('m');
263
+ expect(data[0].example.attributes.m.type).to.equal('json');
264
+ expect(data[0].example.attributes.m.array).to.equal(false);
265
+ expect(data[0].example.attributes.m.default).to.eql(null);
266
+
267
+ expect(data[0].example.attributes.m.schema).to.exist;
268
+ });
269
+
270
+ it('n: should not be recognised as an attribute (no attribute tag)', function () {
271
+ expect(data[0].example.attributes.n).to.not.exist;
272
+ });
273
+
274
+ it('o: should not be recognised as an attribute (type not an interface)', function () {
275
+ expect(data[0].example.attributes.o).to.not.exist;
276
+ });
277
+ });
@@ -13,5 +13,6 @@ describe('VALID: Program ', function () {
13
13
  expect(data).to.exist;
14
14
  expect(data[0]).to.not.be.empty;
15
15
  expect(data[1]).to.be.empty;
16
+ expect(data[0].example.errors).to.be.empty;
16
17
  });
17
18
  });