@playcanvas/attribute-parser 1.3.2 → 1.4.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,9 +5,8 @@
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
7
7
  },
8
- "version": "1.3.2",
8
+ "version": "1.4.0",
9
9
  "dependencies": {
10
- "@microsoft/tsdoc": "^0.15.0",
11
10
  "@playcanvas/eslint-config": "^1.7.4 || ^2.0.0",
12
11
  "@typescript/vfs": "^1.6.0",
13
12
  "eslint": "^8.56.0 || ^9.0.0",
@@ -0,0 +1,33 @@
1
+ import commonjs from '@rollup/plugin-commonjs';
2
+ import resolve from '@rollup/plugin-node-resolve';
3
+ import swc from '@rollup/plugin-swc';
4
+
5
+ export default {
6
+ input: 'src/index.js',
7
+ treeshake: { moduleSideEffects: false },
8
+ // Mark 'typescript' as external, so it won’t get bundled
9
+ // external: ['typescript'],
10
+ output: {
11
+ dir: 'dist',
12
+ format: 'esm'
13
+ },
14
+ plugins: [
15
+ commonjs({
16
+ include: /node_modules/,
17
+ ignoreGlobal: true,
18
+ sourceMap: false
19
+ }),
20
+ resolve({
21
+ browser: true, // This instructs the plugin to resolve for the browser
22
+ preferBuiltins: false // Prefer the local versions of built-ins instead of Node.js versions
23
+ }),
24
+ swc({ swc: {
25
+ minify: true,
26
+ jsc: {
27
+ minify: {
28
+ compress: true
29
+ }
30
+ }
31
+ } })
32
+ ]
33
+ };
@@ -1,10 +1,9 @@
1
- import { TSDocConfiguration, TSDocTagDefinition, TSDocTagSyntaxKind, TextRange, TSDocParser } from '@microsoft/tsdoc';
2
1
  import * as ts from 'typescript';
3
2
 
4
3
  import { ParsingError } from './parsing-error.js';
5
4
  import { hasTag } from '../utils/attribute-utils.js';
6
5
  import { parseTag, validateTag } from '../utils/tag-utils.js';
7
- import { extractTextFromDocNode, getLeadingBlockCommentRanges, getLiteralValue, getType } from '../utils/ts-utils.js';
6
+ import { getLiteralValue, getType } from '../utils/ts-utils.js';
8
7
 
9
8
  /**
10
9
  * A class to parse JSDoc comments and extract attribute metadata.
@@ -25,44 +24,18 @@ export class AttributeParser {
25
24
  this.program = env.languageService.getProgram();
26
25
  this.typeChecker = this.program.getTypeChecker();
27
26
 
28
- this.attributeTag = new TSDocTagDefinition({
29
- tagName: '@attribute', syntaxKind: TSDocTagSyntaxKind.ModifierTag
30
- });
31
-
32
- this.typeTag = new TSDocTagDefinition({
33
- tagName: '@type', syntaxKind: TSDocTagSyntaxKind.BlockTag
34
- });
35
-
36
- const conf = new TSDocConfiguration();
37
- conf.validation.reportUnsupportedTags = false;
38
-
39
- conf.addTagDefinition(this.attributeTag);
40
-
41
- tags.forEach((_, tag) => {
42
- const tagDefinition = new TSDocTagDefinition({
43
- tagName: `@${tag}`, syntaxKind: TSDocTagSyntaxKind.BlockTag, allowMultiple: false
44
- });
45
- conf.addTagDefinition(tagDefinition);
46
- });
47
-
48
- this.parser = new TSDocParser(conf);
49
27
  }
50
28
 
51
29
  /**
52
30
  * Parses a JSdoc comment and extracts any associated metadata if the `@attribute`
53
31
  * modifier is used. Returns null if this is not an attribute comment.
54
32
  *
55
- * @param {TextRange} comment - The comment to parse
56
33
  * @param {ts.Node} node - The node to parse
57
34
  * @param {ParsingError[]} errors - An array to store any parsing errors
58
35
  * @param {boolean} requiresAttributeTag - Whether the comment must have an attribute tag
59
36
  * @returns {*} - The extracted metadata or null if no metadata was found
60
37
  */
61
- parseAttributeComment(comment, node, errors = [], requiresAttributeTag = true) {
62
-
63
- const parserContext = this.parser.parseRange(comment);
64
- const docComment = parserContext.docComment;
65
- const file = node.getSourceFile();
38
+ parseAttributeComment(node, errors = [], requiresAttributeTag = true) {
66
39
 
67
40
  // Check if the comment has an attribute tag
68
41
  if (!requiresAttributeTag || hasTag('attribute', node)) {
@@ -73,7 +46,7 @@ export class AttributeParser {
73
46
  if (!attribute) return;
74
47
 
75
48
  // Extract the description from the summary section
76
- const description = extractTextFromDocNode(docComment.summarySection);
49
+ const description = node.jsDoc[0].comment;
77
50
  if (description) {
78
51
  attribute.description = description.split('.')[0];
79
52
  }
@@ -96,6 +69,7 @@ export class AttributeParser {
96
69
  attribute[tag.tagName.text] = value;
97
70
 
98
71
  } catch (error) {
72
+ const file = node.getSourceFile();
99
73
  const { line, character } = file.getLineAndCharacterOfPosition(tag.getStart());
100
74
  const parseError = new ParsingError(tag, `Error (${line}, ${character}): Parsing Tag '@${tag.tagName.text} ${commentText}' - ${error.message}`);
101
75
  errors.push(parseError);
@@ -107,51 +81,6 @@ export class AttributeParser {
107
81
  }
108
82
  }
109
83
 
110
- /**
111
- * Returns the leading block comments for an typescript AST node.
112
- *
113
- * @param {import('typescript').Node} node - The node to parse
114
- * @param {string[]} errors - An array to store any parsing errors
115
- * @returns {import('@microsoft/tsdoc').DocComment} - The extracted doc comment;
116
- */
117
- getCommentsForNode(node, errors = []) {
118
- const fullText = node.getSourceFile().getFullText();
119
- const comments = getLeadingBlockCommentRanges(node, fullText);
120
-
121
- if (comments.length === 0) return null;
122
-
123
- const lastComment = comments[0];
124
- const textRange = TextRange.fromStringRange(fullText, lastComment.pos, lastComment.end);
125
-
126
- const parserContext = this.parser.parseRange(textRange);
127
- const docComment = parserContext.docComment;
128
-
129
- // Check for comment parsing errors and flag them
130
- this.populateErrors(node, parserContext, errors);
131
-
132
- return docComment;
133
- }
134
-
135
- /**
136
- * Populates the errors array with any parsing errors found in the parser context.
137
- *
138
- * @param {import('typescript').Node} node - The node to parse
139
- * @param {import('@microsoft/tsdoc').ParserContext} parserContext - The parser context to check for errors
140
- * @param {string[]} errors - An array to store any parsing errors
141
- */
142
- populateErrors(node, parserContext, errors) {
143
- if (parserContext.log.messages.length > 0) {
144
- const sourceFile = node.getSourceFile();
145
-
146
- const formattedErrors = parserContext.log.messages.map((message) => {
147
- const location = sourceFile.getLineAndCharacterOfPosition(message.textRange.pos);
148
- return `${sourceFile.fileName}(${location.line + 1},${location.character + 1}): [TSDoc] ${message}`;
149
- });
150
-
151
- errors.push(...formattedErrors);
152
- }
153
- }
154
-
155
84
  /**
156
85
  * Extract the public members and their values from a TypeScript node.
157
86
  *
@@ -1,11 +1,10 @@
1
- import { TextRange } from '@microsoft/tsdoc';
2
1
  import * as ts from 'typescript';
3
2
 
4
3
  import { AttributeParser } from './attribute-parser.js';
5
4
  import { ParsingError } from './parsing-error.js';
6
5
  import { hasTag } from '../utils/attribute-utils.js';
7
6
  import { zipArrays } from '../utils/generic-utils.js';
8
- import { flatMapAnyNodes, getJSDocCommentRanges, getLiteralValue, parseArrayLiteral, parseFloatNode } from '../utils/ts-utils.js';
7
+ import { flatMapAnyNodes, getJSDocTags, getLiteralValue, parseArrayLiteral, parseFloatNode } from '../utils/ts-utils.js';
9
8
 
10
9
  /**
11
10
  * @typedef {object} Attribute
@@ -148,7 +147,9 @@ const SUPPORTED_BLOCK_TAGS = new Map([
148
147
  ['size', 'number'],
149
148
  ['step', 'number'],
150
149
  ['title', 'string'],
151
- ['default', 'any']
150
+ ['default', 'any'],
151
+ ['enabledif', 'any'],
152
+ ['visibleif', 'any']
152
153
  ]);
153
154
 
154
155
  /**
@@ -203,6 +204,16 @@ const mapAttributesToOutput = (attribute) => {
203
204
  delete attribute.default;
204
205
  }
205
206
 
207
+ // enabledif is in a {}, so strip it out
208
+ if (attribute.enabledif?.startsWith('{') && attribute.enabledif?.endsWith('}')) {
209
+ attribute.enabledif = attribute.enabledif.slice(1, -1);
210
+ }
211
+
212
+ // enabledif is in a {}, so strip it out
213
+ if (attribute.visibleif?.startsWith('{') && attribute.visibleif?.endsWith('}')) {
214
+ attribute.visibleif = attribute.visibleif.slice(1, -1);
215
+ }
216
+
206
217
  // remove typeName from the output
207
218
  delete attribute.typeName;
208
219
  delete attribute.value;
@@ -322,17 +333,14 @@ export class ScriptParser {
322
333
  }
323
334
 
324
335
  // Find "/** */" style comments associated with this node.
325
- const comments = getJSDocCommentRanges(node, this.typeChecker);
336
+ const members = getJSDocTags(node, this.typeChecker);
326
337
 
327
338
  // Parse the comments for attribute metadata
328
- for (const comment of comments) {
329
-
330
- const memberFileText = comment.member.getSourceFile().getFullText();
339
+ for (const { member, memberName } of members) {
331
340
 
332
341
  // Parse the comment for attribute metadata
333
342
  const attributeMetadata = this.attributeParser.parseAttributeComment(
334
- TextRange.fromStringRange(memberFileText, comment.range.pos, comment.range.end),
335
- comment.member,
343
+ member,
336
344
  errors,
337
345
  requiresAttributeTag
338
346
  );
@@ -341,11 +349,11 @@ export class ScriptParser {
341
349
  if (attributeMetadata) {
342
350
 
343
351
  const { type, typeName } = attributeMetadata;
344
- const initializer = comment.member?.initializer;
352
+ const initializer = member?.initializer;
345
353
 
346
354
  if (typeName === 'any') {
347
355
 
348
- const error = new ParsingError(comment.member, `The attribute "${comment.memberName}" is not initialized and does not have a @type tag.`);
356
+ const error = new ParsingError(member, `The attribute "${memberName}" is not initialized and does not have a @type tag.`);
349
357
  errors.push(error);
350
358
  continue;
351
359
 
@@ -369,7 +377,7 @@ export class ScriptParser {
369
377
 
370
378
  // Check if the type is a valid interface
371
379
  if (!isInitialized && !typeIsInterface && !typeIsJSDocTypeDef && !isJSDocTypeLiteral) {
372
- const error = new ParsingError(comment.member, `Attribute "${comment.memberName}" is an invalid type.`);
380
+ const error = new ParsingError(member, `Attribute "${memberName}" is an invalid type.`);
373
381
  errors.push(error);
374
382
  continue;
375
383
  }
@@ -408,7 +416,7 @@ export class ScriptParser {
408
416
  }
409
417
 
410
418
  // Add the attribute to the list of found attributes
411
- attributes[comment.memberName] = mapAttributesToOutput(attributeMetadata);
419
+ attributes[memberName] = mapAttributesToOutput(attributeMetadata);
412
420
 
413
421
  }
414
422
  }
@@ -4,10 +4,10 @@
4
4
 
5
5
 
6
6
  /**
7
- * Returns a tsdoc tag from a JSDoc comment.
7
+ * Returns a jsdoc tag from a JSDoc comment.
8
8
  * @param {string} tag - The tag to search for
9
9
  * @param {import('typescript').Node} doc - The JSDoc comment node
10
- * @returns {import('@microsoft/tsdoc').DocNode | null} - The tag node
10
+ * @returns {import('typescript').Node} | null} - The tag node
11
11
  */
12
12
  export const getTagFromJsdoc = (tag, doc) => {
13
13
  return doc?.tags?.find(doc => doc.tagName.text === tag);
@@ -42,29 +42,3 @@ export const hasTag = (tag, node) => {
42
42
  export const isInterface = (node) => {
43
43
  return hasTag('interface', node);
44
44
  };
45
-
46
- // /**
47
- // * Parses a type tag in the format "{type}".
48
- // * @param {import('@microsoft/tsdoc').DocSection} content - The content of the tag.
49
- // * @param {object} metadata - An object to store the extracted metadata.
50
- // * @param {{ node: import('typescript').Node, env: tsdoc.Environment, errors: string[] }} opts - Additional parser options
51
- // * @returns {{ type: import('typescript').Type | null, array: boolean | null }} - The extracted type and array flag.
52
- // */
53
- // export function parseTypeTag(content, metadata, { node, env, errors }) {
54
-
55
- // // Extract the full source text of the original file
56
- // const originalSourceText = node.getSourceFile().getFullText();
57
- // const typeString = extractTextFromDocNode(content);
58
- // const sourceText = `${originalSourceText}\nlet a: ${typeString};`;
59
-
60
- // env.createFile("/virtual.ts", sourceText);
61
-
62
- // const updatedProgram = env.languageService.getProgram();
63
- // const newTypeChecker = updatedProgram.getTypeChecker();
64
-
65
- // const sourceFile = updatedProgram.getSourceFile('/virtual.ts');
66
-
67
- // const variableStatement = sourceFile.statements[sourceFile.statements.length - 1];
68
- // const variableDeclaration = variableStatement.declarationList.declarations[0];
69
- // return getType(variableDeclaration, newTypeChecker);
70
- // }
@@ -1,4 +1,3 @@
1
- import { DocNodeKind } from '@microsoft/tsdoc';
2
1
  import { knownLibFilesForCompilerOptions } from '@typescript/vfs';
3
2
  import * as ts from 'typescript';
4
3
 
@@ -205,10 +204,10 @@ function getSuperClasses(node, typeChecker) {
205
204
  * @param {import('typescript').Node} node - The TypeScript node to analyze
206
205
  * @param {string} text - The full text content of the source file
207
206
  * @param {import('typescript').TypeChecker} typeChecker - The TypeScript type checker
208
- * @returns {{ memberName: string, member: ts.Node, range: import('typescript').CommentRange}[]} - An array of comment ranges
207
+ * @returns {{ memberName: string, member: Node, range: import('typescript').CommentRange}[]} - An array of comment ranges
209
208
  */
210
- export function getJSDocCommentRanges(node, typeChecker) {
211
- const commentRanges = [];
209
+ export function getJSDocTags(node, typeChecker) {
210
+ const tags = [];
212
211
 
213
212
  if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
214
213
  // get an array of the class an all parent classed
@@ -221,12 +220,11 @@ export function getJSDocCommentRanges(node, typeChecker) {
221
220
  classNode.members.forEach((member) => {
222
221
  if (ts.isPropertyDeclaration(member) || ts.isSetAccessor(member) || ts.isPropertySignature(member)) {
223
222
  const memberName = member.name && ts.isIdentifier(member.name) ? member.name.text : 'unnamed';
224
- const ranges = getLeadingBlockCommentRanges(member, member.getSourceFile().getFullText());
225
- if (ranges.length > 0) {
226
- commentRanges.push({
223
+ const hasJsDoc = member.jsDoc?.length > 0;
224
+ if (hasJsDoc) {
225
+ tags.push({
227
226
  memberName,
228
- member,
229
- range: ranges[0]
227
+ member
230
228
  });
231
229
  }
232
230
  }
@@ -234,62 +232,7 @@ export function getJSDocCommentRanges(node, typeChecker) {
234
232
  });
235
233
  }
236
234
 
237
- return commentRanges;
238
- }
239
-
240
- const ASTERISK = 42;
241
- const SLASH = 47;
242
-
243
- /**
244
- * Retrieves the JSDoc-style comments blocks associated with a specific AST node.
245
- * @param {import('typescript').Node} node - The TypeScript node to analyze
246
- * @param {string} text - The full text content of the source file
247
- * @returns {import('typescript').CommentRange[]} - An array of comment ranges
248
- */
249
- export function getLeadingBlockCommentRanges(node, text) {
250
- const ranges = ts.getLeadingCommentRanges(text, node.getFullStart()) ?? [];
251
- return ranges.filter(({ pos }) => {
252
- return (
253
- text.charCodeAt(pos + 1) === ASTERISK &&
254
- text.charCodeAt(pos + 2) === ASTERISK &&
255
- text.charCodeAt(pos + 3) !== SLASH
256
- );
257
- });
258
- }
259
-
260
- /**
261
- * Recursively extract text content from a DocNode.
262
- * @param {tsdoc.DocNode} node - The DocNode from which to extract text.
263
- * @returns {string} - The concatenated string of all text content.
264
- */
265
- export function extractTextFromDocNode(node) {
266
- // Check if the current node is a PlainText node
267
- if (node.kind === DocNodeKind.PlainText) {
268
- return node.text.trim(); // Cast and return as we found a PlainText node
269
- }
270
-
271
- // If not, recursively search its children
272
- for (const child of node.getChildNodes()) {
273
- const result = extractTextFromDocNode(child);
274
- if (result !== null) {
275
- return result; // Return the first PlainText node found in the subtree
276
- }
277
- }
278
-
279
- return null;
280
- }
281
-
282
- /**
283
- * Extracts all imports from a TypeScript source file.
284
- * @param {ts.Node} node - The TypeScript node to analyze
285
- * @returns {string[]} - An array of import statements
286
- */
287
- export function getLeadingComments(node) {
288
- const fullText = node.getFullText();
289
- const commentRanges = ts.getLeadingCommentRanges(fullText, 0);
290
-
291
- if (!commentRanges) return [];
292
- return commentRanges.map(range => fullText.slice(range.pos, range.end));
235
+ return tags;
293
236
  }
294
237
 
295
238
  /**
@@ -343,7 +286,7 @@ export function getPrimitiveEnumType(type, typeChecker) {
343
286
  *
344
287
  * @param {ts.Node} node - The TypeScript node to analyze
345
288
  * @param {ts.TypeChecker} typeChecker - The TypeScript type checker
346
- * @returns {{ array: boolean, name: string | undefined, type: ts.Type | null }} - The inferred type of the node
289
+ * @returns {{ array: boolean, name: string | undefined, type: Type | null }} - The inferred type of the node
347
290
  */
348
291
  export function getType(node, typeChecker) {
349
292
  if (node?.name?.kind === ts.SyntaxKind.Identifier) {
@@ -565,11 +508,11 @@ const evaluatePrefixUnaryExpression = (node, typeChecker) => {
565
508
  function handleObjectLiteral(node, typeChecker) {
566
509
  const obj = {};
567
510
  node.properties.forEach((prop) => {
568
- if (ts.isPropertyAssignment(prop)) {
511
+ if (ts.isPropertyAssignment(prop)) {
569
512
  const key = prop.name.getText();
570
513
  const value = getLiteralValue(prop.initializer, typeChecker);
571
514
  obj[key] = value;
572
- } else if (ts.isShorthandPropertyAssignment(prop)) {
515
+ } else if (ts.isShorthandPropertyAssignment(prop)) {
573
516
  const key = prop.name.getText();
574
517
  const value = resolveIdentifier(prop.name, typeChecker);
575
518
  obj[key] = value;
@@ -632,7 +575,7 @@ export function getLiteralValue(node, typeChecker) {
632
575
  */
633
576
  export const parseStringNode = (node) => {
634
577
  if (node.kind !== ts.SyntaxKind.StringLiteral) {
635
- if (ts.isPropertyAssignment(node.parent) || ts.isPropertyDeclaration(node.parent)) {
578
+ if (ts.isPropertyAssignment(node.parent) || ts.isPropertyDeclaration(node.parent)) {
636
579
  throw new ParsingError(node, `"${node.parent.getText()}" must be defined with a string literal.`);
637
580
  }
638
581
  return '';
@@ -642,7 +585,7 @@ export const parseStringNode = (node) => {
642
585
 
643
586
  export const parseArrayLiteralNode = (node, isArgument = false) => {
644
587
  if (node.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
645
- if (ts.isPropertyAssignment(node.parent) || ts.isPropertyDeclaration(node.parent) || isArgument) {
588
+ if (ts.isPropertyAssignment(node.parent) || ts.isPropertyDeclaration(node.parent) || isArgument) {
646
589
  throw new ParsingError(node, `"${node.parent.getText()}" must be defined with an array literal.`);
647
590
  }
648
591
  return [];
@@ -0,0 +1,22 @@
1
+ import { Script } from 'playcanvas';
2
+
3
+ class Example extends Script {
4
+ /**
5
+ * @attribute
6
+ * @visibleif {b === 2}
7
+ */
8
+ a = 1;
9
+
10
+ /**
11
+ * @attribute
12
+ * @enabledif {a === 3}
13
+ */
14
+ b = 1;
15
+
16
+ /**
17
+ * @attribute
18
+ */
19
+ c = 1;
20
+ }
21
+
22
+ export { Example };
@@ -0,0 +1,45 @@
1
+ import { expect } from 'chai';
2
+ import { describe, it, before } from 'mocha';
3
+
4
+ import { parseAttributes } from '../../utils.js';
5
+
6
+ describe('VALID: Conditional Tags', function () {
7
+ let data;
8
+ before(async function () {
9
+ data = await parseAttributes('./conditional.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 conditional `@visibleif` tag', function () {
25
+ expect(data[0].example.attributes.a).exist;
26
+ expect(data[0].example.attributes.a.name).to.equal('a');
27
+ expect(data[0].example.attributes.a.type).to.equal('number');
28
+ expect(data[0].example.attributes.a.visibleif).to.equal('b === 2');
29
+ });
30
+
31
+ it('b: should conditional `@enabledif` tag', function () {
32
+ expect(data[0].example.attributes.b).exist;
33
+ expect(data[0].example.attributes.b.name).to.equal('b');
34
+ expect(data[0].example.attributes.b.type).to.equal('number');
35
+ expect(data[0].example.attributes.b.enabledif).to.equal('a === 3');
36
+ });
37
+
38
+ it('c: should not include `@enabledif` or `@visibleif` tag if none is declared', function () {
39
+ expect(data[0].example.attributes.c).exist;
40
+ expect(data[0].example.attributes.c.name).to.equal('c');
41
+ expect(data[0].example.attributes.c.type).to.equal('number');
42
+ expect(data[0].example.attributes.c.enabledif).to.not.exist;
43
+ expect(data[0].example.attributes.c.visibleif).to.not.exist;
44
+ });
45
+ });