@playcanvas/attribute-parser 1.5.1 → 1.6.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.
@@ -18,7 +18,7 @@ jobs:
18
18
  - name: Setup Node.js 18.x
19
19
  uses: actions/setup-node@v4
20
20
  with:
21
- node-version: 18.x
21
+ node-version: 22.x
22
22
  cache: 'npm'
23
23
 
24
24
  - name: Install dependencies
@@ -39,7 +39,7 @@ jobs:
39
39
  - name: Setup Node.js 18.x
40
40
  uses: actions/setup-node@v4
41
41
  with:
42
- node-version: 18.x
42
+ node-version: 22.x
43
43
  cache: 'npm'
44
44
 
45
45
  - name: Install dependencies
package/README.md CHANGED
@@ -1,59 +1,55 @@
1
1
  # PlayCanvas Attribute Parser
2
2
 
3
- This the official JSDoc attribute parser used in the PlayCanvas editor.
3
+ This is the official JSDoc attribute parser used in the PlayCanvas Editor.
4
4
 
5
- It's used in the PlayCanvas Editor to collect metadata from jsdoc annotated users scripts. This allows special `@attribute` members which can surface contextual data about class members.
5
+ It collects metadata from user scripts by parsing `@attribute` JSDoc annotations. These attributes enable the PlayCanvas Editor to expose UI controls and contextual information for your script properties.
6
6
 
7
- ## A quick examples
7
+ Example
8
8
 
9
- It takes a script like this...
9
+ Given a script like this:
10
10
 
11
11
  ```javascript
12
12
  class Rotator extends ScriptType {
13
13
  /**
14
14
  * @attribute
15
- * Speed determines how fast to rotate things"
15
+ * Speed determines how fast to rotate things
16
16
  */
17
- speed = new Vec3()
18
-
19
- /**
20
- * @attribute
21
- * An array of Entities to rotate
22
- *
23
- * @type {Entity[]}
24
- */
25
- thingsToRotate
17
+ speed = new Vec3();
18
+
19
+ /**
20
+ * @attribute
21
+ * An array of Entities to rotate
22
+ *
23
+ * @type {Entity[]}
24
+ */
25
+ thingsToRotate;
26
26
  }
27
27
  ```
28
28
 
29
- and turns it into the following data ...
29
+ The parser outputs:
30
30
 
31
31
 
32
32
  ```json
33
33
  {
34
- "rotator": {
35
- "attributes": {
36
- "speed": {
37
- "type": "vec3",
38
- "name": "speed",
39
- "array": false,
40
- "description": "Speed determines how fast to rotate things\"",
41
- "default": [
42
- 0,
43
- 0,
44
- 0
45
- ]
46
- },
47
- "thingsToRotate": {
48
- "type": "entity",
49
- "name": "thingsToRotate",
50
- "array": true,
51
- "description": "An array of Entities to rotate",
52
- "default": null
53
- }
54
- },
55
- "errors": []
56
- }
34
+ "rotator": {
35
+ "attributes": {
36
+ "speed": {
37
+ "type": "vec3",
38
+ "name": "speed",
39
+ "array": false,
40
+ "description": "Speed determines how fast to rotate things",
41
+ "default": [0, 0, 0]
42
+ },
43
+ "thingsToRotate": {
44
+ "type": "entity",
45
+ "name": "thingsToRotate",
46
+ "array": true,
47
+ "description": "An array of Entities to rotate",
48
+ "default": null
49
+ }
50
+ },
51
+ "errors": []
52
+ }
57
53
  }
58
54
  ```
59
55
 
@@ -63,26 +59,26 @@ JSDocs tags are parsed and values and outputs the metadata in a serializable for
63
59
 
64
60
  ```javascript
65
61
  // Initialise the parser
62
+ // Initialize the parser
66
63
  const parser = new JSDocParser();
67
64
  await parser.init();
68
65
 
69
- // fetch your program source {[filename: string, contents: string][]}
66
+ // Load your source files: {[filename: string]: string}[]
70
67
  const scripts = await fetchScripts([...paths, './playcanvas.d.ts']);
71
68
 
72
- // update the progam
73
- parser.updateProgram(scripts)
69
+ // Update the parser program
70
+ parser.updateProgram(scripts);
74
71
 
75
- // Parse the program, starting from the first path
76
- const [attribute, errors] parser.parseAttributes("./index.js");
72
+ // Parse attributes from the entry point
73
+ const [attributes, errors] = parser.parseAttributes("./index.js");
77
74
  ```
