@playcanvas/attribute-parser 1.0.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.
Files changed (56) hide show
  1. package/.github/workflows/ci.yml +28 -0
  2. package/README.md +76 -0
  3. package/package.json +24 -0
  4. package/src/index.js +226 -0
  5. package/src/parsers/attribute-parser.js +280 -0
  6. package/src/parsers/parsing-error.js +14 -0
  7. package/src/parsers/script-parser.js +404 -0
  8. package/src/utils/attribute-utils.js +72 -0
  9. package/src/utils/generic-utils.js +18 -0
  10. package/src/utils/tag-utils.js +65 -0
  11. package/src/utils/ts-utils.js +501 -0
  12. package/test/fixtures/asset.invalid.js +26 -0
  13. package/test/fixtures/asset.valid.js +25 -0
  14. package/test/fixtures/checkbox.valid.js +23 -0
  15. package/test/fixtures/color.valid.js +23 -0
  16. package/test/fixtures/curve.invalid.js +28 -0
  17. package/test/fixtures/curve.valid.js +49 -0
  18. package/test/fixtures/entity.valid.js +23 -0
  19. package/test/fixtures/enum.valid.js +78 -0
  20. package/test/fixtures/export.import.js +3 -0
  21. package/test/fixtures/export.valid.js +23 -0
  22. package/test/fixtures/inherit.valid.js +52 -0
  23. package/test/fixtures/intellisense.valid.js +37 -0
  24. package/test/fixtures/json.invalid.js +22 -0
  25. package/test/fixtures/json.valid.js +103 -0
  26. package/test/fixtures/misc.invalid.js +25 -0
  27. package/test/fixtures/misc.valid.js +33 -0
  28. package/test/fixtures/numeric.invalid.js +38 -0
  29. package/test/fixtures/numeric.valid.js +63 -0
  30. package/test/fixtures/playcanvas.d.ts +47963 -0
  31. package/test/fixtures/program.invalid.js +11 -0
  32. package/test/fixtures/text.valid.js +30 -0
  33. package/test/fixtures/vector.invalid.js +13 -0
  34. package/test/fixtures/vector.valid.js +23 -0
  35. package/test/tests/invalid/asset.test.js +48 -0
  36. package/test/tests/invalid/curve.test.js +24 -0
  37. package/test/tests/invalid/json.test.js +28 -0
  38. package/test/tests/invalid/misc.test.js +42 -0
  39. package/test/tests/invalid/numeric.test.js +56 -0
  40. package/test/tests/invalid/program.test.js +18 -0
  41. package/test/tests/invalid/vector.test.js +29 -0
  42. package/test/tests/valid/asset.test.js +51 -0
  43. package/test/tests/valid/checkbox.test.js +50 -0
  44. package/test/tests/valid/color.test.js +50 -0
  45. package/test/tests/valid/curve.test.js +77 -0
  46. package/test/tests/valid/entity.test.js +50 -0
  47. package/test/tests/valid/enum.test.js +74 -0
  48. package/test/tests/valid/export.test.js +56 -0
  49. package/test/tests/valid/inherit.test.js +103 -0
  50. package/test/tests/valid/intellisense.test.js +36 -0
  51. package/test/tests/valid/json.test.js +150 -0
  52. package/test/tests/valid/misc.test.js +47 -0
  53. package/test/tests/valid/numeric.test.js +104 -0
  54. package/test/tests/valid/text.test.js +59 -0
  55. package/test/tests/valid/vector.test.js +54 -0
  56. package/test/utils.js +53 -0
