@playcanvas/attribute-parser 1.6.1 → 1.7.1
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 +445 -0
- package/dist/libs.d.ts +50978 -0
- package/package.json +50 -10
- 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/index.js +0 -273
- 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,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;
|
package/src/utils/tag-utils.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { DiagnosticCategory } from 'typescript';
|
|
2
|
-
|
|
3
|
-
import { parseNumber, parseStringToNumericalArray } from './ts-utils.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Parses a tag value into a boolean, number, array or string
|
|
7
|
-
* @param {string} input - The tag value to parse
|
|
8
|
-
* @returns {boolean|number|string|Array<any>} - The parsed value
|
|
9
|
-
*/
|
|
10
|
-
export function parseTag(input = '') {
|
|
11
|
-
|
|
12
|
-
// Remove any leading or trailing whitespace, replace single quotes with double quotes
|
|
13
|
-
input = input.replace(/['`]/g, '"').trim();
|
|
14
|
-
|
|
15
|
-
// Check for boolean
|
|
16
|
-
if (input === 'true') return true;
|
|
17
|
-
if (input === 'false') return false;
|
|
18
|
-
|
|
19
|
-
// Check for number
|
|
20
|
-
const num = parseNumber(input);
|
|
21
|
-
if (!isNaN(num)) return num;
|
|
22
|
-
|
|
23
|
-
// Check for numerical array
|
|
24
|
-
try {
|
|
25
|
-
const arr = parseStringToNumericalArray(input);
|
|
26
|
-
if (Array.isArray(arr)) return arr;
|
|
27
|
-
} catch { }
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const arr = JSON.parse(input);
|
|
31
|
-
if (Array.isArray(arr)) return arr;
|
|
32
|
-
} catch { }
|
|
33
|
-
|
|
34
|
-
// If none of the above, return the string
|
|
35
|
-
return input;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Validates that a tag value matches the expected type
|
|
40
|
-
*
|
|
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.
|
|
46
|
-
*/
|
|
47
|
-
export function validateTag(value, typeAnnotation, env) {
|
|
48
|
-
const virtualFileName = '/___virtual__.ts';
|
|
49
|
-
const sourceText = `let a: ${typeAnnotation} = ${value};`;
|
|
50
|
-
|
|
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;
|
|
56
|
-
|
|
57
|
-
// Fetch semantic diagnostics only for the virtual file
|
|
58
|
-
const errors = languageService.getSemanticDiagnostics(virtualFileName);
|
|
59
|
-
|
|
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
|
-
);
|
|
64
|
-
|
|
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}`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// If no type errors are found, return true indicating successful validation
|
|
72
|
-
return true;
|
|
73
|
-
}
|