@playcanvas/attribute-parser 1.6.1 → 1.7.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/LICENSE.md +19 -0
- package/dist/index.js +194566 -0
- package/dist/libs.d.ts +50978 -0
- package/package.json +49 -9
- package/types/index.d.ts +55 -0
- package/types/parsers/attribute-parser.d.ts +55 -0
- package/types/parsers/parsing-error.d.ts +6 -0
- package/types/parsers/script-parser.d.ts +124 -0
- package/types/utils/attribute-utils.d.ts +5 -0
- package/{src/utils/generic-utils.js → types/utils/generic-utils.d.ts} +1 -10
- package/types/utils/tag-utils.d.ts +16 -0
- package/types/utils/ts-utils.d.ts +103 -0
- package/.github/workflows/ci.yml +0 -49
- package/eslint.config.mjs +0 -35
- package/renovate.json +0 -25
- package/src/parsers/attribute-parser.js +0 -197
- package/src/parsers/parsing-error.js +0 -14
- package/src/parsers/script-parser.js +0 -432
- package/src/utils/attribute-utils.js +0 -53
- package/src/utils/tag-utils.js +0 -73
- package/src/utils/ts-utils.js +0 -634
- package/test/fixtures/asset.invalid.js +0 -27
- package/test/fixtures/asset.invalid.ts +0 -24
- package/test/fixtures/asset.valid.js +0 -26
- package/test/fixtures/asset.valid.ts +0 -21
- package/test/fixtures/checkbox.valid.js +0 -23
- package/test/fixtures/color.valid.js +0 -23
- package/test/fixtures/conditional.valid.js +0 -22
- package/test/fixtures/curve.invalid.js +0 -29
- package/test/fixtures/curve.valid.js +0 -49
- package/test/fixtures/empty.valid.js +0 -0
- package/test/fixtures/entity.valid.js +0 -23
- package/test/fixtures/enum.valid.js +0 -66
- package/test/fixtures/enum.valid.ts +0 -50
- package/test/fixtures/example.dep.js +0 -9
- package/test/fixtures/export.import.js +0 -8
- package/test/fixtures/export.valid.js +0 -26
- package/test/fixtures/inherit.valid.js +0 -45
- package/test/fixtures/intellisense.valid.js +0 -40
- package/test/fixtures/json.invalid.js +0 -23
- package/test/fixtures/json.valid.js +0 -115
- package/test/fixtures/json.valid.ts +0 -81
- package/test/fixtures/misc.invalid.js +0 -24
- package/test/fixtures/misc.valid.js +0 -51
- package/test/fixtures/numeric.invalid.js +0 -38
- package/test/fixtures/numeric.valid.js +0 -63
- package/test/fixtures/playcanvas.d.ts +0 -47963
- package/test/fixtures/program.invalid.js +0 -12
- package/test/fixtures/program.valid.js +0 -27
- package/test/fixtures/text.valid.js +0 -31
- package/test/fixtures/vector.invalid.js +0 -14
- package/test/fixtures/vector.valid.js +0 -24
- package/test/tests/invalid/asset.test.js +0 -47
- package/test/tests/invalid/curve.test.js +0 -23
- package/test/tests/invalid/json.test.js +0 -27
- package/test/tests/invalid/misc.test.js +0 -41
- package/test/tests/invalid/numeric.test.js +0 -55
- package/test/tests/invalid/program.test.js +0 -17
- package/test/tests/invalid/vector.test.js +0 -28
- package/test/tests/valid/asset.test.js +0 -59
- package/test/tests/valid/checkbox.test.js +0 -57
- package/test/tests/valid/color.test.js +0 -49
- package/test/tests/valid/conditional.test.js +0 -45
- package/test/tests/valid/curve.test.js +0 -76
- package/test/tests/valid/empty.test.js +0 -17
- package/test/tests/valid/entity.test.js +0 -49
- package/test/tests/valid/enum.test.js +0 -79
- package/test/tests/valid/export.test.js +0 -67
- package/test/tests/valid/inherit.test.js +0 -110
- package/test/tests/valid/intellisense.test.js +0 -39
- package/test/tests/valid/json.test.js +0 -315
- package/test/tests/valid/misc.test.js +0 -54
- package/test/tests/valid/numeric.test.js +0 -103
- package/test/tests/valid/program.test.js +0 -23
- package/test/tests/valid/text.test.js +0 -58
- package/test/tests/valid/vector.test.js +0 -53
- package/test/utils.js +0 -54
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript';
|
|
2
|
-
|
|
3
|
-
import { ParsingError } from './parsing-error.js';
|
|
4
|
-
import { hasTag } from '../utils/attribute-utils.js';
|
|
5
|
-
import { parseTag, validateTag } from '../utils/tag-utils.js';
|
|
6
|
-
import { getLiteralValue, getType } from '../utils/ts-utils.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* A class to parse JSDoc comments and extract attribute metadata.
|
|
10
|
-
*/
|
|
11
|
-
export class AttributeParser {
|
|
12
|
-
/**
|
|
13
|
-
* Creates a new instance of the ScriptParser class.
|
|
14
|
-
*
|
|
15
|
-
* @param {Map<string, string>} tags - An set of tag definitions and their validators to add to the parser
|
|
16
|
-
* @param {object} env - The TypeScript environment to use
|
|
17
|
-
* @param {Map<string, Function>} typeSerializerMap - A map of custom serializers
|
|
18
|
-
*/
|
|
19
|
-
constructor(tags = new Map(), env, typeSerializerMap) {
|
|
20
|
-
|
|
21
|
-
this.env = env;
|
|
22
|
-
this.tagTypeAnnotations = tags;
|
|
23
|
-
this.typeSerializerMap = typeSerializerMap;
|
|
24
|
-
this.program = env.languageService.getProgram();
|
|
25
|
-
this.typeChecker = this.program.getTypeChecker();
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Parses a JSdoc comment and extracts any associated metadata if the `@attribute`
|
|
31
|
-
* modifier is used. Returns null if this is not an attribute comment.
|
|
32
|
-
*
|
|
33
|
-
* @param {ts.Node} node - The node to parse
|
|
34
|
-
* @param {ParsingError[]} errors - An array to store any parsing errors
|
|
35
|
-
* @param {boolean} requiresAttributeTag - Whether the comment must have an attribute tag
|
|
36
|
-
* @returns {*} - The extracted metadata or null if no metadata was found
|
|
37
|
-
*/
|
|
38
|
-
parseAttributeComment(node, errors = [], requiresAttributeTag = true) {
|
|
39
|
-
|
|
40
|
-
// Check if the comment has an attribute tag
|
|
41
|
-
if (!requiresAttributeTag || hasTag('attribute', node)) {
|
|
42
|
-
|
|
43
|
-
// Fetch the primary attribute metadata
|
|
44
|
-
const attribute = this.getNodeAsAttribute(node, errors);
|
|
45
|
-
|
|
46
|
-
if (!attribute) return;
|
|
47
|
-
|
|
48
|
-
// Extract the description from the summary section
|
|
49
|
-
attribute.description = node.jsDoc?.[0]?.comment || '';
|
|
50
|
-
|
|
51
|
-
// Extract metadata from custom tags
|
|
52
|
-
const jsDocTags = node.jsDoc?.[0]?.tags ?? [];
|
|
53
|
-
jsDocTags.forEach((tag) => {
|
|
54
|
-
let commentText = tag.comment;
|
|
55
|
-
|
|
56
|
-
// Check if the tag is a supported tag
|
|
57
|
-
if (this.tagTypeAnnotations.has(tag.tagName.text)) {
|
|
58
|
-
try {
|
|
59
|
-
const value = parseTag(commentText);
|
|
60
|
-
const tagTypeAnnotation = this.tagTypeAnnotations.get(tag.tagName.text);
|
|
61
|
-
|
|
62
|
-
// Tags like @resource with string values do not need quotations, so we need to manually add them
|
|
63
|
-
if (typeof value === 'string') commentText = `"${commentText}"`;
|
|
64
|
-
|
|
65
|
-
validateTag(commentText, tagTypeAnnotation, this.env);
|
|
66
|
-
attribute[tag.tagName.text] = value;
|
|
67
|
-
|
|
68
|
-
} catch (error) {
|
|
69
|
-
const file = node.getSourceFile();
|
|
70
|
-
const { line, character } = file.getLineAndCharacterOfPosition(tag.getStart());
|
|
71
|
-
const parseError = new ParsingError(tag, `Error (${line}, ${character}): Parsing Tag '@${tag.tagName.text} ${commentText}' - ${error.message}`);
|
|
72
|
-
errors.push(parseError);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
return attribute;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Extract the public members and their values from a TypeScript node.
|
|
83
|
-
*
|
|
84
|
-
* @param {ts.Node} node - The node to parse
|
|
85
|
-
* @param {ParsingError[]} errors - An array to store any parsing errors
|
|
86
|
-
* @returns {*} - The extracted members
|
|
87
|
-
*/
|
|
88
|
-
getNodeAsAttribute(node, errors = []) {
|
|
89
|
-
|
|
90
|
-
const name = node.name && ts.isIdentifier(node.name) && node.name.text;
|
|
91
|
-
const { type, name: typeName, array } = getType(node, this.typeChecker);
|
|
92
|
-
const enums = this.getEnumMembers(node, errors);
|
|
93
|
-
let value = null;
|
|
94
|
-
|
|
95
|
-
// we don't need to serialize the value for arrays
|
|
96
|
-
const serializer = !array && this.typeSerializerMap.get(typeName);
|
|
97
|
-
if (serializer) {
|
|
98
|
-
try {
|
|
99
|
-
value = serializer(node.initializer ?? node, this.typeChecker);
|
|
100
|
-
} catch (error) {
|
|
101
|
-
errors.push(error);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return { type, name, typeName, array, value, enum: enums };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Extracts the members of an enum from a TypeScript node.
|
|
111
|
-
*
|
|
112
|
-
* @param {import('typescript').Node} node - The TypeScript node to analyze
|
|
113
|
-
* @param {ParsingError[]} errors - An array to store any parsing errors
|
|
114
|
-
* @returns {Object<string, *>[]} - An array of enum members
|
|
115
|
-
*/
|
|
116
|
-
getEnumMembers(node, errors) {
|
|
117
|
-
let typeNode = null;
|
|
118
|
-
let members = [];
|
|
119
|
-
|
|
120
|
-
// Check if there's a type annotation directly on the variable declaration
|
|
121
|
-
if (node.type) {
|
|
122
|
-
typeNode = node;
|
|
123
|
-
} else {
|
|
124
|
-
// Check for JSDoc annotations
|
|
125
|
-
const jsDocs = ts.getJSDocTags(node);
|
|
126
|
-
const typeTag = jsDocs.find(doc => doc.tagName.text === 'type');
|
|
127
|
-
if (typeTag && ts.isJSDocTypeTag(typeTag)) {
|
|
128
|
-
typeNode = typeTag.typeExpression;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Also consider the elementType
|
|
133
|
-
const type = typeNode && (typeNode.type.elementType ?? typeNode.type);
|
|
134
|
-
|
|
135
|
-
if (typeNode && ts.isTypeReferenceNode(type)) {
|
|
136
|
-
|
|
137
|
-
// resolve the symbol of the type
|
|
138
|
-
let symbol = this.typeChecker.getSymbolAtLocation(type.typeName);
|
|
139
|
-
|
|
140
|
-
// Resolve aliases, which are common with imports
|
|
141
|
-
if (symbol && symbol.flags & ts.SymbolFlags.Alias) {
|
|
142
|
-
symbol = this.typeChecker.getAliasedSymbol(symbol);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (symbol) {
|
|
146
|
-
const declarations = symbol.getDeclarations();
|
|
147
|
-
|
|
148
|
-
if (declarations && declarations.length > 0) {
|
|
149
|
-
|
|
150
|
-
declarations.forEach((declaration) => {
|
|
151
|
-
|
|
152
|
-
// Check if the declaration is a TypeScript enum
|
|
153
|
-
if (ts.isEnumDeclaration(declaration)) {
|
|
154
|
-
members = declaration.members.map(member => ({ [member.name.text]: member.initializer.text }));
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Additionally check for JSDoc enum tag
|
|
158
|
-
const jsDocs = ts.getJSDocTags(declaration);
|
|
159
|
-
const enumTag = jsDocs.find(doc => doc.tagName.text === 'enum');
|
|
160
|
-
if (enumTag && ts.isJSDocEnumTag(enumTag)) {
|
|
161
|
-
// This assumes enum members are correctly annotated in JSDoc comments
|
|
162
|
-
// Example: @enum {type} EnumName { Member1, Member2 }
|
|
163
|
-
members = members.concat(this.getMembers(declaration.initializer, errors)); // declaration.initializer.properties.map(member => member.name.getText());
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return members;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Extract the public members and their values from a TypeScript node.
|
|
175
|
-
*
|
|
176
|
-
* @param {ts.Node} node - The node to parse
|
|
177
|
-
* @returns {any[]} - The extracted members
|
|
178
|
-
*/
|
|
179
|
-
getMembers(node) {
|
|
180
|
-
|
|
181
|
-
const members = [];
|
|
182
|
-
|
|
183
|
-
if (ts.isObjectLiteralExpression(node)) {
|
|
184
|
-
node.properties.forEach((property) => {
|
|
185
|
-
|
|
186
|
-
if (ts.isPropertyAssignment(property)) {
|
|
187
|
-
const isValidName = ts.isIdentifier(property.name) || ts.isStringLiteral(property.name);
|
|
188
|
-
const name = isValidName && property.name.text;
|
|
189
|
-
const value = getLiteralValue(property.initializer, this.typeChecker);
|
|
190
|
-
members.push({ [name]: value });
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return members;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export class ParsingError {
|
|
2
|
-
constructor(node, message) {
|
|
3
|
-
this.node = node; // AST node which caused the error
|
|
4
|
-
this.message = message; // Description of the error
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
toString() {
|
|
8
|
-
// Ensure node.location exists and has the necessary properties
|
|
9
|
-
const file = this.node.getSourceFile();
|
|
10
|
-
const fileName = file.fileName;
|
|
11
|
-
const start = this.node.getSourceFile().getLineAndCharacterOfPosition(this.node.getStart());
|
|
12
|
-
return `ParsingError: ${this.message} at ${fileName}:${start.line + 1}:${start.character}`;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
@@ -1,432 +0,0 @@
|
|
|
1
|
-
import * as ts from 'typescript';
|
|
2
|
-
|
|
3
|
-
import { AttributeParser } from './attribute-parser.js';
|
|
4
|
-
import { ParsingError } from './parsing-error.js';
|
|
5
|
-
import { hasTag } from '../utils/attribute-utils.js';
|
|
6
|
-
import { zipArrays } from '../utils/generic-utils.js';
|
|
7
|
-
import { flatMapAnyNodes, getJSDocTags, getLiteralValue, parseArrayLiteral, parseFloatNode } from '../utils/ts-utils.js';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @typedef {object} Attribute
|
|
11
|
-
* @property {string} type - The type of the attribute
|
|
12
|
-
* @property {boolean} array - Whether the attribute is an array type of not
|
|
13
|
-
* @property {string} [description] - The description of the attribute
|
|
14
|
-
* @property {string} [placeholder] - Placeholder for Editor's for field UI.
|
|
15
|
-
* @property {*} [defaultValue] - The default value of the attribute
|
|
16
|
-
* @property {Object<string, number|boolean|string>[]} [enum] - An array of possible values for the attribute
|
|
17
|
-
* @property {{ min: number, max: number}} [range] - Numerical constraints for the attribute
|
|
18
|
-
* @property {number} [precision] - Numerical constraints for the attribute
|
|
19
|
-
* @property {number} [step] - Numerical constraints for the attribute
|
|
20
|
-
* @property {string} [resource] - Name of asset type to be used in 'asset' type attribute
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @typedef {object} ValidationError
|
|
25
|
-
* @property {string} message - The error message
|
|
26
|
-
* @property {string} [file] - The file the error occurred in
|
|
27
|
-
* @property {number} [line] - The line the error occurred on
|
|
28
|
-
* @property {number} [column] - The column the error occurred on
|
|
29
|
-
* @property {number} [length] - The length of the error
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Creates a parser for new expressions.
|
|
34
|
-
*
|
|
35
|
-
* @param {string} name - The name of the type
|
|
36
|
-
* @param {Function} argProcessor - Function to process arguments
|
|
37
|
-
* @param {number[]} [defaultArr] - The default array to use if the node is not a valid array
|
|
38
|
-
* @returns {Function} - The parser function that extracts the values from the node
|
|
39
|
-
*/
|
|
40
|
-
const createNewExpressionParser = (name, argProcessor, defaultArr = []) => {
|
|
41
|
-
return (node) => {
|
|
42
|
-
if (node.kind !== ts.SyntaxKind.NewExpression) {
|
|
43
|
-
if (ts.isPropertyAssignment(node.parent) || ts.isPropertyDeclaration(node.parent)) {
|
|
44
|
-
throw new ParsingError(node, `Property "${node.name.text}" is not a ${name}.`);
|
|
45
|
-
}
|
|
46
|
-
return defaultArr;
|
|
47
|
-
}
|
|
48
|
-
const args = argProcessor(node.arguments);
|
|
49
|
-
|
|
50
|
-
// combine the two arrays together, filling any missing values with the defaultArr
|
|
51
|
-
return zipArrays(args, defaultArr);
|
|
52
|
-
};
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Parses single arguments directly.
|
|
57
|
-
* @param {ts.Node[]} args - The arguments to parse
|
|
58
|
-
* @returns {number[]} - The parsed arguments
|
|
59
|
-
*/
|
|
60
|
-
const parseNumberArguments = args => args.map(arg => parseFloatNode(arg, true));
|
|
61
|
-
|
|
62
|
-
// Creating specific parser instances
|
|
63
|
-
const createNumberArgumentParser = (name, defaultArr = []) => (
|
|
64
|
-
createNewExpressionParser(name, parseNumberArguments, defaultArr));
|
|
65
|
-
|
|
66
|
-
const createArrayArgumentParser = (name, defaultArr = []) => (
|
|
67
|
-
createNewExpressionParser(name, parseArrayLiteral, defaultArr));
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Map of pc types that can be initialized with values and their corresponding argument parsers.
|
|
71
|
-
* eg `new Vec3(1, 2, 3)`
|
|
72
|
-
*/
|
|
73
|
-
const SUPPORTED_INITIALIZABLE_TYPE_NAMES = new Map([
|
|
74
|
-
['Curve', createArrayArgumentParser('Curve')],
|
|
75
|
-
['Vec2', createNumberArgumentParser('Vec2', [0, 0])],
|
|
76
|
-
['Vec3', createNumberArgumentParser('Vec3', [0, 0, 0])],
|
|
77
|
-
['Vec4', createNumberArgumentParser('Vec4', [0, 0, 0, 0])],
|
|
78
|
-
['Color', createNumberArgumentParser('Color', [1, 1, 1, 1])],
|
|
79
|
-
['number', getLiteralValue],
|
|
80
|
-
['string', getLiteralValue],
|
|
81
|
-
['boolean', getLiteralValue]
|
|
82
|
-
]);
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* The full set of supported PlayCanvas types.
|
|
86
|
-
*/
|
|
87
|
-
const SUPPORTED_PLAYCANVAS_TYPES = new Set([
|
|
88
|
-
'Curve',
|
|
89
|
-
'Asset',
|
|
90
|
-
'Entity',
|
|
91
|
-
'Vec2',
|
|
92
|
-
'Vec3',
|
|
93
|
-
'Vec4',
|
|
94
|
-
'Color'
|
|
95
|
-
]);
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Checks if a node is a supported PlayCanvas type.
|
|
99
|
-
* @param {ts.Node} node - The node to check
|
|
100
|
-
* @returns {boolean} - True if the node is a supported PlayCanvas type
|
|
101
|
-
*/
|
|
102
|
-
const isSupportedPlayCanvasNode = (node) => {
|
|
103
|
-
return ts.isClassDeclaration(node) && SUPPORTED_PLAYCANVAS_TYPES.has(node.name.text);
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
// valid values for asset.type
|
|
107
|
-
const EDITOR_ASSET_TYPES = [
|
|
108
|
-
'animation',
|
|
109
|
-
'animstategraph',
|
|
110
|
-
'audio',
|
|
111
|
-
'bundle',
|
|
112
|
-
'binary',
|
|
113
|
-
'container',
|
|
114
|
-
'cubemap',
|
|
115
|
-
'css',
|
|
116
|
-
'font',
|
|
117
|
-
'folder',
|
|
118
|
-
'json',
|
|
119
|
-
'html',
|
|
120
|
-
'gsplat',
|
|
121
|
-
'material',
|
|
122
|
-
'model',
|
|
123
|
-
'render',
|
|
124
|
-
'scene',
|
|
125
|
-
'script',
|
|
126
|
-
'shader',
|
|
127
|
-
'sprite',
|
|
128
|
-
'template',
|
|
129
|
-
'text',
|
|
130
|
-
'texture',
|
|
131
|
-
'textureAtlas',
|
|
132
|
-
'wasm'
|
|
133
|
-
];
|
|
134
|
-
|
|
135
|
-
const resourceTypesAsUnion = EDITOR_ASSET_TYPES.map(type => `"${type}"`).join('|');
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* The set of supported block tags and validators
|
|
139
|
-
*/
|
|
140
|
-
const SUPPORTED_BLOCK_TAGS = new Map([
|
|
141
|
-
['resource', resourceTypesAsUnion],
|
|
142
|
-
['color', '"r"|"rg"|"rgb"|"rgba"'],
|
|
143
|
-
['curves', '["x"] | ["x", "y"] | ["x", "y", "z"] | ["x", "z"] | ["y"] | ["y", "z"] | ["z"]'],
|
|
144
|
-
['range', 'number[]'],
|
|
145
|
-
['placeholder', 'string'],
|
|
146
|
-
['precision', 'number'],
|
|
147
|
-
['size', 'number'],
|
|
148
|
-
['step', 'number'],
|
|
149
|
-
['title', 'string'],
|
|
150
|
-
['default', 'any'],
|
|
151
|
-
['enabledif', 'any'],
|
|
152
|
-
['visibleif', 'any']
|
|
153
|
-
]);
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* A mapping function that transforms the Script Parsers internal representation of an attribute to a required output.
|
|
157
|
-
* @param {*} attribute - The attribute to map
|
|
158
|
-
* @returns {*} - The mapped attribute
|
|
159
|
-
*/
|
|
160
|
-
const mapAttributesToOutput = (attribute) => {
|
|
161
|
-
|
|
162
|
-
attribute.type = attribute.typeName.toLowerCase();
|
|
163
|
-
|
|
164
|
-
// Flatten the range into min and max
|
|
165
|
-
if (attribute.range) {
|
|
166
|
-
if (attribute.range.length > 0) {
|
|
167
|
-
attribute.min = attribute.range[0];
|
|
168
|
-
if (attribute.range.length > 1) {
|
|
169
|
-
attribute.max = attribute.range[1];
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
delete attribute.range;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// We have a complex type, map to json schema
|
|
176
|
-
if (typeof attribute.attributes == 'object') {
|
|
177
|
-
attribute.type = 'json';
|
|
178
|
-
attribute.schema = Object.keys(attribute.attributes).map((key) => {
|
|
179
|
-
return attribute.attributes[key];
|
|
180
|
-
});
|
|
181
|
-
delete attribute.attributes;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// 'color' type needs to be mapped to 'rgba'
|
|
185
|
-
if (attribute.type === 'color') {
|
|
186
|
-
attribute.type = 'rgba';
|
|
187
|
-
}
|
|
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
|
-
|
|
195
|
-
// remove enum if it's empty
|
|
196
|
-
if (attribute.enum.length === 0) delete attribute.enum;
|
|
197
|
-
|
|
198
|
-
// If the attribute has no default value then set it
|
|
199
|
-
if (attribute.value === undefined) {
|
|
200
|
-
if (attribute.type === 'string') attribute.value = '';
|
|
201
|
-
if (attribute.type === 'number') attribute.value = 0;
|
|
202
|
-
if (attribute.type === 'boolean') attribute.value = false;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// set the default value
|
|
206
|
-
if (attribute.value !== undefined) attribute.default = attribute.default ?? attribute.value;
|
|
207
|
-
|
|
208
|
-
// Curve Attributes specifically should not expose a default value if it's an empty array
|
|
209
|
-
if (attribute.type === 'curve' && Array.isArray(attribute.value) && attribute.value.length === 0) {
|
|
210
|
-
delete attribute.default;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// enabledif is in a {}, so strip it out
|
|
214
|
-
if (attribute.enabledif?.startsWith('{') && attribute.enabledif?.endsWith('}')) {
|
|
215
|
-
attribute.enabledif = attribute.enabledif.slice(1, -1);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// enabledif is in a {}, so strip it out
|
|
219
|
-
if (attribute.visibleif?.startsWith('{') && attribute.visibleif?.endsWith('}')) {
|
|
220
|
-
attribute.visibleif = attribute.visibleif.slice(1, -1);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// remove typeName from the output
|
|
224
|
-
delete attribute.typeName;
|
|
225
|
-
delete attribute.value;
|
|
226
|
-
|
|
227
|
-
return attribute;
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* A class to parse ESM Scripts and extract attribute metadata from JSDoc comments.
|
|
232
|
-
* This class uses the TypeScript compiler API to parse the script and extract the metadata.
|
|
233
|
-
*/
|
|
234
|
-
export class ScriptParser {
|
|
235
|
-
/**
|
|
236
|
-
* @param {import('@typescript/vfs').VirtualTypeScriptEnvironment} env - The TypeScript virtual environment
|
|
237
|
-
* @param {number} [maxDepth] - The maximum recursion depth
|
|
238
|
-
*/
|
|
239
|
-
constructor(env, maxDepth = 2) {
|
|
240
|
-
|
|
241
|
-
if (!env) {
|
|
242
|
-
throw new Error('A TypeScript Environment must be provided to the ScriptParser.');
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
this.env = env;
|
|
246
|
-
this.program = env.languageService.getProgram();
|
|
247
|
-
this.typeChecker = this.program.getTypeChecker();
|
|
248
|
-
this.maxDepth = maxDepth;
|
|
249
|
-
|
|
250
|
-
// Initialize the supported types
|
|
251
|
-
const playcanvasTypesFile = this.program.getSourceFile('./playcanvas.d.ts');
|
|
252
|
-
|
|
253
|
-
if (!playcanvasTypesFile) {
|
|
254
|
-
throw new Error('The PlayCanvas types file could not be found.');
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Return an array of all nodes that are supported PlayCanvas types eg Vec3, Color, etc.
|
|
258
|
-
const allSupportedTypes = flatMapAnyNodes(playcanvasTypesFile, isSupportedPlayCanvasNode).map(node => this.typeChecker.getTypeAtLocation(node));
|
|
259
|
-
|
|
260
|
-
// Add primitive types
|
|
261
|
-
allSupportedTypes.push(this.typeChecker.getStringType());
|
|
262
|
-
allSupportedTypes.push(this.typeChecker.getNumberType());
|
|
263
|
-
allSupportedTypes.push(this.typeChecker.getBooleanType());
|
|
264
|
-
|
|
265
|
-
this.typeSerializerMap = new Map();
|
|
266
|
-
this.allSupportedTypesSet = new Set(allSupportedTypes);
|
|
267
|
-
|
|
268
|
-
allSupportedTypes.forEach((type) => {
|
|
269
|
-
const typeName = this.typeChecker.typeToString(type);
|
|
270
|
-
const serializer = SUPPORTED_INITIALIZABLE_TYPE_NAMES.get(typeName);
|
|
271
|
-
if (serializer) {
|
|
272
|
-
this.typeSerializerMap.set(typeName, serializer);
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
this.attributeParser = new AttributeParser(SUPPORTED_BLOCK_TAGS, env, this.typeSerializerMap);
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Checks a program for compile errors and returns a list of formatted error messages.
|
|
282
|
-
* @param {ValidationError[]} errors - The array to store error messages
|
|
283
|
-
* @returns {boolean} - True if the program has no errors, false otherwise
|
|
284
|
-
*/
|
|
285
|
-
validateProgram(errors = []) {
|
|
286
|
-
|
|
287
|
-
// Report any compiler errors
|
|
288
|
-
const compilerDiagnostics = this.program.getSyntacticDiagnostics();
|
|
289
|
-
|
|
290
|
-
if (compilerDiagnostics.length > 0) {
|
|
291
|
-
for (const diagnostic of compilerDiagnostics) {
|
|
292
|
-
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
|
|
293
|
-
if (diagnostic.file) {
|
|
294
|
-
const location = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
|
|
295
|
-
errors.push({
|
|
296
|
-
message,
|
|
297
|
-
file: diagnostic.file.fileName,
|
|
298
|
-
line: location.line + 1,
|
|
299
|
-
column: location.character + 1,
|
|
300
|
-
length: diagnostic.length
|
|
301
|
-
});
|
|
302
|
-
} else {
|
|
303
|
-
errors.push({
|
|
304
|
-
message
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return errors.length === 0;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* @typedef {Object} ParserOptions
|
|
315
|
-
* @property {object} [attributes] - The attributes object to add to
|
|
316
|
-
* @property {ParsingError[]} [errors] - The array to store parsing errors
|
|
317
|
-
* @property {boolean} [requiresAttributeTag] - Whether the attribute requires an @attribute tag
|
|
318
|
-
* @property {number} [depth] - The current recursion depth
|
|
319
|
-
*/
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Extracts Script Attributes from JSDoc style tag from a AST node
|
|
323
|
-
*
|
|
324
|
-
* @param {import('typescript').Node} node - The TypeScript node to analyze
|
|
325
|
-
* @param {ParserOptions} [opts] - The input options
|
|
326
|
-
* @returns {Attribute[]} - The extracted attributes;
|
|
327
|
-
*/
|
|
328
|
-
extractAttributes(node, opts = {}) {
|
|
329
|
-
|
|
330
|
-
// Default params
|
|
331
|
-
const attributes = opts.attributes || {};
|
|
332
|
-
const errors = opts.errors ?? [];
|
|
333
|
-
const requiresAttributeTag = opts.requiresAttributeTag ?? true;
|
|
334
|
-
const depth = opts.depth ?? 0;
|
|
335
|
-
|
|
336
|
-
// return early if we've reached the maximum depth
|
|
337
|
-
if (depth > 10) {
|
|
338
|
-
return attributes;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Find "/** */" style comments associated with this node.
|
|
342
|
-
const members = getJSDocTags(node, this.typeChecker);
|
|
343
|
-
|
|
344
|
-
// Parse the comments for attribute metadata
|
|
345
|
-
for (const { member, memberName } of members) {
|
|
346
|
-
|
|
347
|
-
// Parse the comment for attribute metadata
|
|
348
|
-
const attributeMetadata = this.attributeParser.parseAttributeComment(
|
|
349
|
-
member,
|
|
350
|
-
errors,
|
|
351
|
-
requiresAttributeTag
|
|
352
|
-
);
|
|
353
|
-
|
|
354
|
-
// If we found metadata, extract the type and add it to the found attributes
|
|
355
|
-
if (attributeMetadata) {
|
|
356
|
-
|
|
357
|
-
const { type, typeName } = attributeMetadata;
|
|
358
|
-
const initializer = member?.initializer;
|
|
359
|
-
|
|
360
|
-
if (typeName === 'any') {
|
|
361
|
-
|
|
362
|
-
const error = new ParsingError(member, `The attribute "${memberName}" is not initialized and does not have a @type tag.`);
|
|
363
|
-
errors.push(error);
|
|
364
|
-
continue;
|
|
365
|
-
|
|
366
|
-
} else if (!this.allSupportedTypesSet.has(type) && attributeMetadata.enum.length === 0) {
|
|
367
|
-
// If this is a complex type, but not an enum, we may need to recurse through it's nested nested properties
|
|
368
|
-
|
|
369
|
-
// extract attributes for the Types properties
|
|
370
|
-
const symbol = type.aliasSymbol || type.symbol;
|
|
371
|
-
const typeNode = symbol?.valueDeclaration || symbol.declarations?.[0];
|
|
372
|
-
|
|
373
|
-
if (!typeNode) {
|
|
374
|
-
// The attribute does not have a valid type
|
|
375
|
-
continue;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Type constraints: Must be either an interface, typedef or initialized object literal, or a jsdoc type literal
|
|
379
|
-
const typeIsJSDocTypeDef = ts.isJSDocTypedefTag(typeNode);
|
|
380
|
-
const typeIsInterface = hasTag('interface', typeNode) || ts.isInterfaceDeclaration(typeNode);
|
|
381
|
-
const isInitialized = !!initializer && ts.isObjectLiteralExpression(initializer);
|
|
382
|
-
const isJSDocTypeLiteral = type.symbol.name === '__type';
|
|
383
|
-
|
|
384
|
-
// Check if the type is a valid interface
|
|
385
|
-
if (!isInitialized && !typeIsInterface && !typeIsJSDocTypeDef && !isJSDocTypeLiteral) {
|
|
386
|
-
const error = new ParsingError(member, `Attribute "${memberName}" is an invalid type.`);
|
|
387
|
-
errors.push(error);
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Extract the attributes from the nested type
|
|
392
|
-
const members = typeNode.members ?? // interface members
|
|
393
|
-
typeNode.properties ?? // an object literals properties
|
|
394
|
-
Array.from(type.symbol.members).map(arr => arr[1].declarations[0]) ?? // a typedefs property declarations
|
|
395
|
-
[];
|
|
396
|
-
|
|
397
|
-
// If the type is an interface or an initialized object, we need to parse the attribute comments, if not, just the intitialized object
|
|
398
|
-
let commentAttributes;
|
|
399
|
-
if (typeIsInterface || isInitialized) {
|
|
400
|
-
commentAttributes = this.extractAttributes(typeNode, { errors, requiresAttributeTag: false, depth: depth + 1 });
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Iterate through the nested attributes and extract their metadata
|
|
404
|
-
const nestedAttributes = members.reduce((attributes, prop) => {
|
|
405
|
-
|
|
406
|
-
const attribute = this.attributeParser.getNodeAsAttribute(prop, errors);
|
|
407
|
-
|
|
408
|
-
// If the attribute is a supported type, add it to the list of found attributes
|
|
409
|
-
if (attribute && this.allSupportedTypesSet.has(attribute.type)) {
|
|
410
|
-
attributes[attribute.name] = mapAttributesToOutput(attribute);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
return attributes;
|
|
414
|
-
}, {});
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
// Merge the property and comment attributes
|
|
418
|
-
const mergedAttributes = { ...nestedAttributes, ...commentAttributes };
|
|
419
|
-
|
|
420
|
-
// Add the nested attributes to the attribute metadata
|
|
421
|
-
attributeMetadata.attributes = mergedAttributes;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Add the attribute to the list of found attributes
|
|
425
|
-
attributes[memberName] = mapAttributesToOutput(attributeMetadata);
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
return attributes;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { getCombinedModifierFlags, ModifierFlags } from 'typescript';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @file Utility functions for parsing Script attributes.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Returns a jsdoc tag from a JSDoc comment.
|
|
10
|
-
* @param {string} tag - The tag to search for
|
|
11
|
-
* @param {import('typescript').Node} doc - The JSDoc comment node
|
|
12
|
-
* @returns {import('typescript').Node} | null} - The tag node
|
|
13
|
-
*/
|
|
14
|
-
export const getTagFromJsdoc = (tag, doc) => {
|
|
15
|
-
return doc?.tags?.find(doc => doc.tagName.text === tag);
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Checks if a node has a specific JSDoc tag.
|
|
20
|
-
* @param {string} tag - The tag to search for
|
|
21
|
-
* @param {import("typescript").Node} node - The node to analyze
|
|
22
|
-
* @returns {import('typescript').Node} - True if the tag is found
|
|
23
|
-
*/
|
|
24
|
-
export const getTag = (tag, node) => {
|
|
25
|
-
return node?.jsDoc?.find(doc => getTagFromJsdoc(tag, doc));
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Checks if a node has a specific JSDoc tag.
|
|
30
|
-
* @param {string} tag - The tag to search for
|
|
31
|
-
* @param {import("typescript").Node} node - The node to analyze
|
|
32
|
-
* @returns {boolean} - True if the tag is found
|
|
33
|
-
*/
|
|
34
|
-
export const hasTag = (tag, node) => {
|
|
35
|
-
return !!getTag(tag, node);
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Checks if the JSDoc node node has an '@interface' tag
|
|
41
|
-
* @param {import('typescript').Node} node - The node to analyze
|
|
42
|
-
* @returns {boolean} - True if the node is an interface
|
|
43
|
-
*/
|
|
44
|
-
export const isInterface = (node) => {
|
|
45
|
-
return hasTag('interface', node);
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Checks if a node is a scriptName property
|
|
50
|
-
* @param {import('typescript').Node} member - The node to analyze
|
|
51
|
-
* @returns {boolean} - True if the node is a scriptName property
|
|
52
|
-
*/
|
|
53
|
-
export const isStaticMember = member => (getCombinedModifierFlags(member) & ModifierFlags.Static) !== 0;
|