78
75
 
79
76
  ### Attribute structure
80
77
 
81
- You can learn more about the special tags [available here ](https://github.com/playcanvas/attribute-parser/tree/main/test/fixtures)
82
-
78
+ See the [test fixtures](https://github.com/playcanvas/attribute-parser/tree/main/test/fixtures) for examples of supported JSDoc tags and output formats.
83
79
 
84
80
  ### Tests
85
81
 
86
- We provide good coverage
82
+ This project includes good test coverage for all supported tag formats and edge cases.
87
83
 
88
84
 
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
7
7
  },
8
- "version": "1.5.1",
8
+ "version": "1.6.0",
9
9
  "dependencies": {
10
10
  "@playcanvas/eslint-config": "^1.7.4 || ^2.0.0",
11
11
  "@typescript/vfs": "^1.6.0",
package/src/index.js CHANGED
@@ -143,9 +143,26 @@ export class JSDocParser {
143
143
 
144
144
  const esmScriptClass = pcTypes.statements.find(node => node.kind === ts.SyntaxKind.ClassDeclaration && node.name.text === 'Script')?.symbol;
145
145
 
146
+ const isScriptNameMember = member => (
147
+ ts.isPropertyDeclaration(member) && // Is a property declaration
148
+ ts.isIdentifier(member.name) &&
149
+ member.name.text === 'scriptName' &&
150
+ (ts.getCombinedModifierFlags(member) & ts.ModifierFlags.Static) !== 0 &&
151
+ ts.isStringLiteral(member.initializer)
152
+ );
153
+
146
154
  // Check if the file exports a class that inherits from `Script`
147
155
  nodes.forEach((node, name) => {
148
156
  if (isAliasedClassDeclaration(node, typeChecker) && inheritsFrom(node, typeChecker, esmScriptClass)) {
157
+
158
+ // Check for a static scriptName property and use that as the name if it exists
159
+ const scriptNameMember = node.members.find(isScriptNameMember);
160
+
161
+ // If the scriptName property exists, use that as the name
162
+ if (scriptNameMember) {
163
+ name = scriptNameMember.initializer.text;
164
+ }
165
+
149
166
  esmScripts.set(name, node);
150
167
  }
151
168
  });
@@ -46,10 +46,7 @@ export class AttributeParser {
46
46
  if (!attribute) return;
47
47
 
48
48
  // Extract the description from the summary section
49
- const description = node.jsDoc[0].comment;
50
- if (description) {
51
- attribute.description = description.split('.')[0];
52
- }
49
+ attribute.description = node.jsDoc?.[0]?.comment || '';
53
50
 
54
51
  // Extract metadata from custom tags
55
52
  const jsDocTags = node.jsDoc?.[0]?.tags ?? [];
@@ -186,6 +186,12 @@ const mapAttributesToOutput = (attribute) => {
186
186
  attribute.type = 'rgba';
187
187
  }
188
188
 
189
+ // 'resource' type needs to be mapped to 'assetType'
190
+ if (attribute.resource) {
191
+ attribute.assetType = attribute.resource;
192
+ delete attribute.resource;
193
+ }
194
+
189
195
  // remove enum if it's empty
190
196
  if (attribute.enum.length === 0) delete attribute.enum;
191
197
 
@@ -388,9 +394,9 @@ export class ScriptParser {
388
394
  Array.from(type.symbol.members).map(arr => arr[1].declarations[0]) ?? // a typedefs property declarations
389
395
  [];
390
396
 
391
- // If the type is an interface, we need to parse the attribute comments, if not, just the intitialized object
397
+ // If the type is an interface or an initialized object, we need to parse the attribute comments, if not, just the intitialized object
392
398
  let commentAttributes;
393
- if (typeIsInterface) {
399
+ if (typeIsInterface || isInitialized) {
394
400
  commentAttributes = this.extractAttributes(typeNode, { errors, requiresAttributeTag: false, depth: depth + 1 });
395
401
  }
396
402
 
@@ -209,6 +209,7 @@ function getSuperClasses(node, typeChecker) {
209
209
  export function getJSDocTags(node, typeChecker) {
210
210
  const tags = [];
211
211
 
212
+ // check if class interface of initialized object
212
213
  if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
213
214
  // get an array of the class an all parent classed
214
215
  const heritageChain = getSuperClasses(node, typeChecker)
@@ -240,6 +241,23 @@ export function getJSDocTags(node, typeChecker) {
240
241
  }
241
242
  });
242
243
  });
244
+ } else if (ts.isObjectLiteralExpression(node)) {
245
+ // Handle object literals
246
+ node.properties.forEach((property) => {
247
+ if (ts.isPropertyAssignment(property)) {
248
+ const memberName = property.name &&
249
+ (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ?
250
+ property.name.text :
251
+ 'unnamed';
252
+
253
+ if (property.jsDoc?.length > 0) {
254
+ tags.push({
255
+ memberName,
256
+ member: property
257
+ });
258
+ }
259
+ }
260
+ });
243
261
  }
244
262
 
245
263
  return tags;
@@ -6,6 +6,7 @@ import { Script, Vec3 } from 'playcanvas';
6
6
  // eslint-disable-next-line
7
7
  class Folder {
8
8
  /**
9
+ * This is some description of the attribute.
9
10
  * @attribute
10
11
  * @type {boolean}
11
12
  */
@@ -13,6 +14,7 @@ class Folder {
13
14
 
14
15
  /**
15
16
  * @attribute
17
+ * @range [10, 20]
16
18
  */
17
19
  b = 10;
18
20
 
@@ -64,7 +66,14 @@ class Example extends Script {
64
66
  /**
65
67
  * @attribute
66
68
  */
67
- i = { a: true, b: 10, c: 'hello', d: [new Vec3(1, 2, 3)] };
69
+ i = {
70
+ /** description of a */
71
+ a: true,
72
+ /** @range [10, 20] */
73
+ b: 10,
74
+ c: 'hello',
75
+ d: [new Vec3(1, 2, 3)]
76
+ };
68
77
 
69
78
  /**
70
79
  * @typedef {object} TypeDefFolder
@@ -2,6 +2,7 @@ import { Script, Vec3 } from 'playcanvas';
2
2
 
3
3
  interface Folder {
4
4
  /**
5
+ * This is some description of the attribute.
5
6
  * @attribute
6
7
  */
7
8
  a: boolean;
@@ -9,6 +10,7 @@ interface Folder {
9
10
  /**
10
11
  * @attribute
11
12
  * @default 10
13
+ * @range [10, 20]
12
14
  */
13
15
  b: number;
14
16
 
@@ -54,7 +56,14 @@ class Example extends Script {
54
56
  /**
55
57
  * @attribute
56
58
  */
57
- i = { a: true, b: 10, c: 'hello', d: [new Vec3(1, 2, 3)] };
59
+ i = {
60
+ /** description of a */
61
+ a: true,
62
+ /** @range [10, 20] */
63
+ b: 10,
64
+ c: 'hello',
65
+ d: [new Vec3(1, 2, 3)]
66
+ };
58
67
 
59
68
  /**
60
69
  * @attribute
@@ -2,7 +2,9 @@ import { Script } from 'playcanvas';
2
2
 
3
3
  class Example extends Script {
4
4
  /**
5
- * This is a description of the attribute.
5
+ * This is a description of the attribute. It can be multi-sentence.
6
+ *
7
+ * And multi-line too
6
8
  *
7
9
  * @attribute
8
10
  * @type {number}
@@ -6,6 +6,8 @@ import { Script } from 'playcanvas';
6
6
  export const MyEnum = { value: 0 };
7
7
 
8
8
  class Example extends Script {
9
+ static scriptName = 'customExample';
10
+
9
11
  /**
10
12
  * @attribute
11
13
  * @precision 1
@@ -26,7 +26,7 @@ describe('INVALID: Asset attribute', function () {
26
26
  expect(data[0].example.attributes.a.name).to.equal('a');
27
27
  expect(data[0].example.attributes.a.type).to.equal('asset');
28
28
  expect(data[0].example.attributes.a.default).to.not.exist;
29
- expect(data[0].example.attributes.a.resource).to.not.exist;
29
+ expect(data[0].example.attributes.a.assetType).to.not.exist;
30
30
  });
31
31
 
32
32
  it('b: should fallback to an asset attribute without resource', function () {
@@ -34,7 +34,7 @@ describe('INVALID: Asset attribute', function () {
34
34
  expect(data[0].example.attributes.b.name).to.equal('b');
35
35
  expect(data[0].example.attributes.b.type).to.equal('asset');
36
36
  expect(data[0].example.attributes.b.default).to.not.exist;
37
- expect(data[0].example.attributes.b.resource).to.not.exist;
37
+ expect(data[0].example.attributes.b.assetType).to.not.exist;
38
38
  });
39
39
 
40
40
  it('c: should fallback to an asset attribute without resource', function () {
@@ -42,6 +42,6 @@ describe('INVALID: Asset attribute', function () {
42
42
  expect(data[0].example.attributes.c.name).to.equal('c');
43
43
  expect(data[0].example.attributes.c.type).to.equal('asset');
44
44
  expect(data[0].example.attributes.c.default).to.not.exist;
45
- expect(data[0].example.attributes.c.resource).to.not.exist;
45
+ expect(data[0].example.attributes.c.assetType).to.not.exist;
46
46
  });
47
47
  });
@@ -40,7 +40,7 @@ function runTests(fileName) {
40
40
  expect(data[0].example.attributes.b.type).to.equal('asset');
41
41
  expect(data[0].example.attributes.b.array).to.equal(false);
42
42
  expect(data[0].example.attributes.b.default).to.equal(null);
43
- expect(data[0].example.attributes.b.resource).to.equal('texture');
43
+ expect(data[0].example.attributes.b.assetType).to.equal('texture');
44
44
  });
45
45
 
46
46
  it('c: should be a asset attribute array with a container resource', function () {
@@ -49,7 +49,7 @@ function runTests(fileName) {
49
49
  expect(data[0].example.attributes.c.type).to.equal('asset');
50
50
  expect(data[0].example.attributes.c.array).to.equal(true);
51
51
  expect(data[0].example.attributes.c.default).to.equal(null);
52
- expect(data[0].example.attributes.c.resource).to.equal('container');
52
+ expect(data[0].example.attributes.c.assetType).to.equal('container');
53
53
  });
54
54
 
55
55
  });
@@ -37,12 +37,15 @@ describe('JS: VALID: Asset attribute', function () {
37
37
  expect(data[0].example.attributes.f.schema[0].type).to.equal('boolean');
38
38
  expect(data[0].example.attributes.f.schema[0].array).to.equal(false);
39
39
  expect(data[0].example.attributes.f.schema[0].default).to.equal(false);
40
+ expect(data[0].example.attributes.f.schema[0].description).to.equal('This is some description of the attribute.');
40
41
  });
41
42
 
42
43
  it('f[b]: should be a numeric attribute', function () {
43
44
  expect(data[0].example.attributes.f.schema[1].name).to.equal('b');
44
45
  expect(data[0].example.attributes.f.schema[1].type).to.equal('number');
45
46
  expect(data[0].example.attributes.f.schema[1].array).to.equal(false);
47
+ expect(data[0].example.attributes.f.schema[1].min).to.equal(10);
48
+ expect(data[0].example.attributes.f.schema[1].max).to.equal(20);
46
49
  expect(data[0].example.attributes.f.schema[1].default).to.equal(10);
47
50
  });
48
51
 
@@ -100,6 +103,22 @@ describe('JS: VALID: Asset attribute', function () {
100
103
  expect(data[0].example.attributes.i.schema).to.exist;
101
104
  });
102
105
 
106
+ it('i[a]: should be a boolean attribute', function () {
107
+ expect(data[0].example.attributes.i.schema[0].name).to.equal('a');
108
+ expect(data[0].example.attributes.i.schema[0].type).to.equal('boolean');
109
+ expect(data[0].example.attributes.i.schema[0].array).to.equal(false);
110
+ expect(data[0].example.attributes.i.schema[0].default).to.equal(true);
111
+ expect(data[0].example.attributes.i.schema[0].description).to.equal('description of a');
112
+ });
113
+
114
+ it('i[b]: should be a numeric attribute', function () {
115
+ expect(data[0].example.attributes.i.schema[1].name).to.equal('b');
116
+ expect(data[0].example.attributes.i.schema[1].type).to.equal('number');
117
+ expect(data[0].example.attributes.i.schema[1].array).to.equal(false);
118
+ expect(data[0].example.attributes.i.schema[1].min).to.equal(10);
119
+ expect(data[0].example.attributes.i.schema[1].max).to.equal(20);
120
+ });
121
+
103
122
  it('j: should be a typedef json attribute', function () {
104
123
  expect(data[0].example.attributes.j).to.exist;
105
124
  expect(data[0].example.attributes.j.name).to.equal('j');
@@ -184,6 +203,7 @@ describe('TS: VALID: Asset attribute', function () {
184
203
  expect(data[0].example.attributes.f.schema[0].type).to.equal('boolean');
185
204
  expect(data[0].example.attributes.f.schema[0].array).to.equal(false);
186
205
  expect(data[0].example.attributes.f.schema[0].default).to.equal(false);
206
+ expect(data[0].example.attributes.f.schema[0].description).to.equal('This is some description of the attribute.');
187
207
  });
188
208
 
189
209
  it('f[b]: should be a numeric attribute', function () {
@@ -191,6 +211,8 @@ describe('TS: VALID: Asset attribute', function () {
191
211
  expect(data[0].example.attributes.f.schema[1].type).to.equal('number');
192
212
  expect(data[0].example.attributes.f.schema[1].array).to.equal(false);
193
213
  expect(data[0].example.attributes.f.schema[1].default).to.equal(10);
214
+ expect(data[0].example.attributes.f.schema[1].min).to.equal(10);
215
+ expect(data[0].example.attributes.f.schema[1].max).to.equal(20);
194
216
  });
195
217
 
196
218
  it('f[c]: should be a string attribute', function () {
@@ -247,6 +269,22 @@ describe('TS: VALID: Asset attribute', function () {
247
269
  expect(data[0].example.attributes.i.schema).to.exist;
248
270
  });
249
271
 
272
+ it('i[a]: should be a boolean attribute', function () {
273
+ expect(data[0].example.attributes.i.schema[0].name).to.equal('a');
274
+ expect(data[0].example.attributes.i.schema[0].type).to.equal('boolean');
275
+ expect(data[0].example.attributes.i.schema[0].array).to.equal(false);
276
+ expect(data[0].example.attributes.i.schema[0].default).to.equal(true);
277
+ expect(data[0].example.attributes.i.schema[0].description).to.equal('description of a');
278
+ });
279
+
280
+ it('i[b]: should be a numeric attribute', function () {
281
+ expect(data[0].example.attributes.i.schema[1].name).to.equal('b');
282
+ expect(data[0].example.attributes.i.schema[1].type).to.equal('number');
283
+ expect(data[0].example.attributes.i.schema[1].array).to.equal(false);
284
+ expect(data[0].example.attributes.i.schema[1].min).to.equal(10);
285
+ expect(data[0].example.attributes.i.schema[1].max).to.equal(20);
286
+ });
287
+
250
288
  it('l: should be an inline type json attribute', function () {
251
289
  expect(data[0].example.attributes.l).to.exist;
252
290
  expect(data[0].example.attributes.l.name).to.equal('l');
@@ -28,7 +28,7 @@ describe('VALID: Misc attribute types', function () {
28
28
  expect(data[0].example.attributes.a.array).to.equal(false);
29
29
  expect(data[0].example.attributes.a.default).to.equal(0);
30
30
  expect(data[0].example.attributes.a.title).to.equal('A number');
31
- expect(data[0].example.attributes.a.description).to.equal('This is a description of the attribute');
31
+ expect(data[0].example.attributes.a.description).to.equal('This is a description of the attribute. It can be multi-sentence.\n\nAnd multi-line too');
32
32
  });
33
33
 
34
34
  it('b: should be a numeric attribute with a getter and setter', function () {
@@ -9,10 +9,15 @@ describe('VALID: Program ', function () {
9
9
  data = await parseAttributes('./program.valid.js');
10
10
  });
11
11
 
12
+ it('should have a customExample script', function () {
13
+ expect(data[0].customExample).to.exist;
14
+ expect(data[0].customExample.errors).to.be.empty;
15
+ });
16
+
12
17
  it('only results should exist', function () {
13
18
  expect(data).to.exist;
14
19
  expect(data[0]).to.not.be.empty;
15
20
  expect(data[1]).to.be.empty;
16
- expect(data[0].example.errors).to.be.empty;
21
+ expect(data[0].customExample.errors).to.be.empty;
17
22
  });
18
23
  });