@@ -0,0 +1,404 @@
1
+ import ts from 'typescript';
2
+ import { TextRange } from '@microsoft/tsdoc';
3
+
4
+ import { AttributeParser } from './attribute-parser.js';
5
+ import { ParsingError } from './parsing-error.js';
6
+ import { zipArrays } from '../utils/generic-utils.js';
7
+ import { hasTag } from '../utils/attribute-utils.js';
8
+ import { flatMapAnyNodes, getJSDocCommentRanges, parseArrayLiteral, parseBooleanNode, parseFloatNode, parseStringNode } from "../utils/ts-utils.js";
9
+
10
+ /**
11
+ * @typedef {object} Attribute
12
+ * @property {string} type - The type of the attribute
13
+ * @property {boolean} array - Whether the attribute is an array type of not
14
+ * @property {string} [description] - The description of the attribute
15
+ * @property {string} [placeholder] - Placeholder for Editor's for field UI.
16
+ * @property {*} [defaultValue] - The default value of the attribute
17
+ * @property {Object<string, number|boolean|string>[]} [enum] - An array of possible values for the attribute
18
+ * @property {{ min: number, max: number}} [range] - Numerical constraints for the attribute
19
+ * @property {number} [precision] - Numerical constraints for the attribute
20
+ * @property {number} [step] - Numerical constraints for the attribute
21
+ * @property {string} [resource] - Name of asset type to be used in 'asset' type attribute
22
+ */
23
+
24
+ /**
25
+ * @typedef {object} ValidationError
26
+ * @property {string} message - The error message
27
+ * @property {string} [file] - The file the error occurred in
28
+ * @property {number} [line] - The line the error occurred on
29
+ * @property {number} [column] - The column the error occurred on
30
+ * @property {number} [length] - The length of the error
31
+ */
32
+
33
+ /**
34
+ * Creates a parser for new expressions.
35
+ *
36
+ * @param {string} name - The name of the type
37
+ * @param {Function} argProcessor - Function to process arguments
38
+ * @param {number[]} [defaultArr] - The default array to use if the node is not a valid array
39
+ * @returns {Function} - The parser function that extracts the values from the node
40
+ */
41
+ const createNewExpressionParser = (name, argProcessor, defaultArr = []) => {
42
+ return (node) => {
43
+ if (node.kind !== ts.SyntaxKind.NewExpression) {
44
+ if (ts.isPropertyAssignment(node.parent) || ts.isPropertyDeclaration(node.parent)) {
45
+ throw new ParsingError(node, `Property "${node.name.text}" is not a ${name}.`);
46
+ }
47
+ return defaultArr;
48
+ }
49
+ const args = argProcessor(node.arguments);
50
+
51
+ // combine the two arrays together, filling any missing values with the defaultArr
52
+ return zipArrays(args, defaultArr);
53
+ };
54
+ };
55
+
56
+ /**
57
+ * Parses single arguments directly.
58
+ * @param {ts.Node[]} args - The arguments to parse
59
+ * @returns {number[]} - The parsed arguments
60
+ */
61
+ const parseNumberArguments = args => args.map(arg => parseFloatNode(arg, true));
62
+
63
+ // Creating specific parser instances
64
+ const createNumberArgumentParser = (name, defaultArr = []) => (
65
+ createNewExpressionParser(name, parseNumberArguments, defaultArr));
66
+
67
+ const createArrayArgumentParser = (name, defaultArr = []) => (
68
+ createNewExpressionParser(name, parseArrayLiteral, defaultArr));
69
+
70
+ /**
71
+ * Map of pc types that can be initialized with values and their corresponding argument parsers.
72
+ * eg `new Vec3(1, 2, 3)`
73
+ */
74
+ const SUPPORTED_INITIALIZABLE_TYPE_NAMES = new Map([
75
+ ["Curve", createArrayArgumentParser("Curve")],
76
+ ["Vec2", createNumberArgumentParser("Vec2", [0, 0])],
77
+ ["Vec3", createNumberArgumentParser("Vec3", [0, 0, 0])],
78
+ ["Vec4", createNumberArgumentParser("Vec4", [0, 0, 0, 0])],
79
+ ["Color", createNumberArgumentParser("Color", [1, 1, 1, 1])],
80
+ ["number", parseFloatNode],
81
+ ["string", parseStringNode],
82
+ ["boolean", parseBooleanNode]
83
+ ]);
84
+
85
+ /**
86
+ * The full set of supported PlayCanvas types.
87
+ */
88
+ const SUPPORTED_PLAYCANVAS_TYPES = new Set([
89
+ "Curve",
90
+ "Asset",
91
+ "Entity",
92
+ "Vec2",
93
+ "Vec3",
94
+ "Vec4",
95
+ "Color"
96
+ ]);
97
+
98
+ /**
99
+ * Checks if a node is a supported PlayCanvas type.
100
+ * @param {ts.Node} node - The node to check
101
+ * @returns {boolean} - True if the node is a supported PlayCanvas type
102
+ */
103
+ const isSupportedPlayCanvasNode = (node) => {
104
+ return ts.isClassDeclaration(node) && SUPPORTED_PLAYCANVAS_TYPES.has(node.name.text);
105
+ };
106
+
107
+ // valid values for asset.type
108
+ const EDITOR_ASSET_TYPES = [
109
+ 'animation',
110
+ 'animstategraph',
111
+ 'audio',
112
+ 'bundle',
113
+ 'binary',
114
+ 'container',
115
+ 'cubemap',
116
+ 'css',
117
+ 'font',
118
+ 'folder',
119
+ 'json',
120
+ 'html',
121
+ 'gsplat',
122
+ 'material',
123
+ 'model',
124
+ 'render',
125
+ 'scene',
126
+ 'script',
127
+ 'shader',
128
+ 'sprite',
129
+ 'template',
130
+ 'text',
131
+ 'texture',
132
+ 'textureAtlas',
133
+ 'wasm'
134
+ ];
135
+
136
+ const resourceTypesAsUnion = EDITOR_ASSET_TYPES.map(type => `"${type}"`).join('|');
137
+
138
+ /**
139
+ * The set of supported block tags and validators
140
+ */
141
+ const SUPPORTED_BLOCK_TAGS = new Map([
142
+ ['resource', resourceTypesAsUnion],
143
+ ['color', '"r"|"rg"|"rgb"|"rgba"'],
144
+ ['curves', '["x"] | ["x", "y"] | ["x", "y", "z"] | ["x", "z"] | ["y"] | ["y", "z"] | ["z"]'],
145
+ ['range', 'number[]'],
146
+ ['placeholder', 'string'],
147
+ ['precision', 'number'],
148
+ ['size', 'number'],
149
+ ['step', 'number'],
150
+ ['title', 'string']
151
+ ]);
152
+
153
+ /**
154
+ * A mapping function that transforms the Script Parsers internal representation of an attribute to a required output.
155
+ * @param {*} attribute - The attribute to map
156
+ * @returns {*} - The mapped attribute
157
+ */
158
+ const mapAttributesToOutput = (attribute) => {
159
+
160
+ attribute.type = attribute.typeName.toLowerCase();
161
+
162
+ // Flatten the range into min and max
163
+ if (attribute.range) {
164
+ if (attribute.range.length > 0) {
165
+ attribute.min = attribute.range[0];
166
+ if (attribute.range.length > 1) {
167
+ attribute.max = attribute.range[1];
168
+ }
169
+ }
170
+ delete attribute.range;
171
+ }
172
+
173
+ // We have a complex type, map to json schema
174
+ if (typeof attribute.attributes == 'object') {
175
+ attribute.type = 'json';
176
+ attribute.schema = Object.keys(attribute.attributes).map((key) => {
177
+ return attribute.attributes[key];
178
+ });
179
+ delete attribute.attributes;
180
+ }
181
+
182
+ // 'color' type needs to be mapped to 'rgba'
183
+ if (attribute.type === 'color') {
184
+ attribute.type = 'rgba';
185
+ }
186
+
187
+ // remove enum if it's empty
188
+ if (attribute.enum.length === 0) delete attribute.enum;
189
+
190
+ // set the default value
191
+ if (attribute.value !== undefined) attribute.default = attribute.value;
192
+
193
+ // remove typeName from the output
194
+ delete attribute.typeName;
195
+ delete attribute.value;
196
+
197
+ return attribute;
198
+ };
199
+
200
+ /**
201
+ * A class to parse ESM Scripts and extract attribute metadata from JSDoc comments.
202
+ * This class uses the TypeScript compiler API to parse the script and extract the metadata.
203
+ */
204
+ export class ScriptParser {
205
+ /**
206
+ * @param {import('@typescript/vfs').VirtualTypeScriptEnvironment} env - The TypeScript virtual environment
207
+ * @param {number} [maxDepth] - The maximum recursion depth
208
+ */
209
+ constructor(env, maxDepth = 2) {
210
+
211
+ if (!env) {
212
+ throw new Error('A TypeScript Environment must be provided to the ScriptParser.');
213
+ }
214
+
215
+ this.env = env;
216
+ this.program = env.languageService.getProgram();
217
+ this.typeChecker = this.program.getTypeChecker();
218
+ this.maxDepth = maxDepth;
219
+
220
+ // Initialize the supported types
221
+ const playcanvasTypesFile = this.program.getSourceFile('./playcanvas.d.ts');
222
+
223
+ if (!playcanvasTypesFile) {
224
+ throw new Error('The PlayCanvas types file could not be found.');
225
+ }
226
+
227
+ // Return an array of all nodes that are supported PlayCanvas types eg Vec3, Color, etc.
228
+ const allSupportedTypes = flatMapAnyNodes(playcanvasTypesFile, isSupportedPlayCanvasNode).map(node => this.typeChecker.getTypeAtLocation(node));
229
+
230
+ // Add primitive types
231
+ allSupportedTypes.push(this.typeChecker.getStringType());
232
+ allSupportedTypes.push(this.typeChecker.getNumberType());
233
+ allSupportedTypes.push(this.typeChecker.getBooleanType());
234
+
235
+ this.typeSerializerMap = new Map();
236
+ this.allSupportedTypesSet = new Set(allSupportedTypes);
237
+
238
+ allSupportedTypes.forEach((type) => {
239
+ const typeName = this.typeChecker.typeToString(type);
240
+ const serializer = SUPPORTED_INITIALIZABLE_TYPE_NAMES.get(typeName);
241
+ if (serializer) {
242
+ this.typeSerializerMap.set(type, serializer);
243
+ }
244
+ });
245
+
246
+ this.attributeParser = new AttributeParser(SUPPORTED_BLOCK_TAGS, env, this.typeSerializerMap);
247
+
248
+ }
249
+
250
+ /**
251
+ * Checks a program for compile errors and returns a list of formatted error messages.
252
+ * @param {ValidationError[]} errors - The array to store error messages
253
+ * @returns {boolean} - True if the program has no errors, false otherwise
254
+ */
255
+ validateProgram(errors = []) {
256
+
257
+ // Report any compiler errors
258
+ const compilerDiagnostics = this.program.getSemanticDiagnostics();
259
+ if (compilerDiagnostics.length > 0) {
260
+ for (const diagnostic of compilerDiagnostics) {
261
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
262
+ if (diagnostic.file) {
263
+ const location = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
264
+ errors.push({
265
+ message,
266
+ file: diagnostic.file.fileName,
267
+ line: location.line + 1,
268
+ column: location.character + 1,
269
+ length: diagnostic.length
270
+ });
271
+ } else {
272
+ errors.push({
273
+ message
274
+ });
275
+ }
276
+ }
277
+ }
278
+
279
+ return errors.length === 0;
280
+ }
281
+
282
+ /**
283
+ * @typedef {Object} ParserOptions
284
+ * @property {object} [attributes] - The attributes object to add to
285
+ * @property {ParsingError[]} [errors] - The array to store parsing errors
286
+ * @property {boolean} [requiresAttributeTag] - Whether the attribute requires an @attribute tag
287
+ * @property {number} [depth] - The current recursion depth
288
+ */
289
+
290
+ /**
291
+ * Extracts Script Attributes from JSDoc style tag from a AST node
292
+ *
293
+ * @param {import('typescript').Node} node - The TypeScript node to analyze
294
+ * @param {ParserOptions} [opts] - The input options
295
+ * @returns {Attribute[]} - The extracted attributes;
296
+ */
297
+ extractAttributes(node, opts = {}) {
298
+
299
+ // Default params
300
+ const attributes = opts.attributes || {};
301
+ const errors = opts.errors ?? [];
302
+ const requiresAttributeTag = opts.requiresAttributeTag ?? true;
303
+ const depth = opts.depth ?? 0;
304
+
305
+ // return early if we've reached the maximum depth
306
+ if (depth > 10) {
307
+ return attributes;
308
+ }
309
+
310
+ const buffer = node.getSourceFile().getFullText();
311
+
312
+ // Find "/** */" style comments associated with this node.
313
+ const comments = getJSDocCommentRanges(node, buffer, this.typeChecker);
314
+
315
+ // Parse the comments for attribute metadata
316
+ for (const comment of comments) {
317
+
318
+ // Parse the comment for attribute metadata
319
+ const attributeMetadata = this.attributeParser.parseAttributeComment(
320
+ TextRange.fromStringRange(buffer, comment.range.pos, comment.range.end),
321
+ comment.member,
322
+ errors,
323
+ requiresAttributeTag
324
+ );
325
+
326
+ // If we found metadata, extract the type and add it to the found attributes
327
+ if (attributeMetadata) {
328
+
329
+ const { type, typeName } = attributeMetadata;
330
+ const initializer = comment.member?.initializer;
331
+
332
+ if (typeName === 'any') {
333
+
334
+ const error = new ParsingError(comment.member, `The attribute "${comment.memberName}" is not initialized and does not have a @type tag.`);
335
+ errors.push(error);
336
+ continue;
337
+
338
+ } else if (!this.allSupportedTypesSet.has(type) && attributeMetadata.enum.length === 0) {
339
+ // If this is a complex type, but not an enum, we may need to recurse through it's nested nested properties
340
+
341
+ // extract attributes for the Types properties
342
+ const symbol = type.aliasSymbol || type.symbol;
343
+ const typeNode = symbol?.valueDeclaration || symbol.declarations?.[0];
344
+
345
+ if (!typeNode) {
346
+ // The attribute does not have a valid type
347
+ continue;
348
+ }
349
+
350
+ // Type constraints: Must be either an interface, typedef or initialized object literal, or a jsdoc type literal
351
+ const typeIsJSDocTypeDef = ts.isJSDocTypedefTag(typeNode);
352
+ const typeIsInterface = hasTag('interface', typeNode) || ts.isInterfaceDeclaration(typeNode);
353
+ const isInitialized = !!initializer && ts.isObjectLiteralExpression(initializer);
354
+ const isJSDocTypeLiteral = type.symbol.name === '__type';
355
+
356
+ // Check if the type is a valid interface
357
+ if (!isInitialized && !typeIsInterface && !typeIsJSDocTypeDef && !isJSDocTypeLiteral) {
358
+ const error = new ParsingError(comment.member, `Attribute "${comment.memberName}" is an invalid type.`);
359
+ errors.push(error);
360
+ continue;
361
+ }
362
+
363
+ // Extract the attributes from the nested type
364
+ const members = typeNode.members ?? // interface members
365
+ typeNode.properties ?? // an object literals properties
366
+ Array.from(type.symbol.members).map(arr => arr[1].declarations[0]) ?? // a typedefs property declarations
367
+ [];
368
+
369
+ // If the type is an interface, we need to parse the attribute comments, if not, just the intitialized object
370
+ let commentAttributes;
371
+ if (typeIsInterface) {
372
+ commentAttributes = this.extractAttributes(typeNode, { errors, requiresAttributeTag: false, depth: depth + 1 });
373
+ }
374
+
375
+ // Iterate through the nested attributes and extract their metadata
376
+ const nestedAttributes = members.reduce((attributes, prop) => {
377
+
378
+ const attribute = this.attributeParser.getNodeAsAttribute(prop, errors);
379
+
380
+ // If the attribute is a supported type, add it to the list of found attributes
381
+ if (attribute && this.allSupportedTypesSet.has(attribute.type)) {
382
+ attributes[attribute.name] = mapAttributesToOutput(attribute);
383
+ }
384
+
385
+ return attributes;
386
+ }, {});
387
+
388
+
389
+ // Merge the property and comment attributes
390
+ const mergedAttributes = { ...nestedAttributes, ...commentAttributes };
391
+
392
+ // Add the nested attributes to the attribute metadata
393
+ attributeMetadata.attributes = mergedAttributes;
394
+ }
395
+
396
+ // Add the attribute to the list of found attributes
397
+ attributes[comment.memberName] = mapAttributesToOutput(attributeMetadata);
398
+
399
+ }
400
+ }
401
+
402
+ return attributes;
403
+ }
404
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * @file Utility functions for parsing Script attributes.
3
+ */
4
+
5
+
6
+ /**
7
+ * Returns a tsdoc tag from a JSDoc comment.
8
+ * @param {string} tag - The tag to search for
9
+ * @param {import('typescript').Node} doc - The JSDoc comment node
10
+ * @returns {import('@microsoft/tsdoc').DocNode | null} - The tag node
11
+ */
12
+ export const getTagFromJsdoc = (tag, doc) => {
13
+ return doc?.tags?.find(doc => doc.tagName.text === tag);
14
+ };
15
+
16
+
17
+ /**
18
+ * Checks if a node has a specific JSDoc tag.
19
+ * @param {string} tag - The tag to search for
20
+ * @param {import("typescript").Node} node - The node to analyze
21
+ * @returns {boolean} - True if the tag is found
22
+ */
23
+ export const hasTag = (tag, node) => {
24
+ return !!getTag(tag, node);
25
+ };
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 {import('typescript').Node} - True if the tag is found
33
+ */
34
+ export const getTag = (tag, node) => {
35
+ return node?.jsDoc?.find(doc => getTagFromJsdoc(tag, doc));
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
+ // * Parses a type tag in the format "{type}".
50
+ // * @param {import('@microsoft/tsdoc').DocSection} content - The content of the tag.
51
+ // * @param {object} metadata - An object to store the extracted metadata.
52
+ // * @param {{ node: import('typescript').Node, env: tsdoc.Environment, errors: string[] }} opts - Additional parser options
53
+ // * @returns {{ type: import('typescript').Type | null, array: boolean | null }} - The extracted type and array flag.
54
+ // */
55
+ // export function parseTypeTag(content, metadata, { node, env, errors }) {
56
+
57
+ // // Extract the full source text of the original file
58
+ // const originalSourceText = node.getSourceFile().getFullText();
59
+ // const typeString = extractTextFromDocNode(content);
60
+ // const sourceText = `${originalSourceText}\nlet a: ${typeString};`;
61
+
62
+ // env.createFile("/virtual.ts", sourceText);
63
+
64
+ // const updatedProgram = env.languageService.getProgram();
65
+ // const newTypeChecker = updatedProgram.getTypeChecker();
66
+
67
+ // const sourceFile = updatedProgram.getSourceFile('/virtual.ts');
68
+
69
+ // const variableStatement = sourceFile.statements[sourceFile.statements.length - 1];
70
+ // const variableDeclaration = variableStatement.declarationList.declarations[0];
71
+ // return getType(variableDeclaration, newTypeChecker);
72
+ // }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * This merges two arrays together, filling in the missing values
3
+ * in the first array with values from the second array.
4
+ *
5
+ * @param {array} a - The first array
6
+ * @param {array} b - The second array
7
+ * @returns {array} - The merged array
8
+ */
9
+ export function zipArrays(a, b) {
10
+ const length = Math.max(a.length, b.length);
11
+ const result = new Array(length);
12
+
13
+ for (let i = 0; i < length; i++) {
14
+ result[i] = i < a.length ? a[i] : b[i];
15
+ }
16
+
17
+ return result;
18
+ }
@@ -0,0 +1,65 @@
1
+ import { parseNumber, parseStringToNumericalArray } from './ts-utils.js';
2
+
3
+ /**
4
+ * Parses a tag value into a boolean, number, array or string
5
+ * @param {string} input - The tag value to parse
6
+ * @returns {boolean|number|string|Array<any>} - The parsed value
7
+ */
8
+ export function parseTag(input = '') {
9
+
10
+ // Remove any leading or trailing whitespace, replace single quotes with double quotes
11
+ input = input.replace(/['`]/g, '"').trim();
12
+
13
+ // Check for boolean
14
+ if (input === 'true') return true;
15
+ if (input === 'false') return false;
16
+
17
+ // Check for number
18
+ const num = parseNumber(input);
19
+ if (!isNaN(num)) return num;
20
+
21
+ // Check for numerical array
22
+ try {
23
+ const arr = parseStringToNumericalArray(input);
24
+ if (Array.isArray(arr)) return arr;
25
+ } catch (e) {}
26
+
27
+ try {
28
+ const arr = JSON.parse(input);
29
+ if (Array.isArray(arr)) return arr;
30
+ } catch (e) {}
31
+
32
+ // If none of the above, return the string
33
+ return input;
34
+ }
35
+
36
+ /**
37
+ * Validates that a tag value matches the expected type
38
+ * @throws {Error} - If the tag value is not the expected type
39
+ *
40
+ * @param {String} value - The string representation of the value to type check
41
+ * @param {string} typeAnnotation - The expected type
42
+ * @param {import('@typescript/vfs').VirtualTypeScriptEnvironment} env - The environment to validate in
43
+ * @returns {boolean} - Whether the tag value is valid
44
+ */
45
+ export function validateTag(value, typeAnnotation, env) {
46
+
47
+ const sourceText = `let a: ${typeAnnotation} = ${value};`;
48
+ env.createFile("/___virtual__.ts", sourceText);
49
+
50
+ // Get the program and check for semantic errors
51
+ const program = env.languageService.getProgram();
52
+ const errors = program.getSemanticDiagnostics();
53
+
54
+ // Filter against the type errors we're concerned with
55
+ const typeErrors = errors.filter(error => error.category === 1 && error.code === 2322);
56
+
57
+ // return first error
58
+ const typeError = typeErrors[0];
59
+
60
+ if (typeError) {
61
+ throw new Error(`${typeError.messageText}`);
62
+ }
63
+
64
+ return true;
65
+